Implementing setEditStrategy in editable QSqlQueryModel - python

This is a follow-up to this question. In there, we created an editable subclass of QSqlQueryModel, to use with complex queries.
Now I need to add a functionality like QTableModel's setEditStrategy so I can cache all changes and accept or revert them using buttons. PyQt apparently doesn't allow multiple inheritance and I cannot find sufficient documentation to re-implement this method in my custom model, therefor here's the question:
How can I re-implement QSqlTableModel.setEditStragety (or something like it) including RevertAll() and SubmitAll() in an editable QSqlQueryModel?
Here's a CVME: (I have out-commented the parts of of my Example class I would like to get working)
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel, QSqlTableModel
from PyQt5.QtWidgets import QApplication, QTableView, QWidget, QGridLayout
from PyQt5.Qt import QPushButton
db_file = "test.db"
def create_connection(file_path):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(file_path)
if not db.open():
print("Cannot establish a database connection to {}!".format(file_path))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Company TEXT, Country TEXT);")
q.exec_("INSERT INTO Manufacturers VALUES ('VW', 'Germany');")
q.exec_("INSERT INTO Manufacturers VALUES ('Honda' , 'Japan');")
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year INT);")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")
class SqlQueryModel_editable(QSqlQueryModel):
"""a subclass of QSqlQueryModel where individual columns can be defined as editable
"""
def __init__(self, editables):
"""editables should be a dict of format:
{INT editable_column_nr : (STR update query to be performed when changes are made on this column
INT model's column number for the filter-column (used in the where-clause),
)}
"""
super().__init__()
self.editables = editables
def flags(self, index):
fl = QSqlQueryModel.flags(self, index)
if index.column() in self.editables:
fl |= Qt.ItemIsEditable
return fl
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
mycolumn = index.column()
if mycolumn in self.editables:
(query, filter_col) = self.editables[mycolumn]
filter_value = self.index(index.row(), filter_col).data()
q = QSqlQuery(query.format(value, filter_value))
result = q.exec_()
if result:
self.query().exec_()
else:
print(self.query().lastError().text())
return result
return QSqlQueryModel.setData(self, index, value, role)
def setFilter(self, myfilter):
text = (self.query().lastQuery() + " WHERE " + myfilter)
self.setQuery(text)
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(400, 150)
self.createModel()
self.initUI()
def createModel(self):
editables = {1 : ("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'", 2)}
self.model = SqlQueryModel_editable(editables)
query = '''
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
cars.company,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
'''
q = QSqlQuery(query)
self.model.setQuery(q)
self.model.setFilter("cars.Company = 'VW'")
# self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
def initUI(self):
self.layout = QGridLayout()
self.setLayout(self.layout)
self.view = QTableView()
self.view.setModel(self.model)
self.view.hideColumn(2)
self.layout.addWidget(self.view,0,0,1,2)
self.accept_btn = QPushButton("Accept Changes")
# self.accept_btn.clicked.connect(self.model.submitAll)
self.layout.addWidget(self.accept_btn, 1,0)
self.reject_btn = QPushButton("Reject Changes")
# self.reject_btn.clicked.connect(self.model.revertAll)
self.layout.addWidget(self.reject_btn, 1,1)
if __name__ == '__main__':
app = QApplication(sys.argv)
if not create_connection(db_file):
sys.exit(-1)
fill_tables()
ex = Example()
ex.show()
sys.exit(app.exec_())
Edit to clarify:
I need an editable QSqlQueryModel, on which I can use submitAll() and revertAll(), so that changes to the model's data are only accepted after an Accept-button is clicked, or can be reverted using a "Reject" button.

Related

QComboBox populated from a relation model and connected to a QDataWidgetMapperbehaves strangely when setEditable(True)

