Using PyQt5, I'm trying to build a GUI part that displays two parts; one gives an overview in a QTableView (class Overview), the other shows details of the selected item of the overview in a QTabWidget (class DetailledView).
Now, the data in the QTabWidget is spread over several tabs, showing data from different tables (it's much more complex than in the minimal example below). Since these behave like key:value pairs, I want to display them vertically instead of horizontally. So I have an InvertedTable class that does this.
However, the filtering of the QTabWidget tables doesn't quite work: when I select an item in the Overview Table, the filters on the QTabWidget do get updated, but it's only visible once I click on a different tab.
I think the problem is the layering of the proxy models: for the InvertedTables, I have two layers of proxy models. One is a normal QSortFilterProxyModel that I use to filter for the right subset of data to be displayed. On top of that, there's another proxy model ("FlippedProxyModel", subclassed from QSortFilterProxyModel) to invert the data. I use the first one for the filtering, and I think that's why the QTableViews don't get updated immediately. (When I use SQLTables instead of InvertedTables in the code below, everything is fine - except for the orientation, of course.)
This is probably also the reason for the empty columns remaining after filtering...
I could put the flipped model below the filter model, but then the column I want to filter on is already row at the time of filtering, so how would I filter on that? (Also, the tables being displayed may get large, so putting the filtering fist seems liek a good idea.)
How can I use QSortProxyFilterModels to filter AND vertically invert a table so that the QTableView showing it gets updated immediately upon filtering?
An MCVE is contained below:
#!/usr/bin/python3
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel
from PyQt5.QtWidgets import (QTableView, QTabWidget, QGridLayout, QWidget,
QApplication)
from PyQt5.QtCore import (Qt, pyqtSignal)
from PyQt5.Qt import QModelIndex, QSortFilterProxyModel, QSqlRelationalDelegate
import sys
db_file = "test.db"
#========================================
# handle database:
def create_connection(db_file):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(db_file)
if not db.open():
print("Cannot establish a database connection to {}!".format(db_file))
return False
return db
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Name 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);")
#========================================
# general classes:
class FlippedProxyModel(QSortFilterProxyModel):
"""a proxy model where all columns and rows are inverted
(compared to the source model);
source: http://www.howtobuildsoftware.com/index.php/how-do/bgJv/pyqt-pyside-qsqltablemodel-qsqldatabase-qsqlrelationaltablemodel-with-qsqlrelationaldelegate-not-working-behind-qabstractproxymodel
"""
def __init__(self, parent=None):
super().__init__(parent)
def mapFromSource(self, index):
return self.createIndex(index.column(), index.row())
def mapToSource(self, index):
return self.sourceModel().index(index.column(), index.row(), QModelIndex())
def columnCount(self, parent):
return self.sourceModel().rowCount(QModelIndex())
def rowCount(self, parent):
return self.sourceModel().columnCount(QModelIndex())
def index(self, row, column, parent):
return self.createIndex(row, column)
def parent(self, index):
return QModelIndex()
def data(self, index, role):
return self.sourceModel().data(self.mapToSource(index), role)
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal:
return self.sourceModel().headerData(section, Qt.Vertical, role)
if orientation == Qt.Vertical:
return self.sourceModel().headerData(section, Qt.Horizontal, role)
class FlippedProxyDelegate(QSqlRelationalDelegate):
"""a delegate for handling data displayed through a FlippedProxyModel;
source: http://www.howtobuildsoftware.com/index.php/how-do/bgJv/pyqt-pyside-qsqltablemodel-qsqldatabase-qsqlrelationaltablemodel-with-qsqlrelationaldelegate-not-working-behind-qabstractproxymodel
"""
def createEditor(self, parent, option, index):
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(FlippedProxyDelegate, self).createEditor(parent, option, base_index)
def setEditorData(self, editor, index):
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(FlippedProxyDelegate, self).setEditorData(editor, base_index)
def setModelData(self, editor, model, index):
base_model = model.sourceModel()
base_index = model.mapToSource(index)
return super(FlippedProxyDelegate, self).setModelData(editor, base_model, base_index)
class SQLTable(QWidget):
def __init__(self, query):
super().__init__()
self.create_model(query)
self.init_UI()
def create_model(self, query):
raw_model = QSqlTableModel()
q = QSqlQuery()
q.exec_(query)
self.check_error(q)
raw_model.setQuery(q)
self.model = QSortFilterProxyModel()
self.model.setSourceModel(raw_model)
def init_UI(self):
self.grid = QGridLayout()
self.setLayout(self.grid)
self.table = QTableView()
self.grid.addWidget(self.table, 1,0)
self.table.setModel(self.model)
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
self.mydb.close()
exit(1)
class InvertedTable(SQLTable):
"""a Widget that displays content of an SQLite query inverted
(= with rows and columns flipped);
"""
def __init__(self, query = ""):
self.query = query
super().__init__(query)
self.flipped_model = FlippedProxyModel()
self.flipped_model.setSourceModel(self.model)
self.table.setModel(self.flipped_model)
self.table.setItemDelegate(FlippedProxyDelegate(self.table)) # use flipped proxy delegate
h_header = self.table.horizontalHeader()
h_header.hide()
v_header = self.table.verticalHeader()
v_header.setFixedWidth(70)
self.table.resizeColumnsToContents()
#========================================
# application classes:
class MainWidget(QWidget):
def __init__(self, company):
super().__init__()
self.init_UI()
self.filter(company)
self.overview.company_changed.connect(self.details.filter)
def init_UI(self):
self.resize(400,400)
self.grid = QGridLayout()
self.setLayout(self.grid)
self.overview = Overview()
self.grid.addWidget(self.overview, 0, 0)
self.details = DetailedView()
self.grid.addWidget(self.details, 1, 0)
def filter(self, company):
self.details.filter(company)
class Overview(SQLTable):
company_changed = pyqtSignal(str)
def __init__(self):
query = "select * from Manufacturers"
super().__init__(query)
self.table.clicked.connect(self.on_clicked)
def on_clicked(self, index):
company_index = self.model.index(index.row(), 0)
company = self.model.data(company_index)
self.company_changed.emit(company)
class DetailedView(QTabWidget):
def __init__(self):
super().__init__()
self.add_tab1()
self.add_tab2()
def add_tab1(self):
query = "select * from cars"
self.tab1 = InvertedTable(query)
self.addTab(self.tab1, "Cars")
def add_tab2(self):
query = "SELECT company, count(*) as nr_cars from cars group by company"
self.tab2 = InvertedTable(query)
self.addTab(self.tab2, "Numbers")
def filter(self, company):
for mytab in [self.tab1, self.tab2]:
mytab.model.setFilterKeyColumn(0)
mytab.model.setFilterFixedString(company)
#========================================
# execution:
def main():
mydb = create_connection(db_file)
if not mydb:
sys.exit(-1)
fill_tables()
app = QApplication(sys.argv)
ex = MainWidget('VW')
ex.show()
result = app.exec_()
if (mydb.open()):
mydb.close()
sys.exit(result)
if __name__ == '__main__':
main()