getting 2 comboboxes working from QDataWidgetMapper - python

Please be kind. This is my first question. I have included a minimum listing that illustrates the problem.
from PyQt5 import QtWidgets, QtCore, QtGui, uic, QtSql
from PyQt5.QtCore import Qt
qtCreatorFile = "QuotesGui.ui"
Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorFile)
class QuotesUI(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, model):
QtWidgets.QMainWindow.__init__(self)
uic.loadUi(qtCreatorFile,self)
self.setupUi(self)
self.model = model # View owns the model
self.tableView.setModel(self.model._formModel)
self.setWidgetMapping() # Map model to view widgets
authorSRD = QtSql.QSqlRelationalDelegate(self.authorComboBox)
self.authorComboBox.setModel(self.model._authorModel)
self.authorComboBox.setItemDelegate(authorSRD)
self.authorComboBox.setModelColumn(
self.model._formModel.fieldIndex("authorName"))
citationSRD = QtSql.QSqlRelationalDelegate(self.citationComboBox)
self.citationComboBox.setModel(self.model._citationModel)
self.citationComboBox.setItemDelegate(citationSRD)
self.citationComboBox.setModelColumn(
self.model._formModel.fieldIndex("citationText"))
self.mainMapper.toFirst()
self._updateButtons(0) # To disable the previous button at start up
self.saveBtn.setEnabled(False) # Since we can't edit, we can't save
self._connectSignals(self.model)
def setWidgetMapping(self):
self.mainMapper = QtWidgets.QDataWidgetMapper(self)
self.mainMapper.setModel(self.model._formModel)
self.mainMapper.setItemDelegate(QtSql.QSqlRelationalDelegate(self))
self.mainMapper.addMapping(self.authorComboBox,
self.model._formModel.fieldIndex("authorName"))
self.mainMapper.addMapping(self.citationComboBox,
self.model._formModel.fieldIndex("citationText"))
self.mainMapper.addMapping(self.quoteText,
self.model._formModel.fieldIndex("quoteText"))
def _connectSignals(self, model):
self.lastBtn.clicked.connect(self.mainMapper.toLast)
self.nextBtn.clicked.connect(self.mainMapper.toNext)
self.prevBtn.clicked.connect(self.mainMapper.toPrevious)
self.firstBtn.clicked.connect(self.mainMapper.toFirst)
self.mainMapper.currentIndexChanged.connect(
self._updateButtons)
def _updateButtons(self, row):
self.firstBtn.setEnabled(row > 0)
self.prevBtn.setEnabled(row > 0)
self.nextBtn.setEnabled(row < self.model._formModel.rowCount() - 1)
self.lastBtn.setEnabled(row < self.model._formModel.rowCount() - 1)
class QuoteModel():
def __init__(self, data):
# Make a database connection
self.db = self.createConnection(data)
# set models
self._setFormModel()
self._setAuthorComboTableLink()
self._setCitationComboTableLink()
# Connect signals
self._connectSignals()
# Define a model for the form
def _setFormModel(self):
self._formModel = QtSql.QSqlRelationalTableModel(
parent = None, db = self.db)
self._formModel.setEditStrategy(
QtSql.QSqlTableModel.OnManualSubmit)
self._formModel.setTable("Quote")
authorIdx = self._formModel.fieldIndex("authorId")
citationIdx = self._formModel.fieldIndex("citationId")
self._formModel.setJoinMode(1) # Left Join
self._formModel.setRelation(authorIdx, QtSql.QSqlRelation(
"Author", "authorId", "authorName"))
self._formModel.setRelation(citationIdx, QtSql.QSqlRelation(
"Citation", "citationId", "citationText"))
self._formModel.select()
# Define models and link tables for the two comboboxes
def _setAuthorComboTableLink(self):
self._authorModel = QtSql.QSqlTableModel(
parent = None, db = self.db)
self._authorModel.setTable("Author")
self._authorModel.setEditStrategy(
QtSql.QSqlTableModel.OnManualSubmit)
self._authorModel.select()
self._authorModel.sort(1,Qt.AscendingOrder)
def _setCitationComboTableLink(self):
self._citationModel = QtSql.QSqlTableModel(
parent = None, db = self.db)
self._citationModel.setTable("Citation")
self._citationModel.setEditStrategy(
QtSql.QSqlTableModel.OnManualSubmit)
self._citationModel.select()
self._citationModel.sort(1,Qt.AscendingOrder)
def _newData(self):
print('in _newData')
def _connectSignals(self):
self._authorModel.rowsInserted.connect(self._newData)
#######################################################
# A function to connect to a named database
def createConnection(self,dbfile):
db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName(dbfile)
db.open()
if not db.open():
QtWidgets.QMessageBox.critical(
None, QtWidgets.qApp.tr("Cannot open database"),
QtWidgets.qApp.tr("Unable to establish a database connection.\n"),
QtWidgets.QMessageBox.Cancel,
)
return False
return db
dbFile = "Quotations.db"
if __name__=='__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
m = QuoteModel(dbFile)
w = QuotesUI(m)
w.show()
sys.exit(app.exec_())
Still requires the upload of a 8kb .ui file and a 70kb sample database to run. So far I haven't been able to figure out how to do that....
When this is run, incorporating the rest of the necessary code, the author combobox works as expected, i.e.: the dropdown shows all the values in the author table and when I step the the records in the model, the correct author is displayed in the combobox.
The citation combobox does NOT function. As written here it neither shows values from the citation table, nor is it connected to the quotes table. Basically it shows nothing.
I think I need, somehow, a second delegate on the model. But I don't know how.
Any ideas how to get this functioning??