I have a QComboBox populated from a relationModel of a QSqlRelationalTableModel and connected to a QDataWidgetMapper.
I select a row in the QTableView, this row (record) mapped to the QLineEdit and QComboBox widgets then I make some changes and save.
If I select another row and save without changing the QComboBox value, the value changes and submitted to the model.
I use the editable combobox not for adding items to the list, but to use the completer feature when I have a large list instead of dropping down the combobox view
Creating the db:
import sqlite3
conn = sqlite3.connect('customers.db')
c = conn.cursor()
c.execute("PRAGMA foreign_keys=on;")
c.execute("""CREATE TABLE IF NOT EXISTS provinces (
ProvinceId TEXT PRIMARY KEY,
Name TEXT NOT NULL
)""")
c.execute("""CREATE TABLE IF NOT EXISTS customers (
CustomerId TEXT PRIMARY KEY,
Name TEXT NOT NULL,
ProvinceId TEXT,
FOREIGN KEY (ProvinceId) REFERENCES provinces (ProvinceId)
ON UPDATE CASCADE
ON DELETE RESTRICT
)""")
c.execute("INSERT INTO provinces VALUES ('N', 'Northern')")
c.execute("INSERT INTO provinces VALUES ('E', 'Eastern')")
c.execute("INSERT INTO provinces VALUES ('W', 'Western')")
c.execute("INSERT INTO provinces VALUES ('S', 'Southern')")
c.execute("INSERT INTO provinces VALUES ('C', 'Central')")
c.execute("INSERT INTO customers VALUES ('1', 'customer1', 'N')")
c.execute("INSERT INTO customers VALUES ('2', 'customer2', 'E')")
c.execute("INSERT INTO customers VALUES ('3', 'customer3', 'W')")
c.execute("INSERT INTO customers VALUES ('4', 'customer4', 'S')")
c.execute("INSERT INTO customers VALUES ('5', 'customer5', 'C')")
conn.commit()
conn.close()
and here is the window:
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
class Window(QWidget):
def __init__(self):
super().__init__()
self.db = QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("customers.db")
self.db.open()
self.model = QSqlRelationalTableModel(self, self.db)
self.model.setTable("customers")
self.model.setRelation(2, QSqlRelation("provinces", "ProvinceId", "Name"))
self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit)
self.model.select()
self.id = QLineEdit()
self.name = QLineEdit()
self.province = QComboBox()
# stuck here
self.province.setEditable(True)
self.province.setModel(self.model.relationModel(2))
self.province.setModelColumn(1)
self.province.setView(QTableView())
self.mapper = QDataWidgetMapper()
self.mapper.setItemDelegate(QSqlRelationalDelegate())
self.mapper.setModel(self.model)
self.mapper.addMapping(self.id, 0)
self.mapper.addMapping(self.name, 1)
self.mapper.addMapping(self.province, 2)
save = QPushButton("Save")
save.clicked.connect(self.submit)
self.tableView = QTableView()
self.tableView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.tableView.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.tableView.setModel(self.model)
self.tableView.clicked.connect(lambda: self.mapper.setCurrentModelIndex(self.tableView.currentIndex()))
vBox = QVBoxLayout()
vBox.addWidget(self.id)
vBox.addWidget(self.name)
vBox.addWidget(self.province)
vBox.addSpacing(20)
vBox.addWidget(save)
vBox.addWidget(self.tableView)
self.setLayout(vBox)
self.mapper.toFirst()
def submit(self):
self.mapper.submit()
self.model.submitAll()
def main():
import sys
App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec_())
if __name__ == '__main__':
main()
An important thing to consider is that item delegates (and, specifically, QSqlRelationalDelegate) use the widget's user property to read and write data from and to the widget.
The user property of QComboBox is currentText; if it's not editable, its value is an empty string (for -1 index) or the current item's text, and setting that property results in the combo trying to look for the first item that fully matches that text, and changes the current index if a match is found.
When the combo is editable, though, only the text is changed, not the current index, and it's also possible to set
Now, after some digging, I found various "culprits" to the issue you're facing.
QDataWidgetMapper uses the EditRole to both commit data and populate widgets. This clearly represents a problem since the edit role is what the relational model uses for the actual data set on the model (eg. "S" for Southern), while the display role is what is used to display the related value.
The result of all the above aspects is that, assuming the combo is not changed by the user:
the mapper tries to set the data, based on the current delegate editor with setModelData();
the delegate uses the current index (not the current text!) to get both the display and edit role to be set on the model;
the model tries to set both values, but will only be able to set the edit role due to its relational nature;
the data changed causes the mapper to repopulate the widgets;
the display value based on the combo index is then set to the widget using setEditorData();
Also note that, until Qt 5.12 (see QTBUG-59632), the above caused a further issue as the default implementation of setEditorData uses the edit role, so the editable combo would also get the related letter instead of the actual value display.
Considering the above, there are two options:
subclass QSqlRelationalDelegate and properly implement setModelData() by matching the current text and using the relation model
from PyQt5.QtCore import *
_version = tuple(map(int, QT_VERSION_STR.split('.')))
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
value = editor.currentText()
if not value:
return
childModel = model.relationModel(index.column())
for column in range(2):
match = childModel.match(childModel.index(0, column),
Qt.DisplayRole, value, Qt.MatchStartsWith)
if match:
match = match[0]
displayValue = match.sibling(match.row(), 1).data()
editValue = match.sibling(match.row(), 0).data()
model.setData(index, displayValue, Qt.DisplayRole)
model.setData(index, editValue, Qt.EditRole)
return
super().setModelData(editor, model, index)
if _version[1] < 12:
# fix for old Qt versions that don't properly update the QComboBox
def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
value = index.data()
if isinstance(value, str):
propName = editor.metaObject().userProperty().name()
editor.setProperty(propName, value)
else:
super().setEditorData(editor, index)
subclass QComboBox and ensure that it properly updates the index with the current text, by using a new user property; this still requires implementing setModelData to override the default behavior for QComboBox
class MapperCombo(QComboBox):
#pyqtProperty(str, user=True)
def mapperText(self):
text = self.currentText()
if text == self.currentData(Qt.DisplayRole):
return text
model = self.model()
for column in range(2):
match = model.match(model.index(0, column),
Qt.DisplayRole, text, Qt.MatchStartsWith)
if match:
self.setCurrentIndex(match[0].row())
return self.currentText()
return self.itemText(self.currentIndex())
#mapperText.setter
def mapperText(self, text):
model = self.model()
for column in range(2):
match = model.match(model.index(0, column),
Qt.DisplayRole, text, Qt.MatchStartsWith)
if match:
index = match[0].row()
break
else:
index = 0
if index != self.currentIndex():
self.setCurrentIndex(index)
else:
self.setCurrentText(self.currentData(Qt.DisplayRole))
#property
def mapperValue(self):
return self.model().data(self.model().index(
self.currentIndex(), 0), Qt.DisplayRole)
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if isinstance(editor, MapperCombo):
model.setData(index, editor.mapperText, Qt.DisplayRole)
model.setData(index, editor.mapperValue, Qt.EditRole)
else:
super().setModelData(editor, model, index)
Finally, a QLineEdit with a proper QCompleter could be used, but this still requires subclassing the delegate, as setModelData needs to use the proper string.
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if model.relation(index.column()).isValid():
value = editor.text()
if value:
childModel = model.relationModel(index.column())
match = childModel.match(childModel.index(0, 1),
Qt.DisplayRole, value, Qt.MatchStartsWith)
if match:
childIndex = match[0]
model.setData(index, childIndex.data(), Qt.DisplayRole)
model.setData(index,
childIndex.sibling(childIndex.row(), 0).data(), Qt.EditRole)
editor.setText(childIndex.data())
else:
super().setModelData(editor, model, index)
Some further notes and suggestions:
if the mapped data is visible, it's preferable to use the ManualSubmit policy (self.mapper.setSubmitPolicy(self.mapper.ManualSubmit)), alternatively, you could subclass the model and find ways to visually display modified cells until the changes are submitted;
there's no need for a lambda to update the current index when clicking, since clicked already provides the new index: self.tableView.clicked.connect(self.mapper.setCurrentModelIndex)
submitting the model will cause the mapper to reset the current index, with the result that a further editing (without selecting a new item from the table) will be ignored, so you should restore it after changes have been applied:
def submit(self):
current = self.mapper.currentIndex()
self.mapper.submit()
self.model.submitAll()
self.mapper.setCurrentIndex(current)

