1

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
CodingCat
  • 4,999
  • 10
  • 37
  • 59

2 Answers2

3

the solution of @s.nick is forced, it is eliminating the widgets of QTabWidget, and adding back, which consumes a lot of resources if a lot of data is handled.

The problem is that the proxys expect the layoutAboutToBeChanged and layoutChanged signals, but in the case of QSortProxyModel it is not doing so, so the solution is simply to emited it:

def filter(self, company):
    for mytab in [self.tab1, self.tab2]:
        mytab.model.layoutAboutToBeChanged.emit()
        mytab.model.setFilterFixedString(company)
        mytab.model.layoutChanged.emit()

Besides I see that you are using QSqlTableModel unnecessarily, would be enough with QSqlQueryModel, QSqlTableModel in this case is overdimensioned.

Another thing to improve is that FlippedProxyModel should inherit from QIdentityProxyModel, it is not necessary to filter or sort so QSortProxyModel is also overdimensioned.

I have modified the application with the improvements mentioned above, and the resulting code is as follows:

#!/usr/bin/python3

import sys

from PyQt5.QtCore import Qt, pyqtSignal, QIdentityProxyModel, QModelIndex, QSortFilterProxyModel
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel, QSqlRelationalDelegate
from PyQt5.QtWidgets import QTableView, QTabWidget, QGridLayout, QWidget, QApplication

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(QIdentityProxyModel):
    """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 mapFromSource(self, index):
        return self.index(index.column(), index.row())

    def mapToSource(self, index):
        return self.sourceModel().index(index.column(), index.row())

    def columnCount(self, parent=QModelIndex()):
        return self.sourceModel().rowCount(parent)

    def rowCount(self, parent=QModelIndex()):
        return self.sourceModel().columnCount(parent)

    def index(self, row, column, parent=QModelIndex()):
        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):
        self.model = QSortFilterProxyModel()
        querymodel = QSqlQueryModel()
        querymodel.setQuery(query)
        self.model.setSourceModel(querymodel)

    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)


class InvertedTable(SQLTable):
    """a Widget that displays content of an SQLite query inverted
    (= with rows and columns flipped);
    """

    def __init__(self, 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.layoutAboutToBeChanged.emit()
            mytab.model.setFilterFixedString(company)
            mytab.model.layoutChanged.emit()


# ========================================
# 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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I do need the QSqlTableModel, as the tables in my actual application are needed for user interaction with the db (the tables need to be editable). But the rest of your insights have been very helpful. Thank you! – CodingCat Apr 10 '18 at 07:37
  • @CodingCat QSqlTableModel is a QSqlQueryModel specialized in tables, you should not insert queries in a QSqlTableModel, but use its select and filter methods, or similar ones. :D – eyllanesc Apr 10 '18 at 07:39
  • but what if the query is more complex than just "select * from table"? Like, left joins and summarized numbers? I can express all that as an SQL query and then put this to QSqlTableModel. Can you explain why that is problematic? ETA: I've made this a separate question: https://stackoverflow.com/questions/49752388/using-qsqltablemodel-for-a-complex-query – CodingCat Apr 10 '18 at 11:25
0

It works.

Try it:

Added row park to the DetailedView class

class DetailedView(QTabWidget):
    def __init__(self):
        super().__init__()
        self.name_tab = ["Cars", "Numbers"]                  # +++
        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):
        self.clear()                                         # +++

        #for mytab in [self.tab1, self.tab2]:                # ---
        for i, mytab in enumerate([self.tab1, self.tab2]):
            mytab.model.setFilterKeyColumn(0)
            mytab.model.setFilterFixedString(company)
            self.addTab(mytab, self.name_tab[i])             # +++
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
S. Nick
  • 12,879
  • 8
  • 25
  • 33