I made PyQt5 APP which create SQlite3 DB and show data from it in QTableView widget.
I have a problem with editing rows in a widget. There are 3 buttons "Add", "Change" and "Delete" that should delete, modify and add new rows to the widget, as well as edit the database itself, but the buttons do not work properly.
"Add" button - after clicking add button, when all new data inputted and Enter clicked, all values is gone and "!" symbol is showing. I need press Add button, input new data in cells, click Enter and all data must save in SQL DB and displayed in QTableView widget in live time.
"Change" button - when change clicked and Enter pressed after cell editing, all data gone. All data must be save in real time is SQL DB after change button press.
3)"Delete" button - this button don't delete row. There is no error, no any answer from app.
How to program the buttons correctly ?
Do I need to use a delegate?
Which programming approach is more preferable when working with a graphical interface and SQL database?
I made a reproducible example. At startup, a database with 1 table and 2 rows will be created.
import sys, os, sqlite3
from datetime import datetime
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
from PyQt5.QtCore import *
CONFIG_NAME = 'config.ini'
DB_NAME = 'nsi.db'
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__()
self.window_pref()
self.show_widgets()
def window_pref(self):
self.setWindowTitle('PyQt5 APP')
self.def_width = 800
self.def_height = 400
self.def_size = self.setMinimumSize(self.def_width, self.def_height)
def show_widgets(self):
self.createConnection()
self.fillDB()
self.setupMainWidgets()
def createConnection(self):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(DB_NAME)
if not db.open():
QMessageBox.warning(self, 'PyQt5 APP',
'Error:{}'.format(db.lastError().text()))
sys.exit(1)
def fillDB(self):
query = QSqlQuery()
query.exec_("""\
CREATE TABLE sprav (
id_nsi INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
nsi_name TEXT UNIQUE NOT NULL,
file_date TEXT NOT NULL,
file_name TEXT NOT NULL)
""")
query.prepare("""\
INSERT INTO sprav (nsi_name, file_date, file_name)VALUES (?, ?, ?)
""")
sample_list = (('nsi1', 'january', 'file1'), ('nsi2', 'may', 'file2'))
for i in sample_list:
query.addBindValue(i[0])
query.addBindValue(i[1])
query.addBindValue(i[2])
query.exec_()
def setupMainWidgets(self):
mw_widget = QWidget()
main_panel = QHBoxLayout(mw_widget)
# SQL Table
self.modelSql = QSqlTableModel()
self.modelSql.setTable('sprav')
self.modelSql.setQuery(QSqlQuery(
'SELECT nsi_name, file_date, file_name FROM sprav'))
self.modelSql.setHeaderData(self.modelSql.fieldIndex('nsi_name'),
Qt.Horizontal, 'Name')
self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_date'),
Qt.Horizontal, 'Date')
self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_name'),
Qt.Horizontal, 'File')
self.modelSql.setEditStrategy(QSqlTableModel.OnFieldChange)
self.modelSql.select()
# QTableView()
self.table_view = QTableView()
self.table_view.setSelectionBehavior(1)
self.table_view.setAlternatingRowColors(True)
self.table_view.setModel(self.modelSql)
self.table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
main_panel.addWidget(self.table_view)
# QVBoxLayout()
right_panel = QVBoxLayout()
line = QFrame()
line.setFrameShape(QFrame.HLine)
self.add_record = QPushButton('Add', self)
self.add_record.clicked.connect(self.addRecord)
self.change_record = QPushButton('Change', self)
self.change_record.clicked.connect(self.changeRecord)
self.delete_record = QPushButton('Delete', self)
self.delete_record.clicked.connect(self.delRecord)
right_panel.addSpacing(20)
right_panel.addWidget(line)
right_panel.addWidget(self.add_record)
right_panel.addWidget(self.change_record)
right_panel.addWidget(self.delete_record)
right_panel.addStretch()
main_panel.addLayout(right_panel)
self.setCentralWidget(mw_widget)
def addRecord(self):
row = self.modelSql.rowCount()
self.modelSql.insertRow(row)
index = self.modelSql.index(row, 0)
self.table_view.setCurrentIndex(index)
self.table_view.edit(index)
def delRecord(self):
cur_item = self.table_view.selectedIndexes()
for index in cur_item:
self.modelSql.removeRow(index.row())
self.modelSql.select()
def changeRecord(self):
self.table_view.edit(self.table_view.currentIndex())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
There are two problems with your code.
First of all, if you use QSqlTableModel, you should not call setQuery():
This function simply calls QSqlQueryModel::setQuery(query). You should normally not call it on a QSqlTableModel. Instead, use setTable(), setSort(), setFilter(), etc., to set up the query.
This is the main reason for which your row addition/deletion/editing didn't work: the model became partially invalid, and any submission was discarded because columns didn't match the records of the table model, which is very important also because the model requires an incremental key that QSqlTableModel is able to use properly on its own.
Remove the setQuery() from your code, and consider that if you did that only to hide a column, then you just have to hide that column:
self.table_view.setColumnHidden(0, True)
Obviously, you have to keep in mind that all column indexes you will use, will now start from 1, since the model also includes the id:
def addRecord(self):
# ...
index = self.modelSql.index(row, 1)
self.table_view.setCurrentIndex(index)
self.table_view.edit(index)
The other problem was the deletion of rows: even after fixing what described above, the number of rows and its order would have been wrong:
when you cycle through the selectedIndexes() you're getting the same row for each selected item: since you used the SelectRows selection behavior, the for loop would have called removeRow() three times the same row, for each selected row;
removal of indexes should always be in reverse, sorted order; consider if you try to remove row 0 and 1: the first iteration would remove row 0, but at that point the previous row 1 would have become the new row 0, so the next iteration would actually delete the row that previously was the third;
The solution is to have a sorted list of unique row numbers, and cycle through them in reversed order:
def delRecord(self):
# create a set of unique row numbers
rows = set([i.row() for i in self.table_view.selectedIndexes()])
# cycle through them in reversed sorting order
for row in sorted(rows, reverse=True):
self.modelSql.removeRow(row)
self.modelSql.select()
Remember to call select() (I know you did, but better safe than sorry), as explained in the documentation about removeRows():
Deletions are submitted immediately to the database. The model retains a blank row for successfully deleted row until refreshed with select().
Unrelated notes: 1. avoid unnecessary and confusing imports: since you're already importing QtWidgets with wildcard, there's no point for from PyQt5 import QtWidgets; you either import the submodule, or its classes; 2. setMinimumSize returns nothing, so self.def_size will be None; if you want to keep a variable for the default size, use self.def_size = QSize(self.def_width, self.def_height) then self.setMinimumSize(self.def_size); 3. do not use sys.exit() inside a Qt app (and from a QWidget class), instead use QApplication.exit(1); 4. use more verbose names that also clarify their type: you have almost identical names for buttons and functions (add_record and addRecord), which is a poor choice in naming (also considering the different writing style): a better choice would be to name the button like add_record_btn, or addRecordBtn to follow the Qt convention;
Related
I am writing a UI for a simulation program which accepts tabular data.
The basic functionality I need is for the user to be able to enter / change data in cells either by directly typing into them, or by pasting data (usually from an excel sheet). The program checks this data and either accepts or rejects it before running the simulation. I also want to let the user type in their own column headers for the table.
Tksheet is an awesome Tkinter add-on, giving an excel-like "feel" to the input frame, but its documentation leaves much to be desired. (For instance: each event generates a different event-information array--see code for two event-processing routines below--but nowhere is it specified what these parameters are. It is left for the user to discover using trial and error, or trying to read the source code--which is not documented either).
I have two specific questions:
Is there any way to not-commit, or to roll back, changes to the table? If my data-tests fail, how do I prevent potentially harmful user input from being entered into the table?
Obviously I can (and do) add a begin_*** event in which I can keep copies of the original values, and then reset the table values if the data testing at the end_*** event fails, but this is wasteful and inelegant. I have a feeling that the set_data_ref_on_destroy property has something to do with such a capability, but the documentation does not explain what this parameter is or how to use it.
How can I change a single column header at a time? The .headers property seems to work only with a full list of headers starting with column 0 (if I run self.sheet.headers([single_value], index = i) it ignores the index parameter and plugs single_value in column 0)
Again, I can set the column headers to something non-default at init and keep a running list of all headers, so that I can reset all the headers on each change, but this is wasteful and inelegant.
In the following code sample I set up a simple table, and bind three user-generated events: one for typing a value to a cell, one for pasting a block of values, and one for adding an option to the right-click menu of a column header, to allow the user to type a name to the column.
from tksheet import Sheet
import tkinter as tk
import tkinter.messagebox as msg
import tkinter.simpledialog as sd
class demo(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.grid_columnconfigure(0, weight=1) # This configures the window's escalators
self.grid_rowconfigure(0, weight=1)
self.frame = tk.Frame(self)
self.frame.grid_columnconfigure(0, weight=1)
self.frame.grid_rowconfigure(0, weight=1)
self.frame.grid(row=0, column=0, sticky="nswe")
self.sheet = Sheet(self.frame, data=[[]]) # set up empty table
self.sheet.grid(row=0, column=0, sticky="nswe")
self.sheet.enable_bindings(bindings= # enable table behavior
("single_select",
"select_all",
"column_select",
"row_select",
"drag_select",
"arrowkeys",
"column_width_resize",
"double_click_column_resize",
"row_height_resize",
"double_click_row_resize",
"right_click_popup_menu",
"rc_select", # rc = right click
"copy",
"cut",
"paste",
"delete",
"undo",
"edit_cell"
))
# Note that options that change the structure/size of the table (e.g. insert/delete col/row) are disabled
# make sure that pasting data won't change table size
self.sheet.set_options(expand_sheet_if_paste_too_big=False)
# bind specific events to my own functions
self.sheet.extra_bindings("end_edit_cell", func=self.cell_edited)
self.sheet.extra_bindings("end_paste", func=self.cells_pasted)
label = "Change column name" # Add option to the right-click menu for column headers
self.sheet.popup_menu_add_command(label, self.column_header_change, table_menu=False, index_menu=False, header_menu=True)
# Event functions
def cell_edited(self, info_tuple):
r, c, key_pressed, updated_value = info_tuple # break the info about the event to individual variables
if check_input(updated_value):
pass # go do stuff with the updated table
else:
msg.showwarning("Input Error", "'" + updated_value + "' is not a legal value")
pass # what do I do here? How do I make tksheet *not* insert the change to the table?
def cells_pasted(self, info_tuple):
key_pressed, rc_tuple, updated_array = info_tuple # break the info about the event to individual variables
r, c = rc_tuple # row & column where paste begins
if check_input(updated_array):
pass # go do stuff with the updated table
else:
msg.showwarning("Input Error", "pasted array contains illegal values")
pass # what do I do here? How do I make tksheet *not* insert the change to the table?
def column_header_change(self):
r, c = self.sheet.get_currently_selected()
col_name = sd.askstring("User Input", "Enter column name:")
if col_name is not None and col_name != "": # if user cancelled (or didn't enter anything), do nothing
self.sheet.headers([col_name], index=c) # This does not work - it always changes the 1st col
self.sheet.redraw()
# from here down is test code
def check_input(value): # instead of actual data testing we let the tester choose a pass/fail response
return msg.askyesno("Instead of input checking","Did input pass entry checks?")
test = demo()
lst = ["hello", "world"]
test.sheet.insert_column(values=lst)
lst = [0, "hello", "yourself"]
test.sheet.insert_column(values=lst)
test.mainloop()
I realize that the original post is now 5 months old, and I'm a relative n00b, but I hope this helps.
Given a tksheet instance 'sheet' that has already been populated with headers ["A"."B"."C"], the following works to change the header "B" to "NEW":
sheet.headers()[1]="NEW"
Hope this helps.
I want to build a record from multiple QT widgets and store it in a database. The following script runs without an error but does not store the record. Part of the problem is that I cannot determine the last ID already in the database, but even if I manually set the ID--nothing is written. I've seen examples online using insertRecord, but the QT documentation suggests using insertRow. Please feel free to correct my approach. I'm new to Python and to Qt.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtSql import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
#Make Database
self.db = QSqlDatabase.addDatabase('QSQLITE')
self.db.setDatabaseName('C:/Users/dle/Documents/example1.sqlite')
self.db.open()
self.db.transaction()
self.db.exec_(
'CREATE TABLE t1'
'(id INTEGER PRIMARY KEY, f1 INTEGER NOT NULL, f2 INTEGER NOT NULL, f3 TEXT NOT NULL)'
)
self.db.exec_("INSERT INTO t1 VALUES(1, 10, 20, 'db works fine')")
self.db.commit()
#Create User Interface
self.f1 = QLineEdit()
self.f2 = QLineEdit()
self.f3 = QLineEdit()
self.storeButton = QPushButton("Store")
self.storeButton.clicked.connect(self.doStore)
vlayout = QVBoxLayout()
vlayout.addWidget(self.f1)
vlayout.addWidget(self.f2)
vlayout.addWidget(self.f3)
vlayout.addWidget(self.storeButton)
widget = QWidget()
widget.setLayout(vlayout)
self.setCentralWidget(widget)
def doStore(self):
self.dbModel = QSqlTableModel(self)
self.dbModel.setTable('t1')
ID = self.dbModel.query().lastInsertId() #this returns "None" even though last ID
#in table is 1
thisRecord = QSqlRecord
thisRecord = self.dbModel.record()
thisRecord.setValue(0, ID) # Does not write, even if ID is integer
print ID
thisRecord.setValue(1, int(self.f1.text()))
print int(self.f1.text())
thisRecord.setValue(2, int(self.f2.text()))
print int(self.f2.text())
thisRecord.setValue(3, self.f3.text())
print self.f3.text()
print thisRecord
self.dbModel.insertRecord(ID, thisRecord) # Does not write, even if ID is integer
# Doesn't record field 0 already have ID?
if __name__ == '__main__':
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.show()
sys.exit(app.exec_())
If you want to insert a row at the end of the table it is not necessary to indicate the index, since according to the docs:
PySide.QtSql.QSqlTableModel.insertRecord(row, record)
Parameters:
row - PySide.QtCore.int
record – PySide.QtSql.QSqlRecord
Return type:
PySide.QtCore.bool
Inserts the record after row . If row is negative, the record will be
appended to the end. Calls PySide.QtSql.QSqlTableModel.insertRows()
and PySide.QtSql.QSqlTableModel.setRecord() internally.
Returns true if the row could be inserted, otherwise false.
From the above we can conclude that we should only use as row = -1.
self.dbModel.insertRecord(-1, record)
The best thing is to create the model once and not every time the doStore function is called, we must also use the QSqlRecord of the model with the help of the record () function as it comes loaded with the field names.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.db = QSqlDatabase.addDatabase('QSQLITE')
self.db.setDatabaseName('example1.sqlite')
self.db.open()
self.db.transaction()
self.db.exec_(
'CREATE TABLE t1'
'(id INTEGER PRIMARY KEY, f1 INTEGER NOT NULL, f2 INTEGER NOT NULL, f3 TEXT NOT NULL)'
)
self.db.commit()
self.db.exec_("INSERT INTO t1 VALUES(1, 10, 20, 'db works fine')")
#Create User Interface
[...]
self.setCentralWidget(widget)
self.dbModel = QSqlTableModel(self)
self.dbModel.setTable('t1')
def doStore(self):
record = self.dbModel.record()
record.setValue(1, int(self.f1.text()))
record.setValue(2, int(self.f2.text()))
record.setValue(3, self.f3.text())
if not self.dbModel.insertRecord(-1, record):
print(self.db.lastError().text())
I need to add new columns to a Tkinter TreeView widget after creating it, but I can't find a way to do it. I've tried using the configure method to modify the columns attribute of the tree, but this resets all columns except the icon column.
The only solution I see is to configure it to have as many columns as I can possibly need and make them all invisible, so that I can make them visible when I need to add one. Is there a better way?
Newbie here:
I had same issue as you. I solved it as below.
hint: Initialise the Treeview 'after' you have your column names available. I couldn't find any solution on the web to add new columns after the initialisation of the Treeview is done.
Example:
--blah bhah code---
def treecols(self,colnames=[],rowdata=[]):
self.tree = ttk.Treeview ( self.frame1,columns=colnames )
self.tree.grid ( )
for eachcol in colnames:
self.tree.heading(column=eachcol,text=eachcol)
self.tree.column(column=eachcol,width=100,minwidth=0)
All the magic is in the add_columns method, the rest is just get a working example.
I hope this answers your question (a bit late but it might help others).
import tkinter
import tkinter.ttk
class GUI():
def __init__(self, master):
self.view = tkinter.ttk.Treeview(master)
self.view.pack()
self.view.heading('#0', text='Name')
self.view.insert('', 'end', text='Foo')
self.view.insert('', 'end', text='Bar')
self.view['columns'] = ('foo')
self.view.heading('foo', text='foo')
self.view.set(self.view.get_children()[0], 'foo', 'test')
self.add_columns(('bar', 'blesh'))
def add_columns(self, columns, **kwargs):
# Preserve current column headers and their settings
current_columns = list(self.view['columns'])
current_columns = {key:self.view.heading(key) for key in current_columns}
# Update with new columns
self.view['columns'] = list(current_columns.keys()) + list(columns)
for key in columns:
self.view.heading(key, text=key, **kwargs)
# Set saved column values for the already existing columns
for key in current_columns:
# State is not valid to set with heading
state = current_columns[key].pop('state')
self.view.heading(key, **current_columns[key])
tk = tkinter.Tk()
GUI(tk)
tk.mainloop()
I am trying to create a table, where it has 2 columns and several rows.
Column1 will be listing all the available mesh/geos in the scene while Column2 will be in the form of combo box per cell where it contains several options - depending on the mesh/geos from Column1, it will lists different shader options in the combobox as it will be reading off from a file. Meaning to say in the table, each item is on a per-row basis.
I am currently having issues with populating the list of mesh/geos into Column1. Suppose my scene has 5 geos - pCube1, pCube2, pCube3, pCube4, pCube5, in my table, I would be expecting the Column0 of its 5 rows to be populated with pCube#, however instead of that, I got pCube5 as my output result instead.
Please see the following code:
from PyQt4 import QtGui, QtCore
from functools import partial
import maya.cmds as cmds
class combo_box( QtGui.QComboBox ):
# For combox
def __init__( self, *args, **kwargs ):
super( combo_box, self ).__init__( *args, **kwargs)
def get_all_geos():
all_geos = cmds.ls(type='mesh')
return all_geos
class TestTable( QtGui.QWidget ):
def __init__( self, parent=None ):
QtGui.QWidget.__init__( self, parent )
self.setLayout( QtGui.QVBoxLayout() )
self.resize( 600, 300 )
self.myTable = QtGui.QTableWidget()
self.myTable.setColumnCount( 2 )
rowCount = len(get_all_geos())
self.myTable.setRowCount(rowCount)
self.setTable()
self.layout().addWidget(self.myTable)
self.myTable.cellChanged.connect(self.update)
def setTable(self):
# Adding the list of mesh found in scene into first column
for geo in get_all_geos():
item = cmds.listRelatives(geo, parent=True)[0]
for i in range(0, self.myTable.rowCount()):
# instead of being populated with the list of items, I got the same name for the entire column
self.myTable.setItem(i, 0, QtGui.QTableWidgetItem(item))
# sets the combobox into the second column
box = combo_box()
nameList = ("test1","test2","test3")
box.addItems(nameList)
self.myTable.setCellWidget(i,1,box)
box.currentIndexChanged.connect(partial(self.tmp, i))
def tmp(self, rowIndex, comboBoxIndex):
item = "item " + str(comboBoxIndex)
self.myTable.setItem(rowIndex, 2, QtGui.QTableWidgetItem(item))
if __name__ == "__main__":
tableView = TestTable()
tableView.show()
In my setTable function, the item is not being processed correctly? when I am trying to add it into the QTableWidget. Can someone advise?
Additionally, if anyone could answers, does the format I have used, would it be applicable for the scenario I am trying to achieve as I mentioned at the start of the post?
In your setTable() method, you are looping through the geometries, then you are looping through the rows. Since each geometry represents a row you only really need to loop through them and remove the other loop.
Modifying it like so fixes the output:
def setTable(self):
# Adding the list of mesh found in scene into first column
geos = get_all_geos()
for i in range(0, len(geos)):
item = cmds.listRelatives(geos[i], parent=True)[0]
# instead of being populated with the list of items, I got the same name for the entire column
self.myTable.setItem(i, 0, QtGui.QTableWidgetItem(item))
# sets the combobox into the second column
box = combo_box()
nameList = ("test1","test2","test3")
box.addItems(nameList)
self.myTable.setCellWidget(i,1,box)
box.currentIndexChanged.connect(partial(self.tmp, i))
The reason it was failing was because your second loop kept overriding the rows with the last geo in the list.
I have a QTableView with the following code below. It runs fine, but when I am typing the search field is always the first column, which is the number and primary key.
How can I change that the standard search field is another field, second (name) for example, even with select column on the name column or with setsort on name the standard search when typing is the first number field?
When the code is running, I can change the search column by left-clicking in the second column, but I want to achieve this programmatically.
class KL_browse(QDialog, ui_kl_browse.Ui_kl_browse):
def __init__(self):
super(KL_browse, self).__init__()
query = QSqlQuery()
query.exec_("""SELECT * FROM klanten""")
self.setupUi(self)
self.model = QSqlTableModel(self)
self.model.setTable("klanten")
self.model.setSort(1, Qt.AscendingOrder)
self.model.select()
self.tableView.setModel(self.model)
self.tableView.setSelectionMode(QTableView.SingleSelection)
self.tableView.setSelectionBehavior(QTableView.SelectRows)
# self.view.setColumnHidden(ID, True)
self.tableView.horizontalHeader().setSectionsMovable(True)
self.tableView.horizontalHeader().setDragEnabled(True)
self.tableView.horizontalHeader().setDragDropMode(QAbstractItemView.InternalMove)
self.tableView.horizontalHeader().setSortIndicator(1,0)
self.tableView.selectRow(0)
self.tableView.selectColumn(1)
You need to change the index as follows:
# index: the index of a cell in the desired column
index = tableView.model().index(0, 1)
tableView.selectionModel().setCurrentIndex(index, QItemSelectionModel.NoUpdate);