【问题标题】:Layering proxy models in PyQtPyQt 中的分层代理模型
【发布时间】:2018-09-18 23:11:36
【问题描述】:

使用 PyQt5,我正在尝试构建一个显示两个部分的 GUI 部分;一个在 QTableView(Overview 类)中给出概述,另一个在 QTabWidget(DetailledView 类)中显示概述的所选项目的详细信息。

现在,QTabWidget 中的数据分布在多个选项卡中,显示来自不同表的数据(它比下面的最小示例复杂得多)。由于这些行为类似于键:值对,因此我想垂直显示它们而不是水平显示它们。所以我有一个 InvertedTable 类可以做到这一点。

但是,QTabWidget 表的过滤并不完全有效:当我在概览表中选择一个项目时,QTabWidget 上的过滤器确实会更新,但只有在我单击不同的选项卡后才能看到。

我认为问题在于代理模型的分层:对于 InvertedTables,我有两层代理模型。一种是普通的 QSortFilterProxyModel,我用它来过滤要显示的正确数据子集。最重要的是,还有另一个代理模型(“FlippedProxyModel”,从 QSortFilterProxyModel 子类化)来反转数据。我使用第一个进行过滤,我认为这就是 QTableViews 没有立即更新的原因。 (当我在下面的代码中使用 SQLTables 而不是 InvertedTables 时,一切都很好 - 当然,除了方向。)

这大概也是过滤后有空列的原因吧……

我可以将翻转模型放在过滤器模型下方,但是我要过滤的列在过滤时已经是行,那么我该如何过滤呢? (另外,显示的表格可能会变大,所以使用过滤拳头似乎是个好主意。)

如何使用 QSortProxyFilterModels 过滤和垂直反转表格,以便显示它的 QTableView 在过滤后立即更新?

一个 MCVE 包含在下面:

#!/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()

【问题讨论】:

    标签: python pyqt pyqt5 qsortfilterproxymodel


    【解决方案1】:

    @s.nick 的solution 是强制的,就是把QTabWidget 的widget 去掉,然后加回来,处理大量数据会消耗大量资源。

    问题是代理需要 layoutAboutToBeChangedlayoutChanged 信号,但在 QSortProxyModel 的情况下它没有这样做,所以解决方案只是发出它:

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

    除了我看到您不必要地使用QSqlTableModelQSqlQueryModel 就足够了,在这种情况下QSqlTableModel 尺寸过大。

    还有一点需要改进的是FlippedProxyModel 应该继承自QIdentityProxyModel,不需要过滤或排序,所以QSortProxyModel 也是超维的。

    我用上面提到的改进修改了应用程序,结果代码如下:

    #!/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()
    

    【讨论】:

    • 我确实需要 QSqlTableModel,因为我的实际应用程序中的表需要用户与数据库交互(这些表需要是可编辑的)。但是您的其他见解非常有帮助。谢谢!
    • @CodingCat QSqlTableModel 是一个专门用于表的 QSqlQueryModel,你不应该在 QSqlTableModel 中插入查询,而是使用它的 select 和 filter 方法,或类似的方法。 :D
    • 但是如果查询比“从表中选择 *”更复杂怎么办?比如,左连接和汇总数字?我可以将所有这些表达为 SQL 查询,然后将其放入 QSqlTableModel。你能解释为什么这是有问题的吗? ETA:我已将此作为单独的问题:stackoverflow.com/questions/49752388/…
    【解决方案2】:

    有效。

    试试看:

    在DetailedView 类中添加了行公园

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-15
      • 2018-06-18
      • 2012-01-06
      • 1970-01-01
      • 2016-03-19
      • 2018-02-11
      • 2014-08-05
      相关资源
      最近更新 更多