I am using PyQt for a simple application that reads from a log file with JSON formatted strings, and outputs them nicely in a table.
Everything is working as expected except when I try to emit a signal from a 'load' function. This signal is picked up by the main window, in a slot designed to resort the table with new information.
Without the signal emitted, the table populates fully and properly:
By uncommenting the self.emit so that the signal IS emitted, the table ends up being incomplete:
As you can see in the first image, the table is NOT sorted, but all fields are populated. In the second image, the table is sorted, but some fields are blank!
The code that populates the table and sends the signal:
#openLog function does stuff, then populates the table as follows
self.ui.tableWidget.setRowCount(len(entries))
self.ui.tableWidget.verticalHeader().setVisible(False)
for i, row in enumerate(entries):
for j, col in enumerate(row):
item = QtGui.QTableWidgetItem(col)
self.ui.tableWidget.setItem(i, j, item)
#When this is uncommented, the table ends up having a lot of blank cells.
#self.emit(QtCore.SIGNAL("updateSignal"))
The code for receiving the signal, and acting:
#main window class
#__init__
self.ui.tableWidget.connect(self,QtCore.SIGNAL("updateSignal"),self.updateTable)
def updateTable(self):
self.ui.tableWidget.sortItems(0,QtCore.Qt.DescendingOrder)
The program flow is called as : program_init->register_signal. User action to open log ->openLog function that populates table/emit signal->signal received/ resort table
For this method, I am using signals and slots, as if I do not, QT/Python throws a bunch of warnings about it not being safe to redraw the GUI/Pixmap from the function.
Question:
How can I make the QTableWidget sort on the column I desire, while also ensuring the table is fully populated?
I think solution is to disable sorting while populating table by calling QTableWidget.setSortingEnabled(False), and then restore sorting.
Example code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
class MainWindow(QtGui.QWidget):
updateSignal = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.table_widget = QtGui.QTableWidget()
self.button = QtGui.QPushButton('Populate')
self.button.clicked.connect(self.populate)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table_widget)
layout.addWidget(self.button)
self.setLayout(layout)
self.updateSignal.connect(self.update_table)
self.populate()
def populate(self):
nrows, ncols = 5, 2
self.table_widget.setSortingEnabled(False)
self.table_widget.setRowCount(nrows)
self.table_widget.setColumnCount(ncols)
for i in range(nrows):
for j in range(ncols):
item = QtGui.QTableWidgetItem('%s%s' % (i, j))
self.table_widget.setItem(i, j, item)
self.updateSignal.emit()
self.table_widget.setSortingEnabled(True)
def update_table(self):
self.table_widget.sortItems(0,QtCore.Qt.DescendingOrder)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
wnd = MainWindow()
wnd.resize(640, 480)
wnd.show()
sys.exit(app.exec_())
I've been working on something similar, but was not setting up a sort. I tried both ways, and both worked for me.
My list, is a list of dictionaries, so slightly different from yours.
I have created a tabledialog class, that contains my table widget, and this function, is called from my main window:
def setuptable(self, alist):
# setup variables
rows = len(alist)
cols = len(alist[0])
keys = ['number', 'name', 'phone', 'address'] # for dictonary order
# setup cols, rows
self.tableWidget.setRowCount(rows)
self.tableWidget.setColumnCount(cols)
# insert data
for row in range(rows):
for col in range(cols):
item = QtGui.QTableWidgetItem()
item.setText(alist[row][keys[col]] or '') # or '' for any None values
table.setItem(row, col, item)
keys = [item.title() for item in keys] # capitalize
self.tableWidget.setHorizontalHeaderLabels(keys) # add header names
self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft) # set alignment
self.tableWidget.resizeColumnsToContents() # call this after all items have been inserted
self.tableWidget.sortItems(1,QtCore.Qt.AscendingOrder)
Also tried using, at the end of my tablesetup function:
self.emit(QtCore.SIGNAL("loadingDone"))
and setup the slot in my main window, in the init section:
# setup the dialog
import dialogtable
self.tabledialog = dialogtable.dialogtable()
# signal from table dialog
self.tabledialog.connect(self.tabledialog,QtCore.SIGNAL("loadingDone"),self.tableSort)
And the function called:
def tableSort(self):
self.tabledialog.tableWidget.sortItems(1,QtCore.Qt.AscendingOrder)
My tablewidget setup functions:
# set table widget attributes
self.tableWidget.setEditTriggers(QtGui.QAbstractItemView.DoubleClicked) # use NoEditTriggers to disable editing
self.tableWidget.setAlternatingRowColors(True)
self.tableWidget.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
self.tableWidget.verticalHeader().setDefaultSectionSize(18) # tighten up the row size
self.tableWidget.horizontalHeader().setStretchLastSection(True) # stretch last column to edge
self.tableWidget.setSortingEnabled(True) # allow sorting
I don't bother ever set sorting to false, as the answer above mine recommends.
Related
I do have a problem while updating my content on-demand.
My scenario: I want to create a GUI which stores information about some products and it should be displayed via a scroll are. This is working fine, but when I want to update the information like the quantity of my item, the GUI or the layout of my QVBoxLayout does not update the new information on the screen.
My code for the update function:
#pyqtSlot(GroupContent)
def _updateData(self, box: GroupContent) -> None:
prompt = UpdatePrompt(self._conn, box)
prompt.exec()
amount = self._top_layout.count()
for i in range(amount):
tmp = self._top_layout.itemAt(i).widget()
if tmp.id != box.id:
continue
tmp.setTitle(box.title)
tmp.lblPrice.setText(str(box.price))
tmp.lblQuantity.setText(str(box.quantity))
The param box does already get the updated information from a QDialog.
The self._top_layout variable will be created with self._top_layout = QVBoxLayout().
I already tried to call update on the Mainwindow and also on the top layout.
Also tried directly accessing the widget with self._top_layout.itemAt(i).widget().setTitle('test') for example.
If this information is necessary, here is my function to dynamic generate the groupboxes:
def _populate(self, insert: Boolean = False) -> None:
data = self._getDBData(insert)
for row in data:
group_box = GroupContent()
group_box.storage_id.connect(self._updateData)
group_box.id = row.storage_id
group_box.title = row.name
group_box.price = row.price
group_box.quantity = row.quantity
group_box.image = row.image
self._top_layout.addWidget(group_box)
I'm struggling with the code shown below that loads data from a SQLite table which has 3 columns ("codice" (primary key, autoincrement), "descrizione", "peso") using QSQLTable model with QDataWidget mapper.
My problem is I defined a QPushButton to insert new record in such table but I'm not able to get it work. I tried different ways using self.model.insertRows or self.model.insertRecord but I was not successfully. I'm not getting any error but the record is not inserted.
For sure there are conceptual errors I can't catch.
import sys
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QDataWidgetMapper,
QDoubleSpinBox,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QPushButton,
QSpinBox,
QTableView,
QVBoxLayout,
QWidget,
)
from connect_SQLITE import Database
db=Database.con
db.open()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
form = QFormLayout()
self.codice = QSpinBox()
self.codice.setRange(0, 2147483647)
self.codice.setDisabled(True)
self.descrizione = QLineEdit()
self.peso = QDoubleSpinBox()
self.peso.setDecimals(5)
self.peso.setRange(0, 9999.99999)
self.peso.setSingleStep(0.01)
form.addRow(QLabel("Codice Materiale"), self.codice)
form.addRow(QLabel("Nome Materiale"), self.descrizione)
form.addRow(QLabel("Peso specifico Materiale (Kg/dm3)"), self.peso)
self.model = QSqlTableModel(db=db)
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.model)
self.mapper.addMapping(self.codice, 0)
self.mapper.addMapping(self.descrizione, 1)
self.mapper.addMapping(self.peso, 2)
self.model.setTable("Prova")
self.model.select()
self.mapper.toFirst()
# tag::controls[]
self.setMinimumSize(QSize(400, 400))
controls = QHBoxLayout()
prev_rec = QPushButton("Precedente")
prev_rec.clicked.connect(self.mapper.toPrevious)
next_rec = QPushButton("Successivo")
next_rec.clicked.connect(self.mapper.toNext)
ins_rec = QPushButton("Inserimento")
ins_rec.clicked.connect(self.inserimento_materiale)
save_rec = QPushButton("Salvataggio modifiche")
save_rec.clicked.connect(self.mapper.submit)
controls.addWidget(prev_rec)
controls.addWidget(next_rec)
controls.addWidget(ins_rec)
controls.addWidget(save_rec)
layout.addLayout(form)
layout.addLayout(controls)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
# end::controls[]
def inserimento_materiale(self):
self.model.insertRows(self.model.rowCount(), 1)
self.model.setData(self.model.index(0,1), self.descrizione.text())
self.model.setData(self.model.index(0,2), self.peso)
self.model.submit()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
The problem is that by default the mapper uses the AutoSubmit submitPolicy():
Whenever a widget loses focus, the widget's current value is set to the item model.
And QSqlTableModel uses the OnRowChange editStrategy():
Changes to a row will be applied when the user selects a different row.
Also, you're calling setData() on the first row, instead of the new one.
The result is that when inserimento_materiale is called and a new row is inserted, the current index of the mapper is still the previous one (the first on startup, due to toFirst() or any other set after toPrevious() or toNext()), setData() will not work in any case:
in your example, the mapper has already set the cached data due to its AutoSubmit policy, but it has not been actually submitted in the database, and setData() returns False because "For OnRowChange, an index may receive a change only if no other row has a cached change";
even setting the proper row for setData() will not work, as inserting a new row causes the mapper to submit the previous data of the current widgets in the previous row, and will not properly update the new mapper index;
Be aware that your code also has a typo: you're trying to call setData() with self.peso which is a widget.
A possible solution is to read the current widget values before doing anything, revert() the model data (to avoid saving the new data on the previous index), submit the new data to the model and set the new index in the mapper:
def inserimento_materiale(self):
descrizione = self.descrizione.text()
peso = self.peso.value()
self.model.revert()
row = self.model.rowCount()
self.model.insertRow(row)
self.model.setData(self.model.index(row, 1), descrizione)
self.model.setData(self.model.index(row, 2), peso)
self.model.submitAll()
self.mapper.setCurrentIndex(row)
Note that since you have a dedicated submit button, you should change the mapper's submit policy to ManualSubmit, which makes things easier:
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.mapper.setSubmitPolicy(self.mapper.ManualSubmit)
def inserimento_materiale(self):
row = self.model.rowCount()
self.model.insertRow(row)
self.model.setData(self.model.index(row, 1), self.descrizione.text())
self.model.setData(self.model.index(row, 2), self.peso.value())
self.model.submitAll()
self.mapper.setCurrentIndex(row)
I am building a user interface with several (as many as the user wants) tabular (spreadsheet-like) forms of user-specified size (but the size won't change once initialized). The user populates these tables either by copy-pasting data (usually from excel) or directly typing data to the cells. I am using the Tksheet Tkinter add-on.
It seems that there are several options in Tksheet to achieve the goal of opening an empty table of i rows and j columns:
a) set_sheet_data_and_display_dimensions(total_rows = None, total_columns = None).
This routine throws a TypeError. The error is raised in:
GetLinesHeight(self, n, old_method = False)
The subroutine expects the parameter n to be an integer, but receives a tuple.
The calling routine is sheet_display_dimensions, and the relevant line is:
height = self.MT.GetLinesHeight(self.MT.default_rh).
MT.default_rh is apparently a complex object, it can be an integer, but also a string or a tuple. Other routines that use it in Tksheet perform elaborate manipulation to make sure it is handed to the subroutine in integer form, but not so sheet_display_dimensions.
b) sheet_data_dimensions(total_rows = None, total_columns = None)
This seems to work programmatically, but does not display the table to the user.
One may add the line sheet_display_dimensions(i,j) but--you guessed it--this raises an error...
Sample code:
from tksheet import Sheet
import tkinter as tk
from tkinter import ttk
# This class builds and displays a test table. It is not part of the question but merely used to illustrate it
class SeriesTable(tk.Frame):
def __init__(self, master):
super().__init__(master) # call super class init to build frame
self.grid_columnconfigure(0, weight=1) # This configures the window's escalators
self.grid_rowconfigure(0, weight=1)
self.grid(row=0, column=0, sticky="nswe")
self.sheet = Sheet(self, data=[[]]) # set up empty table inside the frame
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
'''
updated_value checked here
'''
# passed tests
pass # go do stuff
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
err_flag = False # will be switched if errors are encountered
'''
updated_array is checked here
'''
# passed tests
if err_flag: # error during checks is indicated
self.sheet.undo() # undo change
else:
pass # go do stuff
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
tk_win = tk.Tk() # establish the root tkinter window
tk_win.title("Master Sequence")
tk_win.geometry("600x400")
tk_win.config(bg='red')
nb = ttk.Notebook(tk_win) # a notebook in ttk is a [horizontal] list of tabs, each associated with a page
nb.pack(expand=True, fill='both') # widget packing strategy
settings_page = tk.Frame(nb) # initiate 1st tab object in the notebook
nb.add(settings_page, text = "Settings") # add it as top page in the notebook
test = SeriesTable(nb) # creates a 1 row X 0 column table
nb.add(test, text = "Table Test") # add it as second page in the notebook
i = 4
j = 3
#test.sheet.set_sheet_data_and_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
#test.sheet.sheet_data_dimensions(total_rows=i, total_columns=j) # extends the table to 4 X 3, but the display is still 1 X 0
#test.sheet.sheet_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
test.sheet.insert_columns(j) # this works
test.sheet.insert_rows(i - 1) # note that we insert i-1 because table already has one row
test.mainloop()
I figured out a work-around with:
c)
insert_columns(j)
insert_rows(i - 1)
Note that you have to insert i-1 rows. This is because the sheet object is initiated with 1 row and 0 columns. (But does it say so in the documentation? No it does not...)
DESCRIPTION
I have a pyqt5 UI which has a QTableWidget with a dynamic row count; there is a button that adds rows. When a row is added, some of the cells contain QSpinBox(s) and one contains a QComboBox. The program also has a Save and a Restore QPushButton(s) that when selected, saves down all the widgets to an ini file located next to the py file. The save and restore methods, created by #eyllanesc and found here are pretty much universally used from what I have found on SO.
PROBLEM
The code below saves down the QSpinBox(s) and QComboBox fine. They are in the ini file. The restore function does not recover these widgets in to the QTableWidget. The row count is recovered, but no input text or widgets are inside the cells they were placed in.
WHAT I HAVE TRIED
I named the dynamically created widgets with a prefix_<row>_<column> name. I tried creating these in the initialisation of the UI, thinking there might be a link to the qApp.allWidgets() and the startup, but this didn't work. I was just guessing.
Reading #zythyr's post here, I thought I might have to add the widget to the QTableWidget in a parent<>child sort of arrangement (this is outside my knowledge), but the QTableWidget doesn't have the addWidget() method. I then tried *.setParent on the QComboBox (commented out in code below) and that didn't work either.
QUESTION
How do you save and restore user-input (typed in empty cell) data as well as QWidgets (namely QSpinBox and QComboBox) that are in a QTableWidget?
CODE
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import QSettings, QFileInfo
from PyQt5.QtWidgets import (QApplication, qApp, QMainWindow, QWidget,
QVBoxLayout, QTableWidget, QTableWidgetItem,
QHeaderView, QPushButton, QLineEdit, QSpinBox,
QComboBox)
def value_is_valid(val):
#https://stackoverflow.com/a/60028282/4988010
if isinstance(val, QPixmap):
return not val.isNull()
return True
def restore(settings):
#https://stackoverflow.com/a/60028282/4988010
finfo = QFileInfo(settings.fileName())
if finfo.exists() and finfo.isFile():
for w in qApp.allWidgets():
if w.objectName():
mo = w.metaObject()
for i in range(mo.propertyCount()):
prop = mo.property(i)
name = prop.name()
last_value = w.property(name)
key = "{}/{}".format(w.objectName(), name)
if not settings.contains(key):
continue
val = settings.value(key, type=type(last_value),)
if (
val != last_value
and value_is_valid(val)
and prop.isValid()
and prop.isWritable()
):
w.setProperty(name, val)
def save(settings):
#https://stackoverflow.com/a/60028282/4988010
for w in qApp.allWidgets():
if w.objectName():
mo = w.metaObject()
for i in range(mo.propertyCount()):
prop = mo.property(i)
name = prop.name()
key = "{}/{}".format(w.objectName(), name)
val = w.property(name)
if value_is_valid(val) and prop.isValid() and prop.isWritable():
settings.setValue(key, w.property(name))
# custom spin box allowing for optional maximum
class CustomSpinBox(QSpinBox):
def __init__(self, sb_value=1, sb_step=1, sb_min=1, *args):
super(CustomSpinBox, self).__init__()
self.setValue(sb_value)
self.setSingleStep(sb_step)
self.setMinimum(sb_min)
if len(args) > 0:
sb_max = args[0]
self.setMaximum(sb_max)
class MainWindow(QMainWindow):
# save settings alongside *py file
settings = QSettings("temp.ini", QSettings.IniFormat)
def __init__(self):
super().__init__()
self.initUI()
self.initSignals()
def initUI(self):
# standar UI stuff
self.setObjectName('MainWindow')
self.setWindowTitle('Program Title')
self.setGeometry(400, 400, 400, 100)
wid = QWidget(self)
self.setCentralWidget(wid)
# create some widgets
self.pb_add_row = QPushButton('Add Row')
self.pb_remove_row = QPushButton('Remove Selected Row')
self.pb_save = QPushButton('Save')
self.pb_restore = QPushButton('Restore')
self.le = QLineEdit()
self.le.setObjectName('le')
self.tbl = QTableWidget()
self.tbl.setObjectName('r_tbl')
header = self.tbl.horizontalHeader()
self.tbl.setRowCount(0)
self.tbl.setColumnCount(4)
input_header = ['Label', 'X', 'Y', 'Comment']
self.tbl.setHorizontalHeaderLabels(input_header)
header.setSectionResizeMode(QHeaderView.Stretch)
# add widgets to UI
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.le)
self.vbox.addWidget(self.tbl)
self.vbox.addWidget(self.pb_add_row)
self.vbox.addWidget(self.pb_remove_row)
self.vbox.addWidget(self.pb_save)
self.vbox.addWidget(self.pb_restore)
wid.setLayout(self.vbox)
# restore previous settings from *.ini file
restore(self.settings)
# pb signals
def initSignals(self):
self.pb_add_row.clicked.connect(self.pb_add_row_clicked)
self.pb_remove_row.clicked.connect(self.pb_remove_row_clicked)
self.pb_save.clicked.connect(self.pb_save_clicked)
self.pb_restore.clicked.connect(self.pb_restore_clicked)
# add a new row to the end - add spin boxes and a combobox
def pb_add_row_clicked(self):
current_row_count = self.tbl.rowCount()
row_count = current_row_count + 1
self.tbl.setRowCount(row_count)
self.sb_1 = CustomSpinBox(1, 1, 1)
self.sb_1.setObjectName(f'sb_{row_count}_1')
self.sb_2 = CustomSpinBox(3, 1, 2, 6)
self.sb_2.setObjectName(f'sb_{row_count}_2')
self.cmb = QComboBox()
choices = ['choice_1', 'choice_2', 'choice_3']
self.cmb.addItems(choices)
self.cmb.setObjectName(f'cmb_{row_count}_0')
self.tbl.setCellWidget(current_row_count, 0, self.cmb)
self.tbl.setCellWidget(current_row_count, 1, self.sb_1)
self.tbl.setCellWidget(current_row_count, 2, self.sb_2)
#self.cmb.setParent(self.tbl) # <<<< this didn't work
def pb_remove_row_clicked(self):
self.tbl.removeRow(self.tbl.currentRow())
def pb_save_clicked(self):
print(f'{self.pb_save.text()} clicked')
save(self.settings)
def pb_restore_clicked(self):
print(f'{self.pb_restore.text()} clicked')
restore(self.settings)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec_()
EDIT1
...removed as it didn't help and just made it more confusing...
EDIT2
Excluding the widgets, I have worked out how to use QSettings to save and restore user-entered cell data from a QTableWidget. Hope the following code helps someone. I have no doubt it could be done better and I welcome improvement suggestions. I'll update if I can work out the addition of widgets (QSpinBoxes and QComboBoxes) in to to cells.
import sys
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget,
QVBoxLayout, QTableWidget, QTableWidgetItem,
QHeaderView, QPushButton)
class MainWindow(QMainWindow):
# save settings alongside *py file
settings = QSettings("temp.ini", QSettings.IniFormat)
def __init__(self):
super().__init__()
self.initUI()
self.initSignals()
self.restore_settings()
def initUI(self):
# standar UI stuff
self.setObjectName('MainWindow')
self.setWindowTitle('Program Title')
self.setGeometry(400, 400, 500, 300)
wid = QWidget(self)
self.setCentralWidget(wid)
# create some widgets
self.pb_add_row = QPushButton('Add Row')
self.pb_remove_row = QPushButton('Remove Selected Row')
self.pb_save = QPushButton('Save')
self.pb_restore = QPushButton('Restore')
self.tbl = QTableWidget(0, 4, self)
# config up the table
header = self.tbl.horizontalHeader()
input_header = ['Label', 'X', 'Y', 'Comment']
self.tbl.setHorizontalHeaderLabels(input_header)
header.setSectionResizeMode(QHeaderView.Stretch)
# add widgets to UI
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.tbl)
self.vbox.addWidget(self.pb_add_row)
self.vbox.addWidget(self.pb_remove_row)
self.vbox.addWidget(self.pb_save)
self.vbox.addWidget(self.pb_restore)
wid.setLayout(self.vbox)
# pb signals
def initSignals(self):#
self.pb_add_row.clicked.connect(self.pb_add_row_clicked)
self.pb_remove_row.clicked.connect(self.pb_remove_row_clicked)
self.pb_save.clicked.connect(self.pb_save_clicked)
self.pb_restore.clicked.connect(self.pb_restore_clicked)
# reads in the ini file adn re-generate the table contents
def restore_settings(self):
self.setting_value = self.settings.value('table')
self.setting_row = self.settings.value('rows')
self.setting_col = self.settings.value('columns')
print(f'RESTORE: {self.setting_value}')
# change the table row/columns, create a dictionary out of the saved table
try:
self.tbl.setRowCount(int(self.setting_row))
self.tbl.setColumnCount(int(self.setting_col))
self.my_dict = dict(eval(self.setting_value))
except TypeError:
print(f'RESTORE: No ini file, resulting in no rows/columns')
# loop over each table cell and populate with old values
for row in range(self.tbl.rowCount()):
for col in range(self.tbl.columnCount()):
try:
if col == 0: self.tbl.setItem(row, col, QTableWidgetItem(self.my_dict['Label'][row]))
if col == 1: self.tbl.setItem(row, col, QTableWidgetItem(self.my_dict['X'][row]))
if col == 2: self.tbl.setItem(row, col, QTableWidgetItem(self.my_dict['Y'][row]))
if col == 3: self.tbl.setItem(row, col, QTableWidgetItem(self.my_dict['Comment'][row]))
except IndexError:
print(f'INDEX ERROR')
# add a new row to the end
def pb_add_row_clicked(self):
current_row_count = self.tbl.rowCount()
row_count = current_row_count + 1
self.tbl.setRowCount(row_count)
# remove selected row
def pb_remove_row_clicked(self):
self.tbl.removeRow(self.tbl.currentRow())
# save the table contents and table row/column to the ini file
def pb_save_clicked(self):
# create an empty dictionary
self.tbl_dict = {'Label':[], 'X':[], 'Y':[], 'Comment':[]}
# loop over the cells and add to the table
for column in range(self.tbl.columnCount()):
for row in range(self.tbl.rowCount()):
itm = self.tbl.item(row, column)
try:
text = itm.text()
except AttributeError: # happens when the cell is empty
text = ''
if column == 0: self.tbl_dict['Label'].append(text)
if column == 1: self.tbl_dict['X'].append(text)
if column == 2: self.tbl_dict['Y'].append(text)
if column == 3: self.tbl_dict['Comment'].append(text)
# write values to ini file
self.settings.setValue('table', str(self.tbl_dict))
self.settings.setValue('rows', self.tbl.rowCount())
self.settings.setValue('columns', self.tbl.columnCount())
print(f'WRITE: {self.tbl_dict}')
def pb_restore_clicked(self):
self.restore_settings()
def closeEvent(self, event):
self.pb_save_clicked()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
CHALLENGE
I am sure there is a better way than this and I challenge those who are well versed in Qt (pyqt5) to find a better solution.
ANSWER
In the mean-time, I hope the below helps someone as I could not find this information anywhere. I can't really read the C++ Qt stuff and I am no pyqt5 Harry Potter, so this took some effort.
The general gist here, is the widgets were never saved. The values were saved to a dictionary and then to the QSettings ini file as a string. On boot, the ini file was parsed and the table rebuilt - rows and columns. The widgets were then built from scratch and re-inserted in to the table. The dictionary from the ini file was then parsed and the values applied to the widgets or the blank cells as required.
WISH
I really wish there was a method inline with the save / restore methods as per OP links. I found that if I included those save / restore methods in the init, it sometimes messed the table up completely. As a result, for my greater program, it looks like I am going to have to save and restore all settings manually.
MRE CODE
import sys
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget,
QVBoxLayout, QTableWidget, QTableWidgetItem,
QHeaderView, QPushButton, QComboBox, QSpinBox)
# custom spin box allowing for optional maximum
class CustomSpinBox(QSpinBox):
def __init__(self, sb_value=1, sb_step=1, sb_min=1, *args):
super(CustomSpinBox, self).__init__()
self.setValue(sb_value)
self.setSingleStep(sb_step)
self.setMinimum(sb_min)
if len(args) > 0:
sb_max = args[0]
self.setMaximum(sb_max)
class MainWindow(QMainWindow):
# save settings alongside *py file
settings = QSettings("temp.ini", QSettings.IniFormat)
def __init__(self):
super().__init__()
self.initUI()
self.initSignals()
self.restore_settings()
def initUI(self):
# standar UI stuff
self.setObjectName('MainWindow')
self.setWindowTitle('Program Title')
self.setGeometry(400, 400, 500, 300)
wid = QWidget(self)
self.setCentralWidget(wid)
# create some widgets
self.pb_add_row = QPushButton('Add Row')
self.pb_remove_row = QPushButton('Remove Selected Row')
self.pb_save = QPushButton('Save')
self.pb_restore = QPushButton('Restore')
self.tbl = QTableWidget(0, 4, self)
# config up the table
header = self.tbl.horizontalHeader()
input_header = ['Label', 'X', 'Y', 'Comment']
self.tbl.setHorizontalHeaderLabels(input_header)
header.setSectionResizeMode(QHeaderView.Stretch)
# name the table for QSettings
self.tbl.setObjectName('input_table')
# add widgets to UI
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.tbl)
self.vbox.addWidget(self.pb_add_row)
self.vbox.addWidget(self.pb_remove_row)
self.vbox.addWidget(self.pb_save)
wid.setLayout(self.vbox)
# create an empty dictionary to house the table widgets
self.table_widgets = {'cmb':[], 'sb_1':[], 'sb_2':[]}
# combobox values
self.choices = ['choice_1', 'choice_2', 'choice_3']
# pb signals
def initSignals(self):#
self.pb_add_row.clicked.connect(self.pb_add_row_clicked)
self.pb_remove_row.clicked.connect(self.pb_remove_row_clicked)
self.pb_save.clicked.connect(self.pb_save_clicked)
# reads in the ini file and re-generate the table contents
def restore_settings(self):
try:
self.setting_tbl = self.settings.value('table')
self.setting_row = self.settings.value('rows')
self.setting_col = self.settings.value('columns')
self.my_dict = dict(eval(self.setting_tbl))
# need to rebuild the table first
self.tbl.setRowCount(int(self.setting_row))
self.tbl.setColumnCount(int(self.setting_col))
print(f'RESTORE: row:{self.setting_row} and col:{self.setting_col} and table:{self.setting_tbl}')
# probably don't need to build and return values from the dictionary
for row in range(int(self.setting_row)):
self.table_widgets['cmb'].append(QComboBox())
self.table_widgets['sb_1'].append(CustomSpinBox(1, 1, 1))
self.table_widgets['sb_2'].append(CustomSpinBox(3, 1, 2, 6))
self.table_widgets['cmb'][row].addItems(self.choices)
self.tbl.setCellWidget(row, 0, self.table_widgets['cmb'][row])
self.tbl.setCellWidget(row, 1, self.table_widgets['sb_1'][row])
self.tbl.setCellWidget(row, 2, self.table_widgets['sb_2'][row])
self.tbl.cellWidget(row, 0).setCurrentText(self.my_dict['Label'][row])
self.tbl.cellWidget(row, 1).setValue(self.my_dict['X'][row])
self.tbl.cellWidget(row, 2).setValue(self.my_dict['Y'][row])
self.tbl.setItem(row, 3, QTableWidgetItem(self.my_dict['Comment'][row]))
except TypeError:
print('NO INI FILE PRESENT')
# add a new row to the end
def pb_add_row_clicked(self):
current_row_count = self.tbl.rowCount()
row_count = current_row_count + 1
self.tbl.setRowCount(row_count)
self.table_widgets['cmb'].append(QComboBox())
self.table_widgets['sb_1'].append(CustomSpinBox(1, 1, 1))
self.table_widgets['sb_2'].append(CustomSpinBox(3, 1, 2, 6))
self.table_widgets['cmb'][-1].addItems(self.choices)
self.tbl.setCellWidget(current_row_count, 0, self.table_widgets['cmb'][current_row_count])
self.tbl.setCellWidget(current_row_count, 1, self.table_widgets['sb_1'][current_row_count])
self.tbl.setCellWidget(current_row_count, 2, self.table_widgets['sb_2'][current_row_count])
# save the table contents and table row/column to the ini file
def pb_save_clicked(self):
#save(self.settings)
# create an empty dictionary
self.tbl_dict = {'Label':[], 'X':[], 'Y':[], 'Comment':[]}
# loop over the cells and add to the dictionary
for row in range(self.tbl.rowCount()):
cmb_text = self.tbl.cellWidget(row, 0).currentText()
sb_1_value = self.tbl.cellWidget(row, 1).value()
sb_2_value = self.tbl.cellWidget(row, 2).value()
comment_text = self.tbl.item(row, 3)
try:
comment_text = comment_text.text()
except AttributeError: # happens when the cell is empty or a widget
comment_text = ''
self.tbl_dict['Label'].append(cmb_text)
self.tbl_dict['X'].append(sb_1_value)
self.tbl_dict['Y'].append(sb_2_value)
self.tbl_dict['Comment'].append(comment_text)
# write values to ini file
self.settings.setValue('table', str(self.tbl_dict))
self.settings.setValue('rows', self.tbl.rowCount())
self.settings.setValue('columns', self.tbl.columnCount())
print(f'WRITE TO INI FILE: {self.tbl_dict}')
# remove selected row
def pb_remove_row_clicked(self):
self.tbl.removeRow(self.tbl.currentRow())
def closeEvent(self, event):
self.pb_save_clicked()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I'm not well versed in PyQt by any means, but if I'm interpreting your problem correctly I believe PyQtConfig may help since nothing else has been suggested for some time. Here is the github version link.
Tucked away in a dark corner of github, this is not nearly as popular as it should be. A snippet from the introduction:
The core of the API is a ConfigManager instance that holds configuration settings (either as a Python dict, or a QSettings instance) and provides standard methods to get and set values.
Configuration parameters can have Qt widgets attached as handlers. Once attached the widget and the configuration value will be kept in sync. Setting the value on the ConfigManager will update any attached widgets and changes to the value on the widget will be reflected immediately in the ConfigManager. Qt signals are emitted on each update.
This should allow you to save the state of widgets and I would imagine the widgets inside them separately. However, you will need to map the necessary events to update the config file, then build the widgets "up". If it's still clearing the cache--as it seems to act similar to a dict--when pulling from the config file, this functionality might be easier if other languages were used.
Integrating database storage with java fetching methods might be overkill, but SQL for python would be a middle ground. You can have tables for parent and child widgets as well as parameters, then link them by unique id's. Using python to fetch these then build the widget on startup would be a great alternative, and would actively keep widgets organized. There is also the added benefit of being able to more easily take the application online at some point with MySQL or Oracle.
I've been working on this for a while and I can't find any information about adding a row to a window. I seen it done with pyside2 and qt, witch would work but the users are using multiple versions of Maya (2016 = pyside, 2017=pyside2).
I want it like adding a widget in in pyside. I done it where adding a row is a function like add row 1, add row 2, and add row 3 but the script get to long. I need to parent to rowColumnLayout and make that unique in order to delete that later. Also I have to query the textfield in each row. Maybe a for loop that adds a number to the row? I really don't know but this is what I have so far:
from maya import cmds
def row( ):
global fed
global info
item=cmds.optionMenu(mygroup, q=True, sl=True)
if item == 1:
cam=cmds.optionMenu(mygroup, q=True, v=True)
fed=cmds.rowColumnLayout(nc = 1)
cmds.rowLayout(nc=7)
cmds.text(l= cam )
cmds.text(l=u'Frame Range ')
start = cmds.textField('textField3')
cmds.text(l=u' to ')
finish = cmds.textField('textField2')
cmds.button(l=u'render',c='renderTedd()')
cmds.button(l=u'delete',c='deleteRow()')
cmds.setParent (fed)
def deleteRow ():
cmds.deleteUI(fed, layout=True)
if item == 2:
print item
global red
cam1=cmds.optionMenu(mygroup, q=True, v=True)
red = cmds.rowColumnLayout()
cmds.rowLayout(nc=7)
cmds.text(l= cam1 )
cmds.text(l=u'Frame Range ')
start = cmds.textField('textField3')
cmds.text(l=u' to ')
finish = cmds.textField('textField2')
cmds.button(l=u'render',c='renderTedd()')
cmds.button(l=u'delete',c='deleteRow2()')
cmds.setParent (red)
def deleteRow2 ():
cmds.deleteUI(red, control=True)
def cameraInfo():
info=cmds.optionMenu(mygroup, q=True, sl=True)
print info
def deleteRow ():
cmds.deleteUI(fed, control=True)
def getCamera():
layers=pm.ls(type="renderLayer")
for layer in layers:
pm.editRenderLayerGlobals(currentRenderLayer=layer)
cameras=pm.ls(type='camera')
for cam in cameras:
if pm.getAttr(str(cam) + ".renderable"):
relatives=pm.listRelatives(cam, parent=1)
cam=relatives[0]
cmds.menuItem(p=mygroup,label=str (cam) )
window = cmds.window()
cmds.rowColumnLayout(nr=10)
mygroup = cmds.optionMenu( label='Colors', changeCommand='cameraInfo()' )
getCamera()
cmds.button(l=u'create camera',aop=1,c='row ()')
cmds.showWindow( window )
This is totally doable with cmds. The trick is just to structure the code so that the buttons in each row know and can operate on the widgets in that row; once that works you can add rows all day long.
To make it work you want to do two things:
Don't use the string form of callbacks. It's never a good idea, for reasons detailed here
Do use closures to make sure your callbacks are referring to the right widgets. Done right you can do what you want without the overhead of a class.
Basically, this adds up to making a function which generates both the gui items for the row and also generates the callback functions -- the creator function will 'remember' the widgets and the callbacks it creates will have access to the widgets. Here's a minimal example:
def row_test():
window = cmds.window(title='lotsa rows')
column = cmds.columnLayout()
def add_row(cameraname) :
cmds.setParent(column)
this_row = cmds.rowLayout(nc=6, cw6 = (72, 72, 72, 72, 48, 48) )
cmds.text(l= cameraname )
cmds.text(l=u'Frame Range')
start = cmds.intField()
finish = cmds.intField()
# note: buttons always fire a useless
# argument; the _ here just ignores
# that in both of these callback functions
def do_delete(_):
cmds.deleteUI(this_row)
def do_render(_):
startframe = cmds.intField(start, q=True, v=True)
endframe = cmds.intField(finish, q=True, v=True)
print "rendering ", cameraname, "frames", startframe, endframe
cmds.button(l=u'render',c=do_render)
cmds.button(l=u'delete',c=do_delete)
for cam in cmds.ls(type='camera'):
add_row(cam)
cmds.showWindow(window)
row_test()
By defining the callback functions inside of add_row(), they have access to the widgets which get stored as start and finish. Even though start and finish will be created over and over each time the function runs, the values they store are captured by the closures and are still available when you click a button. They also inherit the value of cameraname so the rendering script can get that information as well.
At the risk of self-advertising: if you need to do serious GUI work using cmds you should check out mGui -- a python module that makes working with cmds gui less painful for complex projects.