我正在尝试使用PyQt5构建一个显示两个部分的GUI部分。一个显示QTableView中的概述(类Overview),另一个显示QTabWidget中的所选项目的详细信息(类DetailledView)。
现在,QTabWidget中的数据分布在多个选项卡上,显示了来自不同表的数据(比下面的最小示例要复杂得多)。由于它们的行为类似于key:value对,因此我想垂直显示它们,而不是水平显示。因此,我有一个执行此操作的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()
最佳答案
@ s.nick的solution是强制性的,它消除了QTabWidget的小部件并添加回去,如果处理大量数据,这将消耗大量资源。
问题在于代理期望layoutAboutToBeChanged
和layoutChanged
信号,但是对于QSortProxyModel
则不这样做,因此解决方案只是发出它:
def filter(self, company):
for mytab in [self.tab1, self.tab2]:
mytab.model.layoutAboutToBeChanged.emit()
mytab.model.setFilterFixedString(company)
mytab.model.layoutChanged.emit()
除了我看到您不必要地使用
QSqlTableModel
,使用QSqlQueryModel
就足够了,在这种情况下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()
关于python - 在PyQt中分层代理模型,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/49734880/