Layering proxy models in PyQt

最后都变了- 提交于 2019-12-13 19:23:20

问题


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()

回答1:


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()



回答2:


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])             # +++


来源:https://stackoverflow.com/questions/49734880/layering-proxy-models-in-pyqt

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!