复杂 SQL 查询的可编辑 QTableView
Editable QTableView of complex SQL query
如何制作可编辑的 QTableView 以显示来自复杂 SQLite 查询的数据?
我需要用来自多个 SQLite 表的数据填充 QTableView。这需要由用户编辑。
由于查询有点复杂(包括 JOIN 和 CASE WHEN 等),我通过 QSqlTableModel 和 QSqlQuery 来实现。然而,有人告诉我,这不是应该使用 QSqlTableModels 的方式。那么,有人可以告诉我如何通过正确的方式得到像这里显示的结果吗?
另外,虽然我的 QTableViews 是可编辑的,但结果似乎没有存储在 SQLite 数据库中。 (当我注释掉 fill_tables 时,我在重新启动 GUI 后仍然得到原始结果。将 EditStrategy 更改为 OnFieldChange 没有帮助。)那是因为我处理 QSqlTableModel 错误吗?
#!/usr/bin/python3
from PyQt5.QtSql import (QSqlDatabase, QSqlQuery, QSqlTableModel,
QSqlRelationalTableModel, QSqlRelation)
from PyQt5.QtWidgets import QTableView, QApplication
from PyQt5.Qt import QSortFilterProxyModel
import sys
db_file = "test.db"
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 (Company 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);")
class CarTable(QTableView):
def __init__(self):
super().__init__()
self.init_UI()
self.create_model()
def create_model(self):
query = """
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
"""
raw_model = QSqlTableModel()
q = QSqlQuery()
q.exec_(query)
self.check_error(q)
raw_model.setQuery(q)
self.model = QSortFilterProxyModel()
self.model.setSourceModel(raw_model)
self.setModel(self.model)
# filtering:
self.model.setFilterKeyColumn(0)
self.model.setFilterFixedString('VW')
def init_UI(self):
self.resize(500,300)
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
exit(1)
def main():
mydb = create_connection(db_file)
if not mydb:
sys.exit(-1)
fill_tables()
app = QApplication(sys.argv)
ex = CarTable()
ex.show()
result = app.exec_()
if (mydb.open()):
mydb.close()
sys.exit(result)
if __name__ == '__main__':
main()
我试过使用 QSqlRelationalTableModel 代替,但我无法完成同样复杂的查询,而且它也不会保存更改,就像上面的代码一样。这是我在尝试中得到的结果:
def create_model_alternative(self):
self.model = QSqlRelationalTableModel()
self.model.setTable("Cars")
self.model.setRelation(0, QSqlRelation("Manufacturers", "Company",
"Company, Country"))
self.setModel(self.model)
self.model.select()
# filtering:
self.model.setFilter("cars.Company = 'VW'")
回答收到的问题:
可编辑性:
在此示例中,唯一绝对需要可编辑的列(以这种方式将更改到达数据库)是国家/地区列(并且应该在此处进行更改影响共享相同内容的所有其他行;例如,如果您将任何一辆大众汽车的 'Germany' 更改为 'France',则两者都应将 'France' 列为国家/地区)。
如果您知道让第一个列也可编辑的方法,以便更新数据库中的相应列,那将是非常好的,但这不是必需的。 (在我的真实表格中,我将这样的 'column-joins' 用于不可编辑的字段。)
在这种特定情况下,我希望将 'VW Polo' 更改为 'Marco Polo' 也会将 'VW Golf' 更新为 'Marco Golf',因为列连接中使用的列是 manufacturers.company 而不是 cars.company。 (实际上,人们可能会使用 cars.company 作为连接,在这种情况下 'VW Golf' 将保持不变。但让我们假设查询如上所示。)
第三列是计算统计结果的示例,这些通常仅供阅读(编辑它们没有意义)。
列顺序:
我会高度感谢能够选择列的显示顺序,即使是跨连接表(就像我可以使用查询一样)。
QSqlTableModel
是继承自QSqlQueryModel
的class,所以可以说QSqlTableModel
是专门QSqlQueryModel
编辑table,因此可以限制或加大尺寸。
对于这种特殊情况,我建议使用 QSqlQueryModel
Editable,为此我做了以下更改:
我已经为第二列启用了标志 Qt.ItemIsEditable
。
我已经覆盖了 setData()
方法来更新制造商 table。
我添加了一个代表公司的列,这将被隐藏但是对于获取之前更改中必须更改的行很有用。
我已经实现了setFilter()
制作过滤器的方法。
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView
db_file = "test.db"
def create_connection(file_path):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(file_path)
if not db.open():
print("Cannot establish a database connection to {}!".format(file_path))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Company 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);")
class SqlQueryModel(QSqlQueryModel):
def flags(self, index):
fl = QSqlQueryModel.flags(self, index)
if index.column() == 1:
fl |= Qt.ItemIsEditable
return fl
def setData(self, index, value, role=Qt.EditRole):
if index.column() == 1:
company = self.index(index.row(), 2).data()
q = QSqlQuery("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'".format(value, company))
result = q.exec_()
if result:
self.query().exec_()
else:
print(self.query().lastError().text())
return result
return QSqlQueryModel.setData(self, index, value, role)
def setFilter(self, filter):
text = (self.query().lastQuery() + " WHERE " + filter)
self.setQuery(text)
query = '''
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
cars.company,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
'''
if __name__ == '__main__':
app = QApplication(sys.argv)
if not create_connection(db_file):
sys.exit(-1)
fill_tables()
view = QTableView()
model = SqlQueryModel()
q = QSqlQuery(query)
model.setQuery(q)
model.setFilter("cars.Company = 'VW'")
view.setModel(model)
view.hideColumn(2)
view.show()
sys.exit(app.exec_())
基于@eyllanesc 的出色具体解决方案,我制作了 QSqlQueryModel 的通用版本,可以在其中指定哪些列应该是可编辑的。它可能需要根据其他人的查询进行调整,但我希望它对遇到类似问题的人有所帮助:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView
db_file = "test.db"
def create_connection(file_path):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(file_path)
if not db.open():
print("Cannot establish a database connection to {}!".format(file_path))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Company 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);")
class SqlQueryModel_editable(QSqlQueryModel):
"""a subclass of QSqlQueryModel where individual columns can be defined as editable
"""
def __init__(self, editables):
"""editables should be a dict of format:
{INT editable_column_nr : (STR update query to be performed when changes are made on this column
INT model's column number for the filter-column (used in the where-clause),
)}
"""
super().__init__()
self.editables = editables
def flags(self, index):
fl = QSqlQueryModel.flags(self, index)
if index.column() in self.editables:
fl |= Qt.ItemIsEditable
return fl
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
mycolumn = index.column()
if mycolumn in self.editables:
(query, filter_col) = self.editables[mycolumn]
filter_value = self.index(index.row(), filter_col).data()
q = QSqlQuery(query.format(value, filter_value))
result = q.exec_()
if result:
self.query().exec_()
else:
print(self.query().lastError().text())
return result
return QSqlQueryModel.setData(self, index, value, role)
def setFilter(self, myfilter):
text = (self.query().lastQuery() + " WHERE " + myfilter)
self.setQuery(text)
if __name__ == '__main__':
app = QApplication(sys.argv)
if not create_connection(db_file):
sys.exit(-1)
fill_tables()
view = QTableView()
editables = {1 : ("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'", 2)}
model = SqlQueryModel_editable(editables)
query = '''
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
cars.company,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
'''
q = QSqlQuery(query)
model.setQuery(q)
model.setFilter("cars.Company = 'VW'")
view.setModel(model)
view.hideColumn(2)
view.show()
sys.exit(app.exec_())
要使连接的列可编辑,需要更多工作和不同的可编辑格式,但这应该适用于不包含 joined/calculated/aggregated 数据的任何列(如本例中的 'Country') .
如何制作可编辑的 QTableView 以显示来自复杂 SQLite 查询的数据?
我需要用来自多个 SQLite 表的数据填充 QTableView。这需要由用户编辑。
由于查询有点复杂(包括 JOIN 和 CASE WHEN 等),我通过 QSqlTableModel 和 QSqlQuery 来实现。然而,有人告诉我,这不是应该使用 QSqlTableModels 的方式。那么,有人可以告诉我如何通过正确的方式得到像这里显示的结果吗?
另外,虽然我的 QTableViews 是可编辑的,但结果似乎没有存储在 SQLite 数据库中。 (当我注释掉 fill_tables 时,我在重新启动 GUI 后仍然得到原始结果。将 EditStrategy 更改为 OnFieldChange 没有帮助。)那是因为我处理 QSqlTableModel 错误吗?
#!/usr/bin/python3
from PyQt5.QtSql import (QSqlDatabase, QSqlQuery, QSqlTableModel,
QSqlRelationalTableModel, QSqlRelation)
from PyQt5.QtWidgets import QTableView, QApplication
from PyQt5.Qt import QSortFilterProxyModel
import sys
db_file = "test.db"
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 (Company 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);")
class CarTable(QTableView):
def __init__(self):
super().__init__()
self.init_UI()
self.create_model()
def create_model(self):
query = """
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
"""
raw_model = QSqlTableModel()
q = QSqlQuery()
q.exec_(query)
self.check_error(q)
raw_model.setQuery(q)
self.model = QSortFilterProxyModel()
self.model.setSourceModel(raw_model)
self.setModel(self.model)
# filtering:
self.model.setFilterKeyColumn(0)
self.model.setFilterFixedString('VW')
def init_UI(self):
self.resize(500,300)
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
exit(1)
def main():
mydb = create_connection(db_file)
if not mydb:
sys.exit(-1)
fill_tables()
app = QApplication(sys.argv)
ex = CarTable()
ex.show()
result = app.exec_()
if (mydb.open()):
mydb.close()
sys.exit(result)
if __name__ == '__main__':
main()
我试过使用 QSqlRelationalTableModel 代替,但我无法完成同样复杂的查询,而且它也不会保存更改,就像上面的代码一样。这是我在尝试中得到的结果:
def create_model_alternative(self):
self.model = QSqlRelationalTableModel()
self.model.setTable("Cars")
self.model.setRelation(0, QSqlRelation("Manufacturers", "Company",
"Company, Country"))
self.setModel(self.model)
self.model.select()
# filtering:
self.model.setFilter("cars.Company = 'VW'")
回答收到的问题:
可编辑性:
在此示例中,唯一绝对需要可编辑的列(以这种方式将更改到达数据库)是国家/地区列(并且应该在此处进行更改影响共享相同内容的所有其他行;例如,如果您将任何一辆大众汽车的 'Germany' 更改为 'France',则两者都应将 'France' 列为国家/地区)。
如果您知道让第一个列也可编辑的方法,以便更新数据库中的相应列,那将是非常好的,但这不是必需的。 (在我的真实表格中,我将这样的 'column-joins' 用于不可编辑的字段。) 在这种特定情况下,我希望将 'VW Polo' 更改为 'Marco Polo' 也会将 'VW Golf' 更新为 'Marco Golf',因为列连接中使用的列是 manufacturers.company 而不是 cars.company。 (实际上,人们可能会使用 cars.company 作为连接,在这种情况下 'VW Golf' 将保持不变。但让我们假设查询如上所示。)
第三列是计算统计结果的示例,这些通常仅供阅读(编辑它们没有意义)。
列顺序:
我会高度感谢能够选择列的显示顺序,即使是跨连接表(就像我可以使用查询一样)。
QSqlTableModel
是继承自QSqlQueryModel
的class,所以可以说QSqlTableModel
是专门QSqlQueryModel
编辑table,因此可以限制或加大尺寸。
对于这种特殊情况,我建议使用 QSqlQueryModel
Editable,为此我做了以下更改:
我已经为第二列启用了标志
Qt.ItemIsEditable
。我已经覆盖了
setData()
方法来更新制造商 table。我添加了一个代表公司的列,这将被隐藏但是对于获取之前更改中必须更改的行很有用。
我已经实现了
setFilter()
制作过滤器的方法。
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView
db_file = "test.db"
def create_connection(file_path):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(file_path)
if not db.open():
print("Cannot establish a database connection to {}!".format(file_path))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Company 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);")
class SqlQueryModel(QSqlQueryModel):
def flags(self, index):
fl = QSqlQueryModel.flags(self, index)
if index.column() == 1:
fl |= Qt.ItemIsEditable
return fl
def setData(self, index, value, role=Qt.EditRole):
if index.column() == 1:
company = self.index(index.row(), 2).data()
q = QSqlQuery("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'".format(value, company))
result = q.exec_()
if result:
self.query().exec_()
else:
print(self.query().lastError().text())
return result
return QSqlQueryModel.setData(self, index, value, role)
def setFilter(self, filter):
text = (self.query().lastQuery() + " WHERE " + filter)
self.setQuery(text)
query = '''
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
cars.company,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
'''
if __name__ == '__main__':
app = QApplication(sys.argv)
if not create_connection(db_file):
sys.exit(-1)
fill_tables()
view = QTableView()
model = SqlQueryModel()
q = QSqlQuery(query)
model.setQuery(q)
model.setFilter("cars.Company = 'VW'")
view.setModel(model)
view.hideColumn(2)
view.show()
sys.exit(app.exec_())
基于@eyllanesc 的出色具体解决方案,我制作了 QSqlQueryModel 的通用版本,可以在其中指定哪些列应该是可编辑的。它可能需要根据其他人的查询进行调整,但我希望它对遇到类似问题的人有所帮助:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView
db_file = "test.db"
def create_connection(file_path):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(file_path)
if not db.open():
print("Cannot establish a database connection to {}!".format(file_path))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (Company 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);")
class SqlQueryModel_editable(QSqlQueryModel):
"""a subclass of QSqlQueryModel where individual columns can be defined as editable
"""
def __init__(self, editables):
"""editables should be a dict of format:
{INT editable_column_nr : (STR update query to be performed when changes are made on this column
INT model's column number for the filter-column (used in the where-clause),
)}
"""
super().__init__()
self.editables = editables
def flags(self, index):
fl = QSqlQueryModel.flags(self, index)
if index.column() in self.editables:
fl |= Qt.ItemIsEditable
return fl
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
mycolumn = index.column()
if mycolumn in self.editables:
(query, filter_col) = self.editables[mycolumn]
filter_value = self.index(index.row(), filter_col).data()
q = QSqlQuery(query.format(value, filter_value))
result = q.exec_()
if result:
self.query().exec_()
else:
print(self.query().lastError().text())
return result
return QSqlQueryModel.setData(self, index, value, role)
def setFilter(self, myfilter):
text = (self.query().lastQuery() + " WHERE " + myfilter)
self.setQuery(text)
if __name__ == '__main__':
app = QApplication(sys.argv)
if not create_connection(db_file):
sys.exit(-1)
fill_tables()
view = QTableView()
editables = {1 : ("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'", 2)}
model = SqlQueryModel_editable(editables)
query = '''
SELECT (comp.company || " " || cars.model) as Car,
comp.Country,
cars.company,
(CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
from manufacturers comp left join cars
on comp.company = cars.company
'''
q = QSqlQuery(query)
model.setQuery(q)
model.setFilter("cars.Company = 'VW'")
view.setModel(model)
view.hideColumn(2)
view.show()
sys.exit(app.exec_())
要使连接的列可编辑,需要更多工作和不同的可编辑格式,但这应该适用于不包含 joined/calculated/aggregated 数据的任何列(如本例中的 'Country') .