QTableView with ComboBox Delegate and Button Entry

61 Views Asked by At

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?

0

There are 0 best solutions below