How to set value to NULL in editable QTableView with QSqlTableModel

I have a QTableView that displays subset of the data from a table in SQLite database. The table is editable only for nullable numeric columns. I created two delegates - one for read-only columns:
class ReadOnlyDelegate(QtWidgets.QItemDelegate):
def editorEvent(self, *args, **kwargs):
return False
def createEditor(self, *args, **kwargs):
return None
and one for editable columns:
class LabelDelegate(QtWidgets.QItemDelegate):
def createEditor(self, parent, options, index):
self.le = QtWidgets.QLineEdit(parent)
return self.le
The table is fed by customized QSqlTableModel, where I overwrite submitAll method:
class MySqlTableModel(QtSql.QSqlTableModel):
def submitAll(self):
for row in range(self.rowCount()):
for col in range(self.columnCount()):
if self.isDirty(self.index(row, col)):
val = self.record(row).value(col)
if val == '':
self.record(row).setNull(col)
else:
try:
self.record(row).setValue(col, float(val))
except (TypeError, ValueError):
display_error_msg('Can not convert to float',
f'The value {val} could not be converted to float')
raise
super().submitAll()
Expected behaviour is (1) to convert values to float before sending to database, (2) reject inputs that can't be converted to float and (3) to convert empty string to NULL. (1) and (2) work as expected, however the last bit is not working. When debugging method .submitAll() it raises no exception on the line self.record(row).setNull(col) but it also seems to have no effect. An empty string is sent and persisted in database. Any ideas why and how to fix it?
I don't see the need to override the submitAll() method, instead you can implement the logic in the setModelData() method:
import sys
from PySide2 import QtCore, QtWidgets, QtSql
TABLENAME = "t1"
def create_connection():
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("database.db")
if not db.open():
QtWidgets.QMessageBox.critical(
None,
QtWidgets.QApplication.instance().tr("Cannot open database"),
QtWidgets.QApplication.instance().tr(
"Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information "
"how to build it.\n\n"
"Click Cancel to exit."
),
QtWidgets.QMessageBox.Cancel,
)
return False
if TABLENAME in db.tables():
return True
query = QtSql.QSqlQuery()
if not query.exec_(f"CREATE TABLE IF NOT EXISTS {TABLENAME}(a REAL, b text);"):
print(query.lastError().text())
queries = (
f"INSERT INTO {TABLENAME} VALUES(1, '1');",
f"INSERT INTO {TABLENAME} VALUES(NULL, '2');",
f"INSERT INTO {TABLENAME} VALUES(3, '3');",
f"INSERT INTO {TABLENAME} VALUES(NULL, '4');",
f"INSERT INTO {TABLENAME} VALUES(5, '4');",
f"INSERT INTO {TABLENAME} VALUES(NULL, '5');",
f"INSERT INTO {TABLENAME} VALUES(NULL, '7');",
)
for query in queries:
q = QtSql.QSqlQuery()
if not q.exec_(query):
print(q.lastError().text(), query)
return False
return True
class ReadOnlyDelegate(QtWidgets.QStyledItemDelegate):
def editorEvent(self, *args, **kwargs):
return False
def createEditor(self, *args, **kwargs):
return None
class LabelDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, options, index):
le = QtWidgets.QLineEdit(parent)
return le
def setModelData(self, editor, model, index):
value = editor.text()
if not value:
model.setData(index, None, QtCore.Qt.EditRole)
else:
try:
number = float(value)
except (TypeError, ValueError):
print(
f"Can not convert to float, The value {value} could not be converted to float'"
)
else:
model.setData(index, number, QtCore.Qt.EditRole)
def main(args):
app = QtWidgets.QApplication(args)
if not create_connection():
sys.exit(-1)
view = QtWidgets.QTableView()
model = QtSql.QSqlTableModel()
model.setTable(TABLENAME)
model.setEditStrategy(QtSql.QSqlTableModel.OnManualSubmit)
model.select()
view.setModel(model)
label_delegate = LabelDelegate(view)
view.setItemDelegateForColumn(0, label_delegate)
readonly_delegate = ReadOnlyDelegate(view)
view.setItemDelegateForColumn(1, readonly_delegate)
button = QtWidgets.QPushButton("Submit all")
widget = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(widget)
lay.addWidget(button)
lay.addWidget(view)
widget.resize(640, 480)
widget.show()
button.clicked.connect(model.submitAll)
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main(sys.argv)
Tested on Linux with:
PyQt5 5.15.4
PySide2 5.15.2
Python 3.9.4

