#python #model-view-controller #qt #sqlite #pyside
#python #модель-представление-контроллер #qt #sqlite #pyside
Вопрос:
Я хочу создать достаточно простое приложение, которое использует виджет QTreeView для отображения иерархических данных из таблицы SQLite3 (flat), использует QDataWidgetMapper для заполнения некоторых полей lineedit, позволяет пользователю редактировать, что, в свою очередь, обновляет таблицу. Простой и базовое (для большинства!).
Я работал на основе того, что следующий процесс был бы лучшим способом сделать это:
- Подключитесь к базе данных
- Запрашивать данные
- Создайте и заполните пользовательскую QAbstractItemModel на основе данных (манипулируя ею с помощью
dict
для динамического создания узлов, родительских и дочерних элементов — для каждой записи dict генерируется «узел» с соответствующим родительским элементом) - Используйте QDataWidgetMapper для заполнения других виджетов
- Пользователь редактирует данные
- Обновлена QAbstractItemModel (QAIM)
- Затем необходимо выполнить запрос UPDATE, INSERT или любой другой запрос, используя новые значения в модели QAIM.
- Обновите QAIM и связанные с ним виджеты.
Я понимаю, что если бы я просто использовал QTableView или QListView, мне бы не понадобилась пользовательская модель, и я мог бы просто записывать данные обратно в базу данных. Процесс, который я описал выше, по-видимому, означает необходимость поддерживать два набора данных, т. е. таблицу SQLite и пользовательский QAIM, и гарантировать, что они оба поддерживаются в актуальном состоянии. Мне это кажется немного громоздким, и я уверен, что должен быть лучший способ сделать это, когда QTreeView берет свои данные прямо из таблицы SQLite — с очевидной необходимостью некоторых манипуляций для преобразования плоских данных в иерархические данные.
Мне, конечно, интересно, не совсем ли я неправильно понял взаимосвязь между моделями QAbstractItemModel и QSQL *, и я слишком усложняю это по незнанию?
Спасибо
Комментарии:
1. в каком формате хранятся ваши иерархические данные? обычно я использую заголовки и строки (например, счета-фактуры), но они используют модель для каждого.
2. для (относительной) простоты я в настоящее время использую только одну таблицу с ‘родительским’ столбцом. Это указывает, к какому узлу / записи дочернего элемента следует добавить.
3. что ж, не взглянув на конкретные данные, я не могу с уверенностью сказать, что вам следует хранить их другим способом, но шансы на то, что хранить родителей и дочерних элементов в одной таблице было бы хорошей идеей, невелики, у меня есть один случай в производстве, хотя, в любом случае, я думаю, вам придется придерживаться своей идеи синхронизировать вашу модель и SQL, Qt не обязательно начинать с идеальной поддержки редактирования таблиц SQL, я создал новую версию. Подкласс QSqlQueryModel чтобы иметь лучшую QSqlTableModel, возможно, вы могли бы сделать то же самое, но если вы собираетесь использовать только один раз, я не рекомендую вам это делать, будет гораздо больше работы.
Ответ №1:
Вам нужна прокси-модель, которая действует как мост между QSql*Model
и представлением. Для этого вам необходимо создать подкласс QAbstractProxyModel
. У вас должен быть согласованный способ нахождения отношений родитель-потомок в прокси-модели и сопоставления их с исходной моделью, так что для этого может потребоваться ведение некоторого учета в прокси-модели.
При подклассировании QAbstractProxyModel
вам необходимо переопределить, как минимум, эти методы:
- Количество строк
- Количество столбцов
- родительский
- Указатель
- данные
- mapToSource
- mapFromSource
Также имейте в виду, что QAbstractProxyModel
не происходит автоматического распространения сигналов через. Итак, чтобы представление было в курсе изменений в исходной модели (например, insert, delete, update), вам необходимо передать их в прокси-модели (при этом, конечно, обновляя свои сопоставления в прокси-модели).
Это потребует некоторой работы, но в итоге у вас будет более гибкая структура. И это устранит все, что вам нужно сделать для синхронизации базы данных и пользовательского QAbstractItemModel
.
Редактировать
Пользовательская прокси-модель, которая группирует элементы из плоской модели в соответствии с заданным столбцом:
import sys
from collections import namedtuple
import random
from PyQt4 import QtCore, QtGui
groupItem = namedtuple("groupItem",["name","children","index"])
rowItem = namedtuple("rowItem",["groupIndex","random"])
class GrouperProxyModel(QtGui.QAbstractProxyModel):
def __init__(self, parent=None):
super(GrouperProxyModel, self).__init__(parent)
self._rootItem = QtCore.QModelIndex()
self._groups = [] # list of groupItems
self._groupMap = {} # map of group names to group indexes
self._groupIndexes = [] # list of groupIndexes for locating group row
self._sourceRows = [] # map of source rows to group index
self._groupColumn = 0 # grouping column.
def setSourceModel(self, source, groupColumn=0):
super(GrouperProxyModel, self).setSourceModel(source)
# connect signals
self.sourceModel().columnsAboutToBeInserted.connect(self.columnsAboutToBeInserted.emit)
self.sourceModel().columnsInserted.connect(self.columnsInserted.emit)
self.sourceModel().columnsAboutToBeRemoved.connect(self.columnsAboutToBeRemoved.emit)
self.sourceModel().columnsRemoved.connect(self.columnsRemoved.emit)
self.sourceModel().rowsInserted.connect(self._rowsInserted)
self.sourceModel().rowsRemoved.connect(self._rowsRemoved)
self.sourceModel().dataChanged.connect(self._dataChanged)
# set grouping
self.groupBy(groupColumn)
def rowCount(self, parent):
if parent == self._rootItem:
# root level
return len(self._groups)
elif parent.internalPointer() == self._rootItem:
# children level
return len(self._groups[parent.row()].children)
else:
return 0
def columnCount(self, parent):
if self.sourceModel():
return self.sourceModel().columnCount(QtCore.QModelIndex())
else:
return 0
def index(self, row, column, parent):
if parent == self._rootItem:
# this is a group
return self.createIndex(row,column,self._rootItem)
elif parent.internalPointer() == self._rootItem:
return self.createIndex(row,column,self._groups[parent.row()].index)
else:
return QtCore.QModelIndex()
def parent(self, index):
parent = index.internalPointer()
if parent == self._rootItem:
return self._rootItem
else:
parentRow = self._getGroupRow(parent)
return self.createIndex(parentRow,0,self._rootItem)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
parent = index.internalPointer()
if parent == self._rootItem:
return self._groups[index.row()].name
else:
parentRow = self._getGroupRow(parent)
sourceRow = self._sourceRows.index(self._groups[parentRow].children[index.row()])
sourceIndex = self.createIndex(sourceRow, index.column(), 0)
return self.sourceModel().data(sourceIndex, role)
return None
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
return self.sourceModel().headerData(section, orientation, role)
def mapToSource(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parent = index.internalPointer()
if not parent.isValid():
return QtCore.QModelIndex()
elif parent == self._rootItem:
return QtCore.QModelIndex()
else:
rowItem_ = self._groups[parent.row()].children[index.row()]
sourceRow = self._sourceRows.index(rowItem_)
return self.createIndex(sourceRow, index.column(), QtCore.QModelIndex())
def mapFromSource(self, index):
rowItem_ = self._sourceRows[index.row()]
groupRow = self._getGroupRow(rowItem_.groupIndex)
itemRow = self._groups[groupRow].children.index(rowItem_)
return self.createIndex(itemRow,index.column(),self._groupIndexes[groupRow])
def _clearGroups(self):
self._groupMap = {}
self._groups = []
self._sourceRows = []
def groupBy(self,column=0):
self.beginResetModel()
self._clearGroups()
self._groupColumn = column
sourceModel = self.sourceModel()
for row in range(sourceModel.rowCount(QtCore.QModelIndex())):
groupName = sourceModel.data(self.createIndex(row,column,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
rowItem_ = rowItem(groupIndex,random.random())
self._groups[groupIndex.row()].children.append(rowItem_)
self._sourceRows.append(rowItem_)
self.endResetModel()
def _getGroupIndex(self, groupName):
""" return the index for a group denoted with name.
if there is no group with given name, create and then return"""
if groupName in self._groupMap:
return self._groupMap[groupName]
else:
groupRow = len(self._groupMap)
groupIndex = self.createIndex(groupRow,0,self._rootItem)
self._groupMap[groupName] = groupIndex
self._groups.append(groupItem(groupName,[],groupIndex))
self._groupIndexes.append(groupIndex)
self.layoutChanged.emit()
return groupIndex
def _getGroupRow(self, groupIndex):
for i,x in enumerate(self._groupIndexes):
if id(groupIndex)==id(x):
return i
return 0
def _rowsInserted(self, parent, start, end):
for row in range(start, end 1):
groupName = self.sourceModel().data(self.createIndex(row,self._groupColumn,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
self._getGroupRow(groupIndex)
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
rowItem_ = rowItem(groupIndex,random.random())
groupItem_.children.append(rowItem_)
self._sourceRows.insert(row, rowItem_)
self.layoutChanged.emit()
def _rowsRemoved(self, parent, start, end):
for row in range(start, end 1):
rowItem_ = self._sourceRows[start]
groupIndex = rowItem_.groupIndex
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
childrenRow = groupItem_.children.index(rowItem_)
groupItem_.children.pop(childrenRow)
self._sourceRows.pop(start)
if not len(groupItem_.children):
# remove the group
groupRow = self._getGroupRow(groupIndex)
groupName = self._groups[groupRow].name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()
def _dataChanged(self, topLeft, bottomRight):
topRow = topLeft.row()
bottomRow = bottomRight.row()
sourceModel = self.sourceModel()
# loop through all the changed data
for row in range(topRow,bottomRow 1):
oldGroupIndex = self._sourceRows[row].groupIndex
oldGroupItem = self._groups[self._getGroupRow(oldGroupIndex)]
newGroupName = sourceModel.data(self.createIndex(row,self._groupColumn,0),QtCore.Qt.DisplayRole)
if newGroupName != oldGroupItem.name:
# move to new group...
newGroupIndex = self._getGroupIndex(newGroupName)
newGroupItem = self._groups[self._getGroupRow(newGroupIndex)]
rowItem_ = self._sourceRows[row]
newGroupItem.children.append(rowItem_)
# delete from old group
oldGroupItem.children.remove(rowItem_)
if not len(oldGroupItem.children):
# remove the group
groupRow = self._getGroupRow(oldGroupItem.index)
groupName = oldGroupItem.name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()
Комментарии:
1. Спасибо за это. Я провел небольшое исследование и пришел к выводу, что QAbstractProxyModel может быть маршрутом. Хотя на данный момент это выглядит очень сложным для меня 🙂
2. @StevenLee: Прямо сейчас у меня нет рабочего примера. Извините. Но я работаю над пользовательским прокси, который группирует элементы из табличной модели (например, QSqlTableModel) на основе заданного столбца. Если хотите, я могу поделиться этим, когда закончу.
3. Это было бы очень ценно. Спасибо
4. да, это может быть хорошим способом получить то, что вы хотите, я никогда не использовал прокси для иерархических данных, но он гибок в том, что позволяет вам делать на самом деле.
5. @StevenLee: Я включил свой пользовательский прокси, который группирует элементы из плоской модели. Извините за задержку, у меня едва было время закончить это. Честно говоря, это может быть немного грубо, и, возможно, потребуется некоторая очистка, но пока все работает так, как ожидалось. Это может натолкнуть вас на некоторые идеи.