Редактируемый QTableView сложного SQL-запроса
Как я могу сделать редактируемый QTableView, отображающий данные из сложного запроса SQLite?
Мне нужно заполнить QTableView данными из нескольких таблиц SQLite. Это должно быть редактируемым пользователем.
Поскольку запросы немного сложны (включая 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'")
Чтобы ответить на входящие вопросы:
Возможностьредактирования:
В этом примере единственным столбцом, который обязательно должен быть редактируемым (таким образом, чтобы изменения поступали в базу данных), был столбец Страна (и изменения там должны повлиять на все другие строки, которые совместно используют тот же контент, например, если вы изменяете От "Германия" до "Франция" для любого автомобиля VW, оба должны указать "Франция" в качестве страны).
Если вы знаете, как получить первый редактируемый файл, чтобы обновлять соответствующие столбцы в базе данных, это было бы очень удобно, но это не является обязательным требованием. (В моих реальных таблицах я использую такие "соединения столбцов" для не редактируемых полей.) В этом конкретном случае я бы ожидал, что "VW Polo" изменится на "Marco Polo", чтобы обновить "VW Golf" на "Marco Golf". ', так как столбец, используемый в столбце-соединении, - это anypany.pany, а не cars.company. (На самом деле, возможно, для соединения можно использовать cars.company, и в этом случае VW Golf останется неизменным. Но давайте предположим, что запрос приведен выше.)
Третий столбец предназначен в качестве примера вычисленного результата статистики, и они, как правило, предназначены только для чтения (их редактирование не имеет смысла).
Порядок столбцов:
Я был бы очень признателен за возможность выбора порядка отображения столбцов даже между объединенными таблицами (как я мог бы сделать с запросом).
2 ответа
Основываясь на отличном конкретном решении @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_())
Чтобы сделать редактируемые объединенные столбцы редактируемыми, потребовалось бы больше работы и другой формат для редактирования, но это должно работать с любыми столбцами, которые не содержат объединенные / вычисленные / агрегированные данные (например, "Страна" в этом примере).
QSqlTableModel
это класс, который наследует от QSqlQueryModel
так что можно сказать, что QSqlTableModel
является специализированным QSqlQueryModel
редактировать таблицу, чтобы она могла быть ограничена или увеличена.
Для этого особого случая я предлагаю QSqlQueryModel
Редактируемый, для этого я сделал следующие изменения:
Я включил флаг
Qt.ItemIsEditable
для второго столбца.Я переписал
setData()
способ обновить таблицу производителей.Я добавил столбец, который представляет компанию, это будет скрыто, но полезно получить строки, которые должны быть изменены в предыдущем изменении.
Я реализовал
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_())