I got the following problem:
I have a GUI with a qtableview, a generic delegate (as in the book of Marc Summerfield) and a datamodel for that.
I designed a Dialog which shows up, if i want to change certain columns. There is a label, a combobox and a button in the dialog.
Until now, everything works fine, selection can be done, data can be processed.
But there is no possibility to set the position where the dialog pops up. So sometimes it is simply un-operatable (almost off the screen and so on)
So my question:
How can I set the position, where the dialog pops up?
Minimum example:
The generic delegate
class GenericDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super(GenericDelegate, self).__init__(parent)
self.delegates = {}
def insertColumnDelegate(self, column, delegate, **kwargs):
delegate.setParent(self)
self.delegates[column] = delegate
def removeColumnDelegate(self, column):
if column in self.delegates:
del self.delegates[column]
def paint(self, painter, option, index):
delegate = self.delegates.get(index.column())
if delegate is not None:
delegate.paint(painter, option, index)
else:
QItemDelegate.paint(self, painter, option, index)
def createEditor(self, parent, option, index):
delegate = self.delegates.get(index.column())
if delegate is not None:
return delegate.createEditor(parent, option, index)
else:
return QStyledItemDelegate.createEditor(self, parent, option, index)
def setEditorData(self, editor, index):
delegate = self.delegates.get(index.column())
if delegate is not None:
delegate.setEditorData(editor, index)
else:
QStyledItemDelegate.setEditorData(self, editor, index)
def setModelData(self, editor, model, index):
delegate = self.delegates.get(index.column())
if delegate is not None:
delegate.setModelData(editor, model, index)
else:
QStyledItemDelegate.setModelData(self, editor, model, index)
def setDelegateData(self, index, data):
delegate = self.delegates.get(index.column())
if delegate is not None and hasattr(delegate, "setDelegateData"):
delegate.setDelegateData(data)
else:
pass
The columns delegate
class ComboBoxColumnDelegate(QStyledItemDelegate):
def __init__(self, dialog, editordata=[], parent=None, target="Folder"):
super().__init__(parent)
self.editor = dialog
self.parent = parent
self.editordata = editordata
self.target = target
def createEditor(self, parent, option, index):
editor = self.editor(parent=self.parent, target = self.target, values=self.editordata)
return editor
def setDelegateData(self, data = {}):
if not data:
return 0
for key, value in data.items():
if hasattr(self, key):
setattr(self, key, value)
def setEditorData(self, editor, index):
editor.setData(self.editordata)
def setModelData(self, editor, model, index):
if editor.result() == QDialog.Accepted:
model.setData(index, editor.selectedChoice(), Qt.EditRole)
For the dialog, a normal QDialog can be taken, since only the position of it matters.
In the mainwindow I initialize the delegate:
class mw(QMainWindow)
def __init(self, parent=None):
super(mw, self).__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self._init_testtablemodel()
....
def _init_testtablemodel(self):
self.testmodel = testtablemodel(self)
self.ui.tableView_tests.setModel(self.testmodel)
# generic delegate
test_delegate = GenericDelegate()
# Insert the delegates for the columns
test_delegate.insertColumnDelegate(0, ComboboxColumnDelegate())
...
self.ui.tableView_tests.setItemDelegate(test_delegate)
The model is as recommended:
class testtablemodel(QAbstractTableModel)
def __init__(self, parent=None)
...
def rowCount(self, index):
...
def columnCount(self, index):
...
def data(self, index, role):
...
def setData(self, index, value, role):
...
def headerData(self, section, orientation, role):
...
def flags(self, index):
...
...
As mentioned befor, all data processing works perfectly nice, but the position of the editor poping up is bad.
I hope, the basic structure of the program is understandable.
Edit:
Changed the QItemDelegate to QStyledItemDelegate, which works as well and makes no difference so far.
Related
I would like to add a column of checkbox to my qtableview. I need the checkboxes to be in the center of column (i.e. aligned center). I have this example which works fine, BUT the checkboxes are aligned left.
import sys
import pandas as pd
from PyQt5 import QtCore, Qt
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QCheckBox, QHBoxLayout, QItemDelegate, QTableView, QWidget, QApplication, QMainWindow
class MyCheckboxDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState())
#pyqtSlot()
def stateChanged(self):
self.commitData.emit(self.sender())
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
print(data)
self._data = data
def rowCount(self, index=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if self._data.columns[index.column()]=='Delete':
return ''
value = self._data.iloc[index.row(), index.column()]
return str(value)
class MyWindow(QMainWindow):
def __init__(self, *args):
QWidget.__init__(self, *args)
table_model = TableModel(pd.DataFrame([['', ''], ['','']]))
self.table_view = QTableView()
self.table_view.setModel(table_model)
self.table_view.setItemDelegateForColumn(0, MyCheckboxDelegate(self))
for row in range(0, table_model.rowCount()):
self.table_view.openPersistentEditor(table_model.index(row, 0))
self.setCentralWidget(self.table_view)
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
To force them to come at center, I create a widget, add a layout and add the check box to the layout. In other words, I change the createEditor function of MyCheckboxDelegate as follows:
def createEditor(self, parent, option, index):
w = QWidget(parent)
layout = QHBoxLayout(w)
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
check.setStyleSheet("color: red;")
layout.addWidget(check)
layout.setAlignment(Qt.AlignCenter)
return w
The problem is that now, the setModelData will not be called anymore. I need to access `model' after a checkbox is clicked.
Has anybody an idea how to fix it?
Item delegates are able to set model data as long as the editor has a user property, and a basic QWidget doesn't.
The solution is to create a QWidget subclass that implements that property, and connect the checkbox to a signal that will actually do the same as before:
class CenterCheckBox(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, parent):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.check = QCheckBox()
layout.addWidget(self.check, alignment=Qt.AlignCenter)
self.check.setFocusProxy(self)
self.check.toggled.connect(self.toggled)
# set a 0 spacing to avoid an empty margin due to the missing text
self.check.setStyleSheet('color: red; spacing: 0px;')
#pyqtProperty(bool, user=True) # note the user property parameter
def checkState(self):
return self.check.isChecked()
#checkState.setter
def checkState(self, state):
self.check.setChecked(state)
class MyCheckboxDelegate(QStyledItemDelegate):
def __init__(self, parent):
super().__init__(parent)
def createEditor(self, parent, option, index):
check = CenterCheckBox(parent)
check.toggled.connect(lambda: self.commitData.emit(check))
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState)
Note that:
it's usually better to use QStyledItemDelegate, which is more consistent with the overall application appearance;
you should always check the column (or row) before returning the editor and do the same (or at least check the editor) in setModelData(), or, alternatively, use setItemDelegateForColumn();
I am trying to add a row to QTableView with a QAbstractTableModel and QItemDelegate where the widgets appear in the added row. From what I've read I need to call .edit(index) on each item of the added row to call createEditor where the widgets are created however I am getting edit: editing failed
QItemDelegate:
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
# COMBOBOX, LINEEDIT, TIMEDIT
if index.column() == 0:
comboBox = QComboBox(parent)
for text in self.type_items:
comboBox.addItem(text, (index.row(), index.column()))
return comboBox
elif index.column() == 1:
lineEdit = QLineEdit(parent)
return lineEdit
elif index.column() == 2:
timeEdit = QTimeEdit(parent)
return timeEdit
def setEditorData(self, editor, index):
value = index.model()._data[index.row()][index.column()]
if index.column() == 0:
editor.setCurrentIndex(self.type_items.index(value))
elif index.column() == 1:
editor.setText(str(value))
elif index.column() == 2:
editor.setTime(value)
QAbstractTableModel:
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
pass
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
Main:
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
localWidget = QWidget()
self.table = QTableView(localWidget)
data = [["1", "Hi", QTime(2, 1)], ["2", "Hello", QTime(3, 0)]]
self.model = TableModel(data)
self.table.setModel(self.model)
self.table.setItemDelegate(Delegate())
self.add_row = QPushButton("Add Row", localWidget)
self.add_row.clicked.connect(self.addRow)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.table.openPersistentEditor(index)
layout_v = QVBoxLayout()
layout_v.addWidget(self.table)
layout_v.addWidget(self.add_row)
localWidget.setLayout(layout_v)
self.setCentralWidget(localWidget)
self.show()
def addRow(self):
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.beginInsertRows(QModelIndex(), self.model.rowCount(), self.model.rowCount())
self.model._data.append(new_row_data)
self.model.endInsertRows()
for i in range(len(new_row_data)):
index = self.table.model().index(self.model.rowCount()-1, i)
self.table.edit(index)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
How can I trigger the QItemDelegate's createEditor to create the widgets for the added row's items and setEditorData to populate them?
First of all, you could use openPersistentEditor as you already did in the __init__.
There are two reasons for the error in self.table.edit():
in order to allow editing of an item through edit(), the flags() function must be overridden and provide the Qt.ItemIsEditable flag;
you cannot call edit() multiple times on different indexes within the same function;
Then, there are other important problems in your code:
you're not correctly using the model, as data() is not implemented;
the delegates should use the base functions of the model whenever they provide standard behavior (accessing index.model()._data is not good);
setEditorData is unnecessary, as long as the above aspects are respected: Qt automatically sets the data based on the data type and the editor class;
setData() should be implemented in order to correctly set the data on the model, otherwise the new data won't be accessible;
changes in the model structure (like creating a new row) should be done in the model class;
Here is a revised version of your code:
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
if index.column() == 0:
comboBox = QComboBox(parent)
for text in self.type_items:
comboBox.addItem(text, (index.row(), index.column()))
return comboBox
# no need to check for the other columns, as Qt automatically creates a
# QLineEdit for string values and QTimeEdit for QTime values;
return super().createEditor(parent, option, index)
# no setEditorData() required
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._data.append(data)
self.endInsertRows()
def data(self, index, role=Qt.DisplayRole):
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def flags(self, index):
# allow editing of the index
return super().flags(index) | Qt.ItemIsEditable
class MainWindow(QMainWindow):
# ...
def addRow(self):
row = self.model.rowCount()
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.appendRowData(new_row_data)
for i in range(self.model.columnCount()):
index = self.model.index(row, i)
self.table.openPersistentEditor(index)
I am trying to make everything shown by the current code un-editable.
Previous searches all suggest either modifying the flags() function of the model or using the setEditTriggers of the table. I do both in this code, but neither of them work.
Looking at a widget-by-widget case, I can find readonly modes for LineEdit and others, but not for ComboBox. So I can not even modify the delegate to force the readonly constraint, not that I would necessarily like to do it this way.
EDIT: to clarify, when I say I want the user to not be able to 'edit' I mean that he shouldn't be able to change the state of the widget in any way. E.g. he won't be able to click on a ComboBox (or at least changing the current selected item/index).
from PyQt5 import QtCore, QtWidgets
import sys
class MyWindow(QtWidgets.QWidget):
def __init__(self, *args):
super().__init__(*args)
tableview = TableView()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(tableview)
self.setLayout(layout)
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, model):
super().__init__()
self.model = model
def createEditor(self, parent, option, index):
widget = QtWidgets.QComboBox(parent)
widget.addItems(['', 'Cat', 'Dog'])
return widget
def setModelData(self, widget, model, index):
self.model.setData(index, widget.currentIndex())
class Model(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self.value = 0
def flags(self, index):
return QtCore.Qt.ItemIsEnabled
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
return QtCore.QVariant(self.value)
def setData(self, index, value, role=QtCore.Qt.EditRole):
self.value = value
print("data[{}][{}] = {}".format(index.row(), index.column(), value))
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return 1
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
self.model = Model(self)
delegate = Delegate(self.model)
self.setItemDelegate(delegate)
self.setModel(self.model)
self.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.openPersistentEditor(index)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Explanation:
Some concepts must be clarified:
For Qt to disable editing that a view (QListView, QTableView, QTreeView, etc.) or item of the view implies only that the editor will not open through user events such as clicked, double-clicked, etc.
The user interaction in Qt follows the following path:
The user interacts through the OS with the mouse, keyboard, etc.
the OS notifies Qt of that interaction.
Qt creates QEvents and sends it to the widgets.
The widget analyzes what you should modify regarding the QEvent you receive.
In your case, using openPersistentEditor() shows the widgets, and so the edibility from the Qt point of view is not valid for this case.
Solution:
Considering the above a possible general methodology to make a widget not editable: block some point of the user-widget interaction path. In this case, the simplest thing is to prevent the widget from receiving the QEvents through an event filter.
Considering the above, the solution is:
class DisableEventsManager(QtCore.QObject):
def __init__(self, *, qobject, events=None, apply_childrens=False):
if not isinstance(qobject, QtCore.QObject):
raise TypeError(
f"{qobject} must belong to a class that inherits from QObject"
)
super().__init__(qobject)
self._qobject = qobject
self._events = events or []
self._qobject.installEventFilter(self)
self._children_filter = []
if apply_childrens:
for child in self._qobject.findChildren(QtWidgets.QWidget):
child_filter = DisableEventsManager(
qobject=child, events=events, apply_childrens=apply_childrens
)
self._children_filter.append(child_filter)
#property
def events(self):
return self._events
#events.setter
def events(self, events):
self._events = events
for child_filter in self._children_filter:
child_filter.events = events
def eventFilter(self, obj, event):
if self.events and self._qobject is obj:
if event.type() in self.events:
return True
return super().eventFilter(obj, event)
def createEditor(self, parent, option, index):
combo = QtWidgets.QComboBox(parent)
combo.addItems(["", "Cat", "Dog"])
combo_event_filter = DisableEventsManager(qobject=combo)
combo_event_filter.events = [
QtCore.QEvent.KeyPress,
QtCore.QEvent.FocusIn,
QtCore.QEvent.MouseButtonPress,
QtCore.QEvent.MouseButtonDblClick,
]
return combo
I'm trying to create a QListView with QStyledItemDelegate to show data more organizing way.
I gone through this site, and its all in C++, and I have no idea about it, guessing from the syntax and calls which has been used in the post, I tried my way to achieve it, but I had not luck. Can someone please help me out with this?
import sys, os
from PyQt4 import QtGui, QtCore
class ListView(QtGui.QListView):
def __init__(self, parent=None):
super(ListView, self).__init__(parent)
self._model = None
self._data = [
[
'Header: King Arthur',
'Project: TBN',
'Asset: arthur',
'Task name: Design doc',
'Start Date: Today',
'End Date: Next Monday'
]
]
self.set_model()
item_delegate = ItemDelegate()
self.setItemDelegate(item_delegate)
self.openPersistentEditor(self._model.createIndex(0, 0))
def set_model(self):
self._model = ListModel(self._data, parent=self)
self.setModel(self._model)
class ListModel(QtCore.QAbstractItemModel):
def __init__(self, data=[], parent=None):
super(ListModel, self).__init__(parent)
self._data = data
def rowCount(self, *arg):
return 1
def columnCount(self, *arg):
return len(self._data)
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(' | '.join(self._data[column]))
return QtCore.QVariant()
def index(self, row, column, parent):
return self.createIndex(row, column)
def parent(self, index):
item = index.internalPointer()
if item:
return item.getParent()
else:
item = self.createIndex(index.row(), index.column()).internalPointer()
if item:
return item.getParent()
return QtCore.QModelIndex()
class ItemDelegate(QtGui.QStyledItemDelegate):
def createEditor(self, parent, option, index):
item_data = str(index.data().toString())
editor = Widget(item_data.split('|'), parent=parent)
return editor
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class Widget(QtGui.QWidget):
def __init__(self, widget_data=[], parent=None):
super(Widget, self).__init__(parent)
vbox = QtGui.QVBoxLayout(self)
key_font = QtGui.QFont()
key_font.setWeight(QtGui.QFont.Bold)
val_font = QtGui.QFont()
val_font.setWeight(QtGui.QFont.Normal)
for each_data in widget_data:
hbox = QtGui.QHBoxLayout()
key, value = each_data.split(':')
key_text = QtGui.QLabel(self)
val_text = QtGui.QLabel(self)
key_text.setToolTip('Key: %s' % key)
val_text.setToolTip('Value: %s' % value)
key_text.setText(key)
val_text.setText(value)
key_text.setFont(key_font)
val_text.setFont(val_font)
hbox.addWidget(key_text)
hbox.addWidget(val_text)
# vbox.addLayout(hbox)
if __name__ == '__main__':
qapp = QtGui.QApplication([])
app = ListView()
app.show()
sys.exit(qapp.exec_())
Unless you have a compelling reason to use the View/Model pattern, in most cases it's going to be easier to use the Widget/Item pattern -- QListWidget and QListWidgetItem.
The QItemDelegate and QStyledItemDelegate are designed to be subclassed. You then define methods that are responsible for handling events, sizing, and painting the views/widgets.
class MyWidget(QtGui.QWidget):
def __init__(self, parent):
super(MyWidget, self).__init__(parent)
self.listwidget = QtGui.QListWidget(self)
self.delegate = MyDelegate(self, self.listwidget)
self.listwidget.setItemDelegate(self.delegate)
datas = [
{'Header': 'Blah', 'Project': 'TBN'},
{'Header': 'Other', 'Project': 'Something'},
]
for data in datas:
MyItem(self.listwidget, data)
class MyItem(QtGui.QListWidgetItem):
def __init__(self, parent, data):
super(MyItem, self).__init__(parent)
self._data = data
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent, listwidget):
super(MyDelegate, self).__init__(parent)
self.listwidget = listwidget
def sizeHint(self, option, index):
if index:
item = self.listwidget.itemFromIndex(index)
print item._data
# Do fontmetrics stuff from C++ article you linked
# using the text in the data dictionary.
def paint(self, painter, option, index):
# same thing, get the item using the index
# get the data from the item
# paint the data however you want.
class ItemDelegate(QtGui.QStyledItemDelegate):
def createEditor(self, parent, option, index):
item_data = str(index.data().toString())
editor = Widget(item_data.split('|'), parent=parent)
return editor
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
painter.save()
index.model().setData(index, option.rect.width(), Qt.UserRole+1)
fixed this issue
I am stumped. In the code below:
class LineEdit(QtGui.QLineEdit):
def __init__(self, value="", parent=None, commit=None):
super(LineEdit, self).__init__(parent=parent)
self.setText("blabla")
self.commit = commit
self.editingFinished.connect(self.on_change)
print self.text()
self.text() is "blabla" but the LineEdit does not show the text and after editing self.text() is "".
The editor is created in a QStyledItemDelegate() with createEditor() for a QTreeView().
Can anyone explain to me why this happens and how to fix it?
If you're using an item delegate, the initial text shown in the editor will be taken from the model, and any existing text will be overwritten.
To control what happens before and after editing, reimplement the setEdtorData and setModelData methods of the item delegate:
class Delegate(QtGui.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if index.column() < 2:
return LineEdit(parent)
return super(Delegate, self).createEditor(parent, option, index)
def setEditorData(self, editor, index):
if index.column() == 0:
editor.setText('blabla')
elif index.column() == 1:
editor.setText(index.data().toString())
# Python 3
# editor.setText(index.data())
else:
super(Delegate, self).setEditorData(editor, index)
def setModelData(self, editor, model, index):
if index.column() < 2:
value = editor.text()
print(value)
model.setData(index, value, QtCore.Qt.EditRole)
else:
super(Delegate, self).setModelData(editor, model, index)