To make this work I had to change:
self.authorComboBox.setModel(self.model._authorModel)
to:
authorRel = self.model._formModel.relationModel(self.model.authorIdx)
self.authorComboBox.setModel(authorRel)
The QSqlRelationalTableModel has a virtual function "relationModel" that returns a QSqlTableModel. When you use this model as the model for the QComboBox, everything works.

Related

problem inserting record in Sqlite table using QSQLTable model and QDataWidget mapper

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)

New row in model doesn't persist in QDataWidgetMapper when QFileDialog is called

I have a simple form where I have a function/method which calls a QFileDialog to choose a file. This form stores the information in a Postgresql database and uses QDataWidgetMapper to map the form fields to database fields. When I add a new row to the model and edit some fields, when I call the QFileDialog, it seems the mappings get removed. All the edited fields on the form go blank. If I go back to a record which was retrieved from the database, I have no problems adding the image as the data persists on the form.
It seems to be related to the submit = self.c_mapper.submit() line of code. When I comment this out, the data persists in the form but I also can’t get the query().LastInsertId().
When stepping through the code, the form field data disappears when if dlg.exec_() == QDialog.Accepted: line is reached.
The desired behaviour is to be able to call the function which selects the image file and continue editing the same record with the correct data in the fields.
See the animation:
See the code:
import sys
import os
from pathlib import Path
import shutil
#PyQt stuff
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
from PyQt5.QtWidgets import *
#form modules
from form import *
class Contact(QMainWindow):
def __init__(self, cont_id, parent=None):
QMainWindow.__init__(self)
self.cont_id = cont_id
self.ui = Ui_contact_form()
self.ui.setupUi(self)
self.c_model = QSqlTableModel(self)
self.c_model.setTable("contacts")
self.c_model.select()
self.row = self.c_model.rowCount()
self.c_model.insertRow(self.row)
self.setWindowTitle('New contact')
self.c_mapper = QDataWidgetMapper(self)
self.c_mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit)
self.c_mapper.setModel(self.c_model)
#self.c_mapper.setItemDelegate(QSqlRelationalDelegate(self))
self.c_mapper.addMapping(self.ui.title, self.c_model.fieldIndex("title"))
self.c_mapper.addMapping(self.ui.first_name, self.c_model.fieldIndex("first_name"))
self.c_mapper.addMapping(self.ui.last_name, self.c_model.fieldIndex("last_name"))
self.c_mapper.addMapping(self.ui.nickname, self.c_model.fieldIndex("nickname"))
self.c_mapper.addMapping(self.ui.job_title, self.c_model.fieldIndex("job_title"))
self.c_mapper.addMapping(self.ui.birth_date, self.c_model.fieldIndex("birth_date"))
self.c_mapper.addMapping(self.ui.website, self.c_model.fieldIndex("website"))
self.c_mapper.addMapping(self.ui.comments, self.c_model.fieldIndex("comments"))
self.c_mapper.addMapping(self.ui.users, self.c_model.fieldIndex("user_name"))
self.c_mapper.addMapping(self.ui.tags, self.c_model.fieldIndex("tags"))
self.c_mapper.setCurrentIndex(self.row)
self.ui.get_image_file.clicked.connect(self.get_profile_image)
def get_profile_image(self):
submit = self.c_mapper.submit()
self.cont_id = self.c_model.query().lastInsertId()
data_loc = Path('/home/dave/hd/Nextcloud/crm_data/')
database = 'crm'
dlg = QFileDialog(self)
dlg.setFileMode(QFileDialog.AnyFile)
home = os.path.expanduser('~') + "/Downloads"
dlg.setDirectory(home)
if dlg.exec_() == QDialog.Accepted:
selected_file = dlg.selectedFiles()[0]
else:
return
selected_file = Path(selected_file)
ext = selected_file.suffix
ext = ext.lower()
target_loc = data_loc / database / "contacts/images"
target_file = str(self.cont_id) + ext
target_dest = target_loc / target_file
shutil.move(str(selected_file), str(target_dest))
self.ui.image.setPixmap(QPixmap(str(target_dest)).scaled(self.ui.image.size(),Qt.KeepAspectRatio))
if __name__=="__main__":
app=QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QPSQL");
db.setHostName('hostname')
db.setDatabaseName('crm')
db.setUserName('usr')
db.setPassword('pw')
if (db.open()==False):
QMessageBox.critical(None, "Database Error", db.lastError().text())
myapp = Contact('')
myapp.show()
sys.exit(app.exec_())

