Alerting QDataWidgetMapper to changes when using a custom Model & Delegate - python

I'm using a subclassed QAbstractTableModel with dataclasses as items. Each dataclass contains a field "field1" with a list, which I'd like to display in a listview and have it automatically change whenever I edit or add an item in the listview.
To do that I set a custom delegate to the QDataWidgetMapper which will retrieve and set the values from that dataclass. This works the way I want it to.
My problem is that I want to add additional items to that listview with the press of a button and have the QDataWidgetMapper add them automatically to the model.
This is what I have so far:
import sys
import dataclasses
from typing import List, Any
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
#dataclasses.dataclass()
class StorageItem:
field1: List[str] = dataclasses.field(default_factory=list)
class StorageModel(QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
test = StorageItem()
test.field1 = ['Item °1', 'Item °2']
self._data: List[StorageItem] = [test]
def data(self, index: QModelIndex, role: int = ...) -> Any:
if not index.isValid():
return
item = self._data[index.row()]
col = index.column()
if role in {Qt.DisplayRole, Qt.EditRole}:
if col == 0:
return item.field1
else:
return None
def setData(self, index: QModelIndex, value, role: int = ...) -> bool:
if not index.isValid() or role != Qt.EditRole:
return False
item = self._data[index.row()]
col = index.column()
if col == 0:
item.field1 = value
self.dataChanged.emit(index, index)
print(self._data)
return True
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemFlags(
Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
)
def rowCount(self, parent=None) -> int:
return len(self._data)
def columnCount(self, parent=None) -> int:
return len(dataclasses.fields(StorageItem))
class TestDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def setEditorData(self, editor: QWidget, index: QModelIndex) -> None:
if isinstance(editor, QListView):
data = index.model().data(index, Qt.DisplayRole)
editor.model().setStringList(data)
else:
super().setEditorData(editor, index)
def setModelData(
self, editor: QWidget,
model: QAbstractItemModel,
index: QModelIndex
) -> None:
if isinstance(editor, QListView):
data = editor.model().stringList()
model.setData(index, data, Qt.EditRole)
else:
super().setModelData(editor, model, index)
class CustomListView(QListView):
item_added = pyqtSignal(name='itemAdded')
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
def add_item(self, item: str):
str_list = self.model().stringList()
str_list.append(item)
self.model().setStringList(str_list)
self.item_added.emit()
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
cent_widget = QWidget()
self.setCentralWidget(cent_widget)
# Vertical Layout
v_layout = QVBoxLayout()
v_layout.setContentsMargins(10, 10, 10, 10)
# Listview
self.listview = CustomListView()
v_layout.addWidget(self.listview)
# Button
self.btn = QPushButton('Add')
self.btn.clicked.connect(lambda: self.listview.add_item('New Item'))
v_layout.addWidget(self.btn)
cent_widget.setLayout(v_layout)
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setItemDelegate(TestDelegate())
self.mapper.setSubmitPolicy(QDataWidgetMapper.AutoSubmit)
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0)
self.mapper.toFirst()
self.listview.itemAdded.connect(self.mapper.submit)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
if __name__ == '__main__':
main()
Currently, I'm using the signal itemAdded from inside the custom ListView to manually submit the QDataWidgetMapper.
Is there a way to do this within CustomListView, without using a custom signal?
Somehow the delegate knows when data in the listview has been edited. How can I trigger that same mechanism when new items are added?

TL; DR; It can not.
The submitPolicy QDataWidgetMapper::AutoSubmit indicates that the model will be updated when focus is lost. The model is also updated when the commitData or closeEditor signal of the delegate is invoked, which happens by default when some specific keys are pressed.
A better implementation would be to create a signal that is emitted every time a change is made in the QListView model and connect it to submit, not just the method of adding elements. Also it is better to use a custom qproperty.
class CustomListView(QListView):
items_changed = pyqtSignal(name="itemsChanged")
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
self.model().rowsInserted.connect(self.items_changed)
self.model().rowsRemoved.connect(self.items_changed)
self.model().dataChanged.connect(self.items_changed)
self.model().layoutChanged.connect(self.items_changed)
def add_item(self, item: str):
self.items += [item]
#pyqtProperty(list, notify=items_changed)
def items(self):
return self.model().stringList()
#items.setter
def items(self, data):
if len(data) == len(self.items) and all(
x == y for x, y in zip(data, self.items)
):
return
self.model().setStringList(data)
self.items_changed.emit()
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0, b"items")
self.mapper.toFirst()
self.listview.items_changed.connect(self.mapper.submit)

