I am using Qt for Python to create a view in a stock entry dialog using custom delegates. At the moment, there is a QTableView which uses two delegate classes which inherit from QStyledItemDelegate; one uses a QComboBox as its editor to provide a drop-down menu and the other a QLineEdit to provide an input mask for numeric data entry.
There is also a keypad containing QPushButtons which use QApplication.postEvent() to send QKeyEvent() key press events to the table for data entry, since this application will mostly be used on touch screens.
The problem I am encountering is trying to set focus to the editor for the correct delegate for the key press events to enter text into the QLineEdit. I have not been able to get focus to the correct object, since the delegate is not derived from QWidget and doesn't have a setFocus() method.
Here are the two delegate classes and some of relevant code from the dialog:
stock_operations_dialog.py:
class StockOperationsDialog(QWidget):
.
.
.
def _setDefaultItems(self, row: int) -> None:
for col in range(len(self._columnData)):
if row == 0:
self._model.setHeaderData(
col,
Qt.Orientation.Horizontal,
self._columnData[col].Name,
Qt.ItemDataRole.DisplayRole
)
opts = self._getFormatOpts(self._columnData[col].Format)
if opts["combo_box"] is not None:
if row == 0:
delegate = ComboBoxDelegate(self._view, self._columnData[col].Value)
delegate.currentTextChanged.connect(self._updateModelView)
self._model.setItem(row, col, QStandardItem(str(self._columnData[col].Value[0])))
else:
if row == 0:
delegate = LineEditDelegate(self._view, self._getInputMask(self._columnData[col].Format))
if isinstance(self._columnData[col].Value, list):
self._model.setItem(row, col, QStandardItem(self._columnData[col].Value[0]))
else:
self._model.setItem(row, col, QStandardItem(self._columnData[col].Value))
self._model.item(row, col).setEditable(opts["read_only"] is None)
if row == 0:
self._view.setItemDelegateForColumn(col, delegate)
self._view.openPersistentEditor(self._model.index(row, col))
if opts["focus"] is not None:
self._view.setFocus()
self._view.setCurrentIndex(self._model.index(row, col))
.
.
.
def processQwertyInput(self, keyNumber: int) -> None:
qtKeyNum = self._getQtKeyNumber(keyNumber)
QApplication.postEvent(
self.focusWidget(),
QKeyEvent(
QEvent.Type.KeyPress,
qtKeyNum,
Qt.KeyboardModifier.NoModifier,
chr(qtKeyNum) if qtKeyNum < 0x110000 else ""
)
)
QApplication.postEvent(
self.focusWidget(),
QKeyEvent(
QEvent.Type.KeyRelease,
qtKeyNum,
Qt.KeyboardModifier.NoModifier
)
)
.
.
.
combo_box_delegate.py:
from typing_extensions import override
from PySide6.QtCore import QModelIndex, QPersistentModelIndex, Signal
from PySide6.QtGui import QPainter, QStandardItemModel
from PySide6.QtWidgets import (
QApplication, QComboBox, QStyle, QStyleOptionComboBox,
QStyleOptionViewItem, QStyledItemDelegate, QTableView, QWidget
)
class ComboBoxDelegate(QStyledItemDelegate):
"""Custom QComboBox delegate for use in QTableView."""
_data: list[str]
_index = 0
currentTextChanged = Signal(str)
@override
def __init__(self, parent: QTableView, data: list[str]):
"""Initialize ComboBoxDelegate.
Args:
parent (QTableView): View which will use this delegate.
data (list[str]): A list of string values to populate the combo box editor.
"""
super(ComboBoxDelegate, self).__init__(parent=parent)
self._data = []
for datum in data:
if not isinstance(datum, str):
self._data.append(str(datum))
else:
self._data.append(datum)
@override
def paint(
self,
painter: QPainter,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> None:
del index # Not used for painting
opt = QStyleOptionComboBox()
opt.rect = option.rect
opt.currentText = self._data[self._index]
opt.editable = False
QApplication.style().drawComplexControl(QStyle.CC_ComboBox, opt, painter)
QApplication.style().drawControl(QStyle.CE_ComboBoxLabel, opt, painter)
@override
def createEditor(
self,
parent: QWidget,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> QWidget:
del option, index # Not used in derived class
comboBox = QComboBox(parent)
comboBox.setEditable(False)
comboBox.addItems(self._data)
comboBox.currentTextChanged.connect(self.updateCurrentText)
return comboBox
@override
def setEditorData(self, editor: QWidget, index: QModelIndex | QPersistentModelIndex) -> None:
del index # Not used for editor
editor.setCurrentIndex(self._index)
editor.setCurrentText(self._data[self._index])
@override
def updateEditorGeometry(
self,
editor: QWidget,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> None:
super().updateEditorGeometry(editor, option, index)
@override
def setModelData(
self,
editor: QWidget,
model: QStandardItemModel,
index: QModelIndex | QPersistentModelIndex
) -> None:
del editor # Not used for model
model.setData(index, self._data[self._index])
def updateCurrentText(self, text: str) -> None:
"""Save index into internal _data list and emit signal for row update.
Args:
text (str): Text selected in combo box.
"""
for row in range(len(self._data)):
if self._data[row] == text:
self._index = row
break
self.currentTextChanged.emit(text)
line_edit_delegate.py:
from typing_extensions import override
from re import sub
from PySide6.QtCore import QModelIndex, QPersistentModelIndex
from PySide6.QtGui import QPainter, QStandardItemModel
from PySide6.QtWidgets import (
QLineEdit, QStyleOptionViewItem, QStyledItemDelegate, QTableView,
QWidget
)
class LineEditDelegate(QStyledItemDelegate):
"""Custom QLineEdit delegate for use in QTableView with input mask."""
_mask: str
@override
def __init__(self, parent: QTableView, mask: str):
"""Initialize LineEditDelegate.
Args:
parent (QTableView): View which will use this delegate.
mask (str): A string to be used as the input mask for this delegate.
"""
super(LineEditDelegate, self).__init__(parent=parent)
self._mask = mask
@override
def paint(
self,
painter: QPainter,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> None:
super(LineEditDelegate, self).paint(painter, option, index)
@override
def createEditor(
self,
parent: QWidget,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> QWidget:
del option, index # Not used in derived class
editor = QLineEdit(parent)
if self._mask != "":
editor.setInputMask(sub(',', '', self._mask, 3))
return editor
@override
def setEditorData(self, editor: QWidget, index: QModelIndex | QPersistentModelIndex) -> None:
editor.setText(index.data())
@override
def updateEditorGeometry(
self,
editor: QWidget,
option: QStyleOptionViewItem,
index: QModelIndex | QPersistentModelIndex
) -> None:
del index # Not used in derived class
editor.setGeometry(option.rect)
@override
def setModelData(
self,
editor: QWidget,
model: QStandardItemModel,
index: QModelIndex | QPersistentModelIndex
) -> None:
model.setData(index, editor.text())
I have tried using setIndexWidget() but then the model does not update when data is entered into the view and I still have problems getting focus to the correct widget to edit.
I've also tried some of the different implementations found in How to update a QTableView cell with a QCombobox selection?, How to include a column of progress bars within a QTableView?, and https://forum.qt.io/topic/3778/item-delegate-editor-focus-problem/13.
Implementing the setFocus() method in the delegate using signals doesn't seem to work and neither does using setFocusProxy() with a QWidget member variable in the delegate class, since in either case the key presses end up having no effect. Has anybody tried something similar and, if so, how did you approach this problem?