In PyQt5, how do you save and restore user-typed values and dynamically created widgets inserted in to QTableWidget cells?

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.

Selecting parameters in mayagui in python

I have to create a GUI in maya python wherein I have to use Sql query to get
projectname,os,projectid , path . For example in my code if Projectname selected is "Abc" then os,projectid and path should be selected as per the selected projectname. how to do this??
I got the projectname but i cannot get other parameters
import os,mysql.connector as mc , platform
cmds.window(title='Test Window')
name = []
id = []
Os = []
path = []
wpath = []
# Query for getting various projects from db
cursor = db.cursor()
#selecting projectname
ans = cursor.fetchall()
cursor.close()
def projid(name):
#selecting project id
pid = cursor.fetchone()
print "out"
print pid
def Getprojid(z):
global pname
pname = cmds.optionMenu(z , query=True,value = True)
for ans1 in ans:
name .append(ans1[0])
cmds.columnLayout()
polygonSelectMenu = cmds.optionMenu(w = 250, h = 30, label = "Project
Selection:")
for proj in name:
cmds.menuItem(label = proj)
cmds.button(label='click me Select project ',
command='printTxtField(polygonSelectMenu)')
cmds.showWindow()
Since you are new to Python Maya Gui, you have to take care of few things while writing tools which involved ui interaction.
Keep your UI module and Core module(The core functionality part or the data layer) separate.
If you have any Database association, keep the DB module separate, which should ideally read, write or update the database.
If possible you can also keep an interaction layer or module for integrating the core and ui module along with db layer.
There can be more granular or organized designs, but as for starting this will help you to get the things done.
Given below is an example from your requirement.
Here I am assuming your project and its relevant details are coming from database or any other resources.
#=========================================================================#
# db code:
#=========================================================================#
# This module can have your own implementation. I am just writing some example code.
def get_projects_data(*args, **kwargs):
""" This should return all the records of the project table from your db.
For filters you can pass them through kwargs.
"""
return [{'id': 1021, 'name': 'Abc', 'os': 'some os', 'path': 'some path'},
{'id': 1022, 'name': 'Def', 'os': 'some other os', 'path': 'some other path'}]
#=========================================================================#
# Ui Code:
#=========================================================================#
import maya.cmds as cmds
class ProjectUI(object):
_name = 'ProjectManager'
_title = 'Project Manager'
def __init__(self, project_data=None):
self.project_data = project_data
self.win = None
# Create the UI
self.create_ui()
# Try to populate the projects
self.populate_projects()
# Force update the details for first time
self.update_project_details()
def create_ui(self):
"""This will create required UI components
"""
# First check if the ui exists, then close it
self.close()
# Now thw create the window
self.win = cmds.window(self.name, title=self.title)
# Creat a column layout with adjustable True and row spacing to 5
cmds.columnLayout(adj=True, rs=5)
# Create project option menu and add a change commang to trigger while
# you chnage the projects from the option menu.
self.project_om = cmds.optionMenu(
label='Projects', ChangeCommand=self.update_project_details)
# Create required text fields
self.project_id_tf = cmds.textFieldGrp(label='Id', editable=False)
self.project_path_tf = cmds.textFieldGrp(label='Path', editable=False)
self.project_os_tf = cmds.textFieldGrp(label='OS', editable=False)
def populate_projects(self):
"""This should populate all the available projects in poject option menu.
"""
# First check if we have any project data in hand. If not then we should
# exit right away.
if not self.project_data:
print('No project data found for populatin projects')
retturn
for data in project_data:
prject = data.get('name')
menuItem(label=project, parent=self.project_om)
def update_project_details(self, project=''):
"""This should update all other project details in UI and must me
triggered while changing projects from poject option menu.
"""
if not self.project_data:
retturn
if not project:
project = cmds.optionMenu(self.project_om, q=True, value=True)
project_details = None
for data in self.project_data:
if project == data.get('name'):
project_details = data
break
if not project_details:
print('No project details found for %s' % project)
return
proj_id = project_details.get('id')
proj_path = project_details.get('path')
proj_os = project_details.get('os')
cmds.textFieldGrp(self.project_id_tf, e=True, text='%s' % proj_id)
cmds.textFieldGrp(self.project_path_tf, e=True, text=proj_path)
cmds.textFieldGrp(self.project_os_tf, e=True, text=proj_os)
def show(self):
"""Just show the UI if its created ever.
"""
if self.win:
cmds.showWindow(self.win)
def close(self):
"""For deleting the UI if exists
"""
if cmds.window(self._name, query=True, exists=True):
cmds.deleteUi(self.name, window=True)
#=========================================================================#
# Integration Code:
#=========================================================================#
def main():
# First fetch the data
data = get_projects_data()
if not data:
print('No project data fetched.')
return
win = ProjectUI(project_data=data)
win.show()
# Return the win just if you want an pointer to same
return win
# Now just call the main method, whenever required
main()
The above code snippet is just an example. This is not tested inside maya. But I hope this will give you a starting point. Again if are not familiar with classes, you can do the same thing procedural way, by passing args. I will highly suggest PySide or PyQt along with PyMel for robust and efficient Ui Tools.

pyqt - Change widget text from other .py file

I have two python files, the first one is to handle database related stuff and the second one imports that file so I can use it with PyQt.
The problem is if I want to change the label in the first file it doesn't work the application just crashes.
The reason I want the first file to be able to change the label is if an error occurs when trying to connect to the DB.
Here is a short view of my code:
First file.
from mysql.connector import (connection)
from datetime import *
from RPI1_ui import Ui_Form
'''Handling of database related stuff'''
class DatabaseUtility(Ui_Form):
def __init__(self):
#DB Connection
self.cnx = None
self.cursor = None
def mysql_connect(self):
# Database connection
try:
self.cnx = connection.MySQLConnection(user='root', password='', host='127.0.0.1', database='spicadb')
self.cursor = self.cnx.cursor()
except connection.errors.InterfaceError as e:
self.lblShowInfo.setText(str(e)) # -> Try to change label
def get_table(self):
return self.run_command("SELECT * FROM tblseal")
def get_columns(self):
return self.run_command("SHOW COLUMNS FROM tblseal")
Second file.
from PyQt5.QtWidgets import QApplication, QWidget, QDialog
from datetime import *
from bs4 import BeautifulSoup as bs
import os
import sys
import DatabaseHandling
'''Convert UI file to Python'''
os.chdir("C:\\Users\Gianni Declercq\AppData\Local\Programs\Python\Python36-32\Scripts")
os.system("pyuic5.exe H:\QtProjects\\RPI1.ui -o H:\QtProjects\\RPI1_ui.py")
from RPI1_ui import Ui_Form # import after recreation of py file
from RPI1_Sec import SecondWindow
'''Main program'''
class MainWindow(QWidget, Ui_Form):
def __init__(self):
super(MainWindow, self).__init__()
# Initialize variables
self.dbu = DatabaseHandling.DatabaseUtility()
self.spica_reference = None
self.barcode = None
self.msl = None
self.package_body = None
self.window2 = None
# Get UI elements + resize window
self.setupUi(self)
self.resize(800, 480)
# Define what should happen on button click
self.btnQuit.clicked.connect(lambda: app.exit())
self.btnInsert.clicked.connect(lambda: self.get_entry())
self.btnTable.clicked.connect(lambda: self.new_window())
# Style buttons
self.btnQuit.setStyleSheet("background-color: red")
self.btnInsert.setStyleSheet("background-color: green")
self.btnTable.setStyleSheet("background-color: orange")
def get_entry(self):
try:
self.spica_reference = self.txtReferentie.text()
self.barcode = self.txtBarcode.text()
self.msl = self.txtMsl.text()
self.package_body = float(self.txtBodyPackage.text())
except ValueError:
self.lblShowInfo.setText("Please insert the correct values")
self.lblShowInfo.setStyleSheet('color: red')
else:
self.dbu.mysql_connect()
if self.dbu.cursor and self.dbu.cnx is not None:
self.dbu.add_entry(self.spica_reference, self.barcode, self.msl, self.package_body, self.calc_floor_life)
Give a "hook function" to the DatabaseUtility when initializing.
class MainWindow(QWidget, Ui_Form):
def __init__():
def ch_lab(text):
self.lblShowInfo.setText(text)
self.dbu = DatabaseHandling.DatabaseUtility(ch_lab)
class DatabaseUtility(Ui_Form):
def __init__(cb):
self.error_warn = cb
#.. error happening
self.error_warn('error')

Categories

Resources