Related

QListView Complexe Delegate editor focus to parent

I'm trying to make a QListView where each rows are represented has a complex widget.
I want to have a QLabel and a QTableView representing some data.
All good so far.
The problem is that when i click on the QTableView (which i disabled the focus policy), it doesn't select the row of the QListView.
But it's working when i click on the QLabel (which is also disabled focus policy)
Is there anything i do wrong ? My change_line method does get called when clicking on the "This is line" label but not on the table bellow it :(
I've tried to play with the FocusPolicy and the setFocusProxy in the editor, but so far can't make it work.
Thanks for the help
import sys
from typing import Any, Dict
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QAbstractTableModel, QSize
from PyQt5.QtMultimedia import QMediaMetaData
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QListView, QWidget, QStyledItemDelegate, \
QTableView, QHeaderView, QAbstractScrollArea, QLabel
class TableModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self._data_list = []
#property
def data_list(self):
return self._data_list
#data_list.setter
def data_list(self, data_list):
self.beginResetModel()
self._data_list = data_list
self.endResetModel()
def headerData(self, section: int, orientation: QMediaMetaData.Orientation, role: int = ...) -> Any:
if role == Qt.DisplayRole:
if section == 0:
return "header"
def data(self, index: QModelIndex, role: int = ...) -> Any:
if index.column() == 0:
if role == Qt.DisplayRole:
return self.data_list[index.row()]
def rowCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return len(self.data_list)
def columnCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return 1
class ListModel(QAbstractListModel):
def __init__(self):
super().__init__()
def rowCount(self, parent=QModelIndex(), *args, **kwargs) -> int:
return 2
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if index.row() == 0:
if role == Qt.DisplayRole:
return ["Hello", "World!"]
elif index.row() == 1:
if role == Qt.DisplayRole:
return ["One", "Two", "Three!"]
class ItemEditor(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.verticalLayout = QVBoxLayout(self)
self.line_label = QLabel("This is a line", self)
self.verticalLayout.addWidget(self.line_label)
self.table_view = QTableView(self)
self.table_view.setFocusPolicy(QtCore.Qt.NoFocus)
self.table_view.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.table_view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.table_view.horizontalHeader().setVisible(False)
self.table_view.verticalHeader().setVisible(False)
self.verticalLayout.addWidget(self.table_view)
class StyledItemDelegate(QStyledItemDelegate):
def __init__(self, parent):
super(StyledItemDelegate, self).__init__(parent)
self.editors: Dict[int, QTableView] = {}
def sizeHint(self, option: 'QStyleOptionViewItem', index: QModelIndex) -> QSize:
if index.row() in self.editors.keys():
return QSize(min(self.parent().width(), self.editors[index.row()].sizeHint().width()),
self.editors[index.row()].sizeHint().height())
else:
return super(StyledItemDelegate, self).sizeHint(option, index)
def createEditor(self, parent, option, index):
editor = ItemEditor(parent)
editor.setFocusProxy(parent)
editor.table_view.setModel(TableModel())
editor.table_view.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
header = editor.table_view.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
return editor
def setEditorData(self, editor: QTableView, index):
editor.table_view.model().data_list = index.data()
self.editors[index.row()] = editor
def change_line(index: QModelIndex):
# FIXME Only working when clicking on the "editor" widget or the QLabel, not on the Table
print(f"Changing to line {index.row()}")
if __name__ == "__main__":
app = QApplication([])
main_window = QMainWindow()
centralwidget = QWidget(main_window)
verticalLayout = QVBoxLayout(centralwidget)
listView = QListView(centralwidget)
listView.clicked.connect(change_line)
verticalLayout.addWidget(listView)
main_window.setCentralWidget(centralwidget)
listModel = ListModel()
listView.setModel(listModel)
delegate = StyledItemDelegate(listView)
listView.setItemDelegate(delegate)
for i in range(listModel.rowCount()):
index = listModel.index(i, 0)
listView.openPersistentEditor(index)
main_window.resize(600,400)
main_window.show()
sys.exit(app.exec_())
Since no mouse interaction is required, a possibility is to make the table "transparent" to mouse events:
self.table_view.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
Note that by default the editor doesn't change the size hint of the cell but the other way around, so you should emit sizeHintChanged when creating the editor, and you should add the editor to the dictionary in createEditor, not in setEditorData.
self.editors[index.row()] = editor
QtCore.QTimer.singleShot(0, lambda: self.sizeHintChanged.emit(index))
return editor

Why does my QStyleItemDelegate pop up in a new window instead of in my TableView?

I'm trying to create a pretty simple app with Pyside2 that displays some data in a table that the user can edit. I want the cells of the table to autocomplete based on other values in the table, so I implemented a custom delegate following this tutorial. It was pretty straightforward and the delegate works as expected except for some reason the delegate pops up in a new window instead of being attached to the cell in the table, illustrated by this example (the custom delegate is defined at the end):
import pandas as pd
from PySide2 import QtWidgets, QtCore
from PySide2.QtCore import Qt
class TableView(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setObjectName(u"TableView")
self.centralwidget = QtWidgets.QWidget(self)
self.centralwidget.setObjectName(u"centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.tableView = QtWidgets.QTableView(self.frame)
self.tableView.setObjectName(u"tableView")
delegate = QAutoCompleteDelegate(self.tableView)
self.tableView.setItemDelegate(delegate)
self.tableModel = TableModel(self.tableView)
self.tableView.setModel(self.tableModel)
self.verticalLayout_2.addWidget(self.tableView)
self.verticalLayout.addWidget(self.frame)
self.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(self)
self.statusbar.setObjectName(u"statusbar")
self.setStatusBar(self.statusbar)
QtCore.QMetaObject.connectSlotsByName(self)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super(TableModel, self).__init__(parent=parent)
self._data = pd.DataFrame([["A0", "B0", "C0"], ["A1", "B1", "C1"], ["A2", "B2", "C2"]], columns=["A", "B", "C"])
def data(self, index, role):
# Should only ever refer to the data by iloc in this method, unless you
# go specifically fetch the correct loc based on the iloc
row = index.row()
col = index.column()
if role == Qt.DisplayRole or role == Qt.EditRole:
return self._data.iloc[row, col]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data.columns)
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
def unique_vals(self, index):
""" Identify the values to include in an autocompletion delegate for the given index """
column = index.column()
col_name = self._data.columns[column]
return list(self._data[col_name].unique())
def setData(self, index, value, role):
if role != Qt.EditRole:
return False
row = self._data.index[index.row()]
column = self._data.columns[index.column()]
self._data.loc[row, column] = value
self.dataChanged.emit(index, index)
return True
def flags(self, index):
flags = super(self.__class__, self).flags(index)
flags |= Qt.ItemIsEditable
flags |= Qt.ItemIsSelectable
flags |= Qt.ItemIsEnabled
flags |= Qt.ItemIsDragEnabled
flags |= Qt.ItemIsDropEnabled
return flags
class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtWidgets.QWidget:
le = QtWidgets.QLineEdit()
test = ["option1", "option2", "option3"]
complete = QtWidgets.QCompleter(test)
le.setCompleter(complete)
return le
def setEditorData(self, editor:QtWidgets.QLineEdit, index:QtCore.QModelIndex):
val = index.model().data(index, Qt.EditRole)
options = index.model().unique_vals(index)
editor.setText(val)
completer = QtWidgets.QCompleter(options)
editor.setCompleter(completer)
def updateEditorGeometry(self, editor: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
editor.setGeometry(option.rect)
app = QtWidgets.QApplication()
tv = TableView()
tv.show()
app.exec_()
Image showing table behavior when a cell is active
From everything I've read, the updateEditorGeometry method on the custom delegate should anchor it to the active cell, but something clearly isn't working as expected in my case. And I am still learning Qt so I can't claim to understand what it is doing. Is there anyone out there who might be able to explain what is going on here? Much appreciated.
OS: Ubuntu 20.04
Python 3.8.5
PySide2 5.15.0
Qt 5.12.9
Any widget that does not have a parent will be a window and since the QLineEdit has no parent then you observe the behavior that the OP indicates, for that reason the createEditor method has "parent" as an argument.
On the other hand, it is better to create a custom QLineEdit that easily implements the handling of adding suggestions to the QCompleter through a model.
class AutoCompleteLineEdit(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self._completer_model = QtGui.QStandardItemModel()
completer = QtWidgets.QCompleter()
completer.setModel(self._completer_model)
self.setCompleter(completer)
#property
def suggestions(self):
return [
self._completer_model.item(i).text()
for i in range(self._completer_model.rowCount())
]
#suggestions.setter
def suggestions(self, suggestions):
self._completer_model.clear()
for suggestion in suggestions:
item = QtGui.QStandardItem(suggestion)
self._completer_model.appendRow(item)
class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(
self,
parent: QtWidgets.QWidget,
option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex,
) -> QtWidgets.QWidget:
le = AutoCompleteLineEdit(parent)
return le
def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
val = index.model().data(index, Qt.EditRole)
options = index.model().unique_vals(index)
editor.setText(val)
editor.suggestions = options

PyQt QListView checkbox click toggle?

I am trying to make a QListView that shows a list of items and whether they're enabled or not. The code below does this, but I also want to be able to toggle the item's enabled state when the user clicks on the checkbox (not just anywhere on the list item, but specifically on the checkbox), how can I do that?
import sys
from dataclasses import dataclass
from typing import Dict, List, Union
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
#dataclass
class Filter:
expr: str
enabled: bool
class Store:
def __init__(self):
super().__init__()
self.filter_viewer: Union["FilterViewer", None] = None
self.filters: List[Filter] = []
def update(self):
if self.filter_viewer is not None:
self.filter_viewer.list_model.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
def add_filter(self, expr: str, enabled=True):
filt = Filter(expr=expr, enabled=enabled)
self.filters.append(filt)
self.update()
def remove_filter(self, index: int):
self.filters.pop(index)
self.update()
def toggle_filter(self, index: int):
self.filters[index].enabled = not self.filters[index].enabled
self.update()
class FilterViewer(QtWidgets.QWidget):
def __init__(self, pgdf: Store):
super().__init__()
pgdf.filter_viewer = self
self.pgdf = pgdf
self.list_view = self.ListView()
self.list_model = self.ListModel(pgdf)
self.list_view.setModel(self.list_model)
self.text_input = QtWidgets.QLineEdit()
self.submit_button = QtWidgets.QPushButton("Add Filter")
self.submit_button.clicked.connect(self.add_filter)
self.text_input.returnPressed.connect(self.add_filter)
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.list_view)
self.layout.addWidget(self.text_input)
self.layout.addWidget(self.submit_button)
self.setLayout(self.layout)
def add_filter(self):
expr = self.text_input.text()
self.text_input.setText("")
self.pgdf.add_filter(expr=expr)
print(self.pgdf.filters)
class ListView(QtWidgets.QListView):
pass
class ListModel(QtCore.QAbstractListModel):
def __init__(self, pgdf: Store):
super().__init__()
self.pgdf = pgdf
def data(self, index: QtCore.QModelIndex, role: int):
row = index.row()
if role == Qt.DisplayRole:
filt = self.pgdf.filters[row]
return filt.expr
if role == Qt.CheckStateRole:
filt = self.pgdf.filters[row]
if filt.enabled:
return Qt.Checked
else:
return Qt.Unchecked
def rowCount(self, parent):
return len(self.pgdf.filters)
def flags(self, index):
return (QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsUserCheckable)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
pgdf = Store()
pgdf.add_filter('Generation > 3', enabled=True)
pgdf.add_filter('Size > 50', enabled=False)
fv = FilterViewer(pgdf)
fv.show()
app.exec_()
The state of the QListView checkboxes is determined by the model, so to change the model information you must override the setData() method:
class ListModel(QtCore.QAbstractListModel):
def __init__(self, pgdf: Store):
super().__init__()
self.pgdf = pgdf
def data(self, index: QtCore.QModelIndex, role: int):
row = index.row()
if role == Qt.DisplayRole:
filt = self.pgdf.filters[row]
return filt.expr
if role == Qt.CheckStateRole:
filt = self.pgdf.filters[row]
if filt.enabled:
return Qt.Checked
else:
return Qt.Unchecked
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
row = index.row()
if role == Qt.CheckStateRole:
filt = self.pgdf.filters[row]
filt.enabled = bool(value)
self.dataChanged.emit(index, index, (role,))
return True
return False
def rowCount(self, parent):
return len(self.pgdf.filters)
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable

Calling a model method through a subclass

What I'm trying to do, is to create a subclass of QTableView that will accept a standardized model (which is the result of a mysql query), and populate it. With it, I'd like to define a custom context menu which allows me to add or remove a row. This approach allows me to define the model and subclass once, and then call them both to populate them with a number of different queries without duplicating code.
The add_row and remove_row QActions would essentially call the model methods insertRows and removeRows.
Here's a functioning example (apart from the mariadb module which is separate):
from PySide2 import QtWidgets, QtCore, Qt, QtGui
import maria_db
import numpy
app = QtWidgets.QApplication([])
#Define model for tablePersonnel
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, db,query,tableName,id):
super(TableModel, self).__init__()
self.db=db
self.query=query
self.tableName=tableName
self.id=id
self.pullData()
def pullData(self):
self.datasheet=self.db.RunQuery(self.query)
self.rows = numpy.array(self.datasheet.fetchall())
self.headers = numpy.array(self.datasheet.column_names)
self.idIndex=int(numpy.where(self.headers==self.id)[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return str(self.rows[index.row()][index.column()])
def rowCount(self, parent):
return len(self.rows)
def columnCount(self, parent):
return len(self.headers)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
def flags(self,index):
if index.column()!=self.idIndex:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
else:
return QtCore.Qt.ItemIsSelectable
def setData(self, index,value,role=QtCore.Qt.EditRole):
if index.isValid():
sel_id=self.rows[index.row()][0]
sel_column=self.headers[index.column()]
if self.db.UpdateCell(self.tableName,sel_id,self.id,sel_column,value)==True:
self.rows[self.idFinder(self.rows,sel_id)][1]=value
self.dataChanged.emit(index,index)
return True
def insertRows(self):
print("inserting row!!!")
def removeRows(self):
pass
#find index of selected ID
def idFinder(self,data, search):
for i in range(len(data)):
for j in range(len(data[i])):
if data[i][j] == search:
return i
else:
return False
class EditTable(QtWidgets.QTableView):
def __init__(self):
super(EditTable, self).__init__()
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.context_menu)
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(lambda: self.personnelTableModel.insertRows())
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(lambda: self.personnelTableModel.removeRows())
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
widget.setLayout(layout)
button = QtWidgets.QPushButton("Push me")
self.db = maria_db.ConnectDb("localhost", "root", "1234", "test")
self.personnelTableModel = TableModel(self.db,"SELECT * FROM personnel","personnel","personnel_id")
table = EditTable()
table.setModel(self.personnelTableModel)
layout.addWidget(table)
layout.addWidget(button)
self.setCentralWidget(widget)
window = MainWindow()
window.show()
app.exec_()
So my problem is that when I call add_row.triggered.connect(lambda: self.personnelTableModel.insertRows()), I get a 'EditTable' object has no attribute 'personnelTableModel' which essentially means that there is no method insertRows in self. How can I refer to the model assigned to the object that is constructed using the subclass of the QTableView EditTable, through the subclass itself? Am I going about this the wrong way?
What is personnelTableModel for EditTable? the model, and how can I access the model from the view? Using the model() method. On the other hand, a lambda method is not necessary.
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(self.model().insertRows)
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(self.model().removeRows)
cursor=QtGui.QCursor()
context.exec_(cursor.pos())

Creating a model/view interface with sliders using PyQt

I have a GUI that consists of a number of sliders, and instead of updating the sliders manually when the underlying data changes, I'd like to store the data in a subclass of QAbstractListModel and have the slider positions update automatically. My subclass looks like this:
from PyQt4 import QtCore
class myDataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(myDataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
How can I connect this model to the sliders in my GUI so that when the data in the model is changed, the sliders change, and vice versa?
Edit: Here is a mockup of the basic interface I have been working on:
Edit: I still haven't been able to get this to work. Here is my model class:
class dataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(dataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
Here is the Delegate class:
class sliderDelegate(QtGui.QItemDelegate):
'''
classdocs
'''
def __init__(self, parent=None):
'''
Constructor
'''
super(sliderDelegate, self).__init__(parent)
def setEditorData(self, editor, index):
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)
And here is the setup code:
self._model = dataModel([0 for i in xrange(20)])
self._parameterMapper = QtGui.QDataWidgetMapper(mainWindowInstance)
self._parameterMapper.setModel(self._model)
self._parameterMapper.setItemDelegate(sliderDelegate(mainWindowInstance))
self._parameterMapper.addMapping(self._mainWindowInstance.ui.mySlider, 0)
self._parameterMapper.toFirst()
Unfortunately I get the following error when toFirst() is called:
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
AttributeError: 'NoneType' object has no attribute 'data'
Any help would be appreciated.
So I haven't used QDataWidgetMapper. It does look interesting, but looks more useful for when you want to have multiple widgets updated to a particular row in a model (and be able to switch between rows easily), rather than each row of a model corresponding to the value of a widget (which I think is what you are after).
So this is my rather rough implementation. Hopefully you'll be able to extend it to your application (might need a bit more error checking added, and maybe the ability to link multiple sliders to a single model row, and possibly then extending to other types of widgets)
When you drag the slider, the model is updated to the sliders new value. I've also added a text box where you can type in a number, and click the button, which will set the model to a specific value. You will notice the slider will update to this value!
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
main_layout = QtGui.QVBoxLayout()
# Create the model
self.model = MyModel()
# Create a slider and link it to the model
self.slider1 = QtGui.QSlider()
self.model.add_slider(self.slider1)
main_layout.addWidget(self.slider1)
# Add a lineEdit and button to force update the model
# Note that the LineEdit is not linked to the model, so won't update with the slider
self.edit = QtGui.QLineEdit()
button = QtGui.QPushButton('update model')
button.clicked.connect(self.on_clicked)
main_layout.addWidget(self.edit)
main_layout.addWidget(button)
self.setLayout(main_layout)
def on_clicked(self):
self.model.update_model(int(self.edit.text()),self.slider1)
class MyModel(QtGui.QStandardItemModel):
def __init__(self,*args,**kwargs):
super(MyModel,self).__init__(*args,**kwargs)
self._slider_list = {}
self.itemChanged.connect(self.on_item_changed)
def add_slider(self,slider):
if slider in self._slider_list:
raise Exception('You cannot link a slider to the model twice')
item = QtGui.QStandardItem(str(slider.value()))
self._slider_list[slider] = item
self.appendRow(item)
slider.valueChanged.connect(lambda value: self.update_model(value,slider))
def update_model(self,value,slider):
if str(value) != self._slider_list[slider].text():
self._slider_list[slider].setText(str(value))
print 'update_model: %d'%value
def on_item_changed(self,item):
slider = self._slider_list.keys()[self._slider_list.values().index(item)]
if slider.value() != int(item.text()):
slider.setValue(int(item.text()))
print 'on_item_changed: %s'%item.text()
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Hope that helps!

Categories