Program quit with error when using QCompleter

I'm writing a program with pyqt5 and want to make the QlineEdit show history input by using sqlite to store the inputs. I use a signal to catch the cursor when focusInEvent happens and select the history records at that time, then I put the results into QCompleter so it can pop up in QlineEdit. Now I can make the history inputs show in QlineEdit object but when I click on any value, 1s later, the whole program quit automatically with error, which says "Python has stopped".
class FocusLineEdit(QLineEdit):
ac = pyqtSignal(list)
def __init__(self, parent=None):
super(FocusLineEdit, self).__init__(parent)
self.ac.connect(self.addCompleter)
def focusInEvent(self, event):
rtl = call_history(self.objectName())
self.ac.emit(rtl)
def addCompleter(self, rtl):
self.autoCompleter = QCompleter(rtl)
self.autoCompleter.setCompletionMode(1)
self.setCompleter(self.autoCompleter)
def focusOutEvent(self, event):
pass
It is difficult to analyze where the problem is without you provide an MCVE, so my response implements what you require without taking into account your current code for it must meet the following requirements:
You must have 2 tables: objects and history that are related.
The table objects saves the name of the filtering history, it is similar to the use of the objectName that you use, but in general 2 widgets can access the same history if the connection establishes the same name
In the history table the information of the words associated with the id of the objects table is saved.
In my example I use the following instructions to create them:
CREATE TABLE IF NOT EXISTS objects (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);
CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY AUTOINCREMENT, id_object INTEGER REFERENCES objects (id), word TEXT NOT NULL, UNIQUE (id_object, word));
Also to test it I created a data, if you already have data then you must eliminate the if test: and everything inside.
Finally for a better understanding of my solution I show a QTableView that is changing according to the selection.
from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
def createConnection():
db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
db_path = 'test.db'
db.setDatabaseName(db_path)
if not db.open():
QMessageBox.critical(None, qApp.tr("Cannot open database"),
qApp.tr("Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information "
"how to build it.\n\n"
"Click Cancel to exit."),
QMessageBox.Cancel)
return False
test = True
if test:
query = QtSql.QSqlQuery()
if not query.exec_('CREATE TABLE IF NOT EXISTS objects (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);'):
return False
if not query.exec_('CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY AUTOINCREMENT, id_object INTEGER REFERENCES objects (id), word TEXT NOT NULL, UNIQUE (id_object, word));'):
return False
for i in range(3):
query.prepare('INSERT INTO objects (name) VALUES (?)')
query.addBindValue("obj{}".format(i))
if not query.exec_():
print(query.lastError().text())
import requests
import random
word_site = "http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain"
response = requests.get(word_site)
WORDS = response.content.decode().splitlines()
print(WORDS)
for i in range(3):
for text in random.sample(WORDS, 50):
query.prepare('INSERT INTO history (id_object, word) VALUES (?, ?)')
query.addBindValue(i+1)
query.addBindValue(text)
if not query.exec_():
print(query.lastError().text())
return True
class Completer(QtWidgets.QCompleter):
def __init__(self, parent=None):
super(Completer, self).__init__(parent)
self._last_words = []
def splitPath(self, path):
if path[-1] != ' ':
words = path.split()
self._last_words = words[:-1] if len(words) > 1 else []
return [words[-1]]
else:
QtCore.QTimer.singleShot(0, self.popup().hide)
return []
def pathFromIndex(self, index):
val = super(Completer, self).pathFromIndex(index)
return ' '.join(self._last_words + [val])
class HistoryManager(QtCore.QObject):
nameChanged = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(HistoryManager, self).__init__(parent)
model = QtSql.QSqlRelationalTableModel(self)
model.setTable("history")
model.setRelation(1, QtSql.QSqlRelation("objects", "id", "name"))
model.select()
self._proxy = QtCore.QSortFilterProxyModel(self)
self._proxy.setSourceModel(model)
self._proxy.setFilterKeyColumn(1)
# proxy.setFilterFixedString("obj1")
self._widgets = {}
self._completer = Completer(self)
self._completer.setModel(self._proxy)
self._completer.setCompletionColumn(2)
def register_widget(self, widget, objectname):
# TODO
if callable(getattr(widget, "setCompleter")):
widget.installEventFilter(self)
self._widgets[widget] = objectname
return True
return False
def eventFilter(self, obj, event):
if obj in self._widgets:
if event.type() == QtCore.QEvent.FocusIn:
name = self._widgets[obj]
self._proxy.setFilterFixedString(name)
obj.setCompleter(self._completer)
self.nameChanged.emit(name)
elif event.type() == QtCore.QEvent.FocusOut:
obj.setCompleter(None)
self._proxy.setFilterFixedString("")
self.nameChanged.emit("")
return super(HistoryManager, self).eventFilter(obj, event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._manager = HistoryManager()
model = QtSql.QSqlRelationalTableModel(self)
model.setTable("history")
model.setRelation(1, QtSql.QSqlRelation("objects", "id", "name"))
model.select()
self._proxy = QtCore.QSortFilterProxyModel(self)
self._proxy.setSourceModel(model)
self._proxy.setFilterKeyColumn(1)
tv = QtWidgets.QTableView()
tv.setModel(self._proxy)
vlay = QtWidgets.QVBoxLayout()
for i in range(3):
le = QtWidgets.QLineEdit()
vlay.addWidget(le)
self._manager.register_widget(le, "obj{}".format(i))
vlay.addStretch()
lay = QtWidgets.QHBoxLayout(self)
lay.addWidget(tv, stretch=1)
lay.addLayout(vlay)
self._manager.nameChanged.connect(self._proxy.setFilterFixedString)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
if not createConnection():
sys.exit(-1)
manager = HistoryManager()
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

PyQt TableView insert computation from previous 2 rows in last column

I have a simple Qtableview where the user inserts dates on the underneath sqlite database.I use a custom delegate on column 3 and 4 to insert a date time directly. I would like to dynamically insert on col 5 i.e total the time delta.In the picture below i have inserted manually 01:00 hour but i would like it to be automatic.One way would be given by this answer:
Virtual column in QTableView?, and I've used it before but this creates a virtual column and doesn't insert it in the DB.
Should I therefore use QSqlQuery and then insert it on the total column or rather use the tableview itself?
The code so far:
from PyQt5.QtWidgets import QMainWindow, QApplication, QStyledItemDelegate, QDateTimeEdit, QHeaderView
from PyQt5.QtCore import pyqtSlot, QDateTime, Qt, QAbstractItemModel,QModelIndex, QDateTime
from datetime import datetime,timedelta
import sys
import essai_find
from essai_find_db import *
class MainWindow(QMainWindow, essai_find.Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.DB = essaiFindDb()
self.db_model = QSqlRelationalTableModel()
self.db_model.setTable('Pilots_exp')
self.db_model.setEditStrategy(QSqlRelationalTableModel.OnFieldChange)
self.db_model.setRelation(2, QSqlRelation('Aircraft', 'immatriculation', 'immatriculation'))
self.db_model.select()
self.tableView.setModel(self.db_model)
self.tableView.setColumnHidden(0, True)
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.custom_delegate = customDelegate()
self.tableView.setItemDelegateForColumn(3, self.custom_delegate)
self.tableView.setItemDelegateForColumn(4, self.custom_delegate)
self.label.setText(str(self.hours_minutes()))
def get_tot_hours(self):
"""select dates from database """
query1 = QSqlQuery("SELECT date_time1,date_time2 FROM Pilots_exp")
liste = []
while query1.next():
date1 = query1.value(0)
date2 = query1.value(1)
essai = datetime.strptime(date2, "%Y/%m/%d %H:%M") - datetime.strptime(date1, "%Y/%m/%d %H:%M")
liste.append(essai)
total = sum(liste, timedelta())
return total
def hours_minutes(self):
"""conversion of time delta get_tot_hours to hours"""
td = self.get_tot_hours()
print(td)
resultat = td.days * 24 + td.seconds // 3600
print(resultat)
return resultat
def get_hours_diff(self):
query1 = QSqlQuery("SELECT date_time1,date_time2 FROM Pilots_exp")
result = []
while query1.next():
date1 = query1.value(0)
date2 = query1.value(1)
diff = datetime.strptime(date2, "%Y/%m/%d %H:%M") - datetime.strptime(date1, "%Y/%m/%d %H:%M")
result.append(str(diff))
return result
#pyqtSlot()
def on_pushButton_clicked(self):
self.add_record()
def add_record(self):
row = self.db_model.rowCount()
self.db_model.insertRow(row)
class customDelegate(QStyledItemDelegate):
"""DELEGATE INSERT CUSTOM DATEEDIT IN CELL """
def __init__(self, parent=None):
super(customDelegate, self).__init__(parent)
def createEditor(self, parent, option, index):
date_edit = QDateTimeEdit(parent)
date_edit.setDisplayFormat("yyyy/MM/dd HH:mm")
date_edit.setDateTime(QDateTime.currentDateTime())
date_edit.setCalendarPopup(True)
return date_edit
def setModelData(self, editor, model, index):
value = editor.dateTime().toString("yyyy/MM/dd HH:mm")
model.setData(index, value)
def setEditorData(self, editor, index):
value = index.model().data(index, Qt.EditRole)
qdate = QDateTime().fromString(value, "yyyy/MM/dd HH:mm")
editor.setDateTime(qdate)
if __name__ == '__main__':
try:
app = QApplication(sys.argv)
form = MainWindow()
# app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
form.show()
app.exec_()
sys.exit(0)
except NameError:
print("Name Error: ", sys.exc_info()[1])
except SystemExit:
print("Closing Window....")
except Exception:
print(sys.exc_info()[1])
The db is a very simple:
from PyQt5.QtSql import *
class essaiFindDb():
def __init__(self):
self.db = QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("essai_find_database.db")
self.db.open()
query = QSqlQuery()
query.exec_('''CREATE TABLE Pilots_exp(id INTEGER PRIMARY KEY UNIQUE , pilot_1 TEXT,aircraft TEXT,
date_time1 TEXT, date_time2 TEXT, total TEXT)''')
query.exec_(
'''CREATE TABLE Aircraft(id INTEGER PRIMARY KEY, immatriculation TEXT)''')
query.exec_()
self.db.commit()
self.db.close()
if __name__ == '__main__':
essaiFindDb()

PyQt4 How to send push button signal to another class in Python?

I need your advice and help for my code.
I am making a GUI program. Basically the program does the followings:
Gets input from the user
When a certain button is pushed, the program retrieves all the input and saved them in the database
The program does computations.
Shows the output
Here is the simple version of the program, calculator program:
As you can see in the figure, it takes 4 input from user, (1) any integer for var1, (2) any integer for var2, (3) operator (addition or subtraction) and (4) click button.
The GUI was designed using QtDesigner.
And here is my code:
import sys
import sqlite3
from PyQt4 import QtCore, QtGui
from testgui import Ui_MainWindow
class ShowAndInput(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#connect to the database
self.connect_to_db()
#create table in the database
self.create_table()
#add items to the comboBox
self.add_items()
#calculate button to start calculating, gives signal to the next class Calculate
self.ui.pushButton_3.clicked.connect(self.calculate)
#close button to close the window
QtCore.QObject.connect(self.ui.pushButton,QtCore.SIGNAL("clicked()"), quit)
self.conn.close()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def create_table(self):
self.cur.execute('''CREATE TABLE IF NOT EXISTS Input (var1 INTEGER, var2 INTEGER)''')
self.conn.commit()
def add_items(self):
lst = ['addition', 'subtraction']
self.ui.comboBox.clear()
self.ui.comboBox.addItems(lst)
def calculate(self):
calculate = Calculate(self)
calculate.exec_()
So the purpose of the first class was to show the GUI and take input from the user. If user clicks the pushbutton named 'Calculate', it should connect to the new class named 'Calculate'.
Here is my code for the Calculate class:
class Calculate(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QWidget.__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#validator to validate that the input is integer or digit
#connect to database
self.connect_to_db()
#get the input values and store them in the database
self.get_the_values()
#print the output
PrintOutcome()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def get_the_values(self):
dicti = {}
dicti['var1'] = self.ui.lineEdit.text()
dicti['var2'] = self.ui.lineEdit_2.text()
for key, val in sorted(dicti.items()):
key = str(key)
val = int(val)
self.cur.execute('INSERT OR IGNORE INTO Input (?) VALUES (?)',(key,val))
self.conn.commit()
And at the end of this class, it calls another class named 'PrintOutcome' to print out the result in the QTextBrowser. Here is the last piece of the code:
class PrintOutcome(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QWidget.__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#connect to database
self.connect_to_db()
#get the value of the combobox
self.get_combox()
#print the output
if self.operator == 'addition': self.print_add()
elif self.operator == 'subtraction': self.print_sub()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def get_combox(self):
self.operator = str(self.ui.comboBox.currentText())
def print_add(self):
dicti={}
for i in range(2):
i = i + 1
dicti['var{0}'.format(i)] = self.cur.execute('SELECT var{0} FROM Input'.format(i))
dicti['var{0}'.format(i)] = int(self.cur.fetchone()[0])
text = str(dicti['var{0}'.format(i)])
self.ui.textBrowser.append(text)
result = dicti['var1'] + dicti['var2']
textsep = '-'*25+'(+)'
text = str(result)
self.ui.textBrowser.append(text)
self.ui.textBrowser.append(text)
def print_sub(self):
dicti={}
for i in range(2):
i = i + 1
dicti['var{0}'.format(i)] = self.cur.execute('SELECT var{0} FROM Input'.format(i))
dicti['var{0}'.format(i)] = int(self.cur.fetchone()[0])
text = str(dicti['var{0}'.format(i)])
self.ui.textBrowser.append(text)
result = dicti['var1'] + dicti['var2']
textsep = '-'*25+'(-)'
text = str(result)
self.ui.textBrowser.append(text)
self.ui.textBrowser.append(text)
if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
myapp = ShowAndInput()
myapp.show()
sys.exit(app.exec_())
When I ran it, everything was fine. But when I pressed the calculate button, it returned an error message.
Here is the error message: RuntimeError: super-class init() of type Calculate was never called.
Any respond will be much appreciated. Thanks a lot.
Regards,
Arnold

Categories