QTableView-防止偏离单元格并关闭Delegate编辑器

时间:2019-02-11 02:48:44

标签: pyqt5

我有一个QAbstractTableModel + QTableView,并分配了一个Delegate,它创建一个QLabel小部件以用作编辑器。

我只是想这样做:当“委托”编辑器处于活动状态时,在某些情况下(当单元格中的数据无效时),禁止离开单元格并停留在编辑会话中。换句话说,如果情况需要,并且用户尝试离开单元格(通过任何方式,例如Tab,箭头键,鼠标单击等),什么都不做。只是保持原状,好像什么都没发生。

我认为这很容易,但是我仍然无法弄清楚该怎么做。

我的第一个想法是我可以捕获代表的closeEditor信号。该代码如下所示。它有点长(以便成为独立的可执行文件),但显示的大部分内容只是标准的模型/视图/委托内容。有趣的部分在底部。我已经定义了一个插槽(on_closeEditor()),并将其连接到closeEditor信号(请参见### ... ###注释)。

当按下Enter键时,代表将捕获并显式发出closeEditor信号。发生这种情况时,将调用on_closeEditor()插槽。因此,连接似乎建立正确。

但是,当单元通过其他方式(例如,Tab键或鼠标单击)离开时,尽管代理编辑器确实似乎已关闭,但插槽从未被调用。

(还有一个问题,即使我的代码可以在Delegate编辑器关闭时获得控制权,我也不清楚如何阻止它发生。时间...)

有没有简单的方法可以做到这一点?我觉得我一定很想念东西...

谢谢!

示例代码

from PyQt5 import QtCore, QtWidgets, QtGui
import sys


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data

    def rowCount(self, parent):
        return len(self.__data)

    def columnCount(self, parent):
        return len(self.__data[0])

    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))


    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False


    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.blocked = False

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())
        row = self.currentIndex().row()

        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()

        super().keyPressEvent(event)


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QLabel(parent)
        return self.editor

    def setEditorData(self, label, index):
        print('setEditorData()')
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)
        model.setData(index, None, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        print('setModelData()')
        value = label.text()
        row = index.row()
        col = index.column()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):
        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

            # Enter (or ctrl-Enter) explicitly emits commitData, closeEditor
            elif (
                key == QtCore.Qt.Key_Return and
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.CTRL)
            ):
                self.commitData.emit(target)
                self.closeEditor.emit(target)
                return True

        return False

    ### closeEditor slot ###
    def on_closeEditor(self, editor, hint):
        print('closeEditor()')


# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    tableView = TableView()
    tableView.resize(550, 160)

    delegate = Delegate()
    tableView.setItemDelegate(delegate)

    ### connect closeEditor signal to slot ###
    delegate.closeEditor.connect(delegate.on_closeEditor)

    tableView.show()

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    model = TableModel(data)
    tableView.setModel(model)

    sys.exit(app.exec_())

[edit]

我的下一个想法是,我可以为Delegate安装一个事件过滤器,并过滤掉FocusAboutToChange和/或FocusOut事件。实际上,我真的以为这将是完美的解决方案。

但是没有用。 :-(

print()语句表明已正确检测到事件。我以为如果eventFilter()返回了True,事件将被停止。但事实并非如此。光标仍然会离开已编辑的单元格。

代码

from PyQt5 import QtCore, QtWidgets, QtGui
import sys


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data

    def rowCount(self, parent):
        return len(self.__data)

    def columnCount(self, parent):
        return len(self.__data[0])

    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.blocked = False

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())

        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()

        super().keyPressEvent(event)


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QLabel(parent)
        return self.editor

    def setEditorData(self, label, index):
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)
        model.setData(index, None, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        value = label.text()
        row = index.row()
        col = index.column()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):

        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            # ASCII input
            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

        ### Ostensibly filter out FocusAboutToChange and FocusOut events ###
        if event.type() == QtCore.QEvent.FocusAboutToChange:
            print('FocusAboutToChange')
            return True
        if event.type() == QtCore.QEvent.FocusOut:
            print('FocusOut')
            return True

        return False


# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    tableView = TableView()
    tableView.resize(550, 160)

    delegate = Delegate()
    tableView.setItemDelegate(delegate)
    delegate.installEventFilter(delegate)

    tableView.show()
    model = TableModel(data)
    tableView.setModel(model)

    sys.exit(app.exec_())

1 个答案:

答案 0 :(得分:0)

万一有人关注并好奇它是如何工作的:

在Riverbank Computing PyQt邮件列表的一些人的帮助下,我得出了以下解决方案。在涉及以下内容:

如果单元格中的数据无效:

  • 重写委托的setModelData()方法,并禁止将单元格数据发布到模型中。使用label.setFocus()保持焦点在编辑器小部件上,使用view.setCurrentIndex()保持视图的当前索引不变。

  • 覆盖视图的closeEditor()插槽以防止编辑器关闭。

解决方案如下所示。

from PyQt5 import QtCore, QtWidgets, QtGui
import sys, re


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data
    def rowCount(self, parent):
        return len(self.__data)
    def columnCount(self, parent):
        return len(self.__data[0])
    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))
    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False
    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

    def __init__(self, parent=None):
        super().__init__(parent)

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())
        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()
        super().keyPressEvent(event)

    def closeEditor(self, editor, hint):

        ### --- If data validates, close editor; otherwise, don't --- ###
        if editor.validate():
            print(f'>> Closing editor')
            super().closeEditor(editor, hint)
        else:
            print(f'>> Not closing editor')


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.editor = None

    def createEditor(self, parent, option, index):
        self.view = parent.parent()
        self.editor = CellEditor(parent)
        return self.editor

    def setEditorData(self, label, index):
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        value = label.text()
        row = index.row()
        col = index.column()

        ### --- If data validates, post it to the model; otherwise, don't --- ###
        if label.validate():
            model.setData(index, value, QtCore.Qt.EditRole)
            print(f'>> [setModelData({value})] accepted')
        else:
            label.setFocus()
            self.view.setCurrentIndex(index)
            print(f'>> [setModelData({value})] rejected')

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):
        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            # ASCII input -- add to cell value
            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

            # [ctrl-H], Backspace -- delete a character
            elif (
                (key == QtCore.Qt.Key_H and mod == QtCore.Qt.CTRL) or
                (key == QtCore.Qt.Key_Backspace and mod == QtCore.Qt.NoModifier)
            ):
                self.editor.setText(self.editor.text()[:-1])
                return True

        return False


# ------------------------------------------------------------------------------
class CellEditor(QtWidgets.QLabel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet('font-style: italic; font-weight: bold; color: blue')
        self.setAutoFillBackground(True)

    ### --- Sample validation function --- ###
    def validate(self):
        return re.fullmatch('\d+', self.text())



# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    view = TableView()
    view.resize(550, 160)
    model = TableModel(data)
    view.setModel(model)
    view.show()

    delegate = Delegate()
    view.setItemDelegate(delegate)

    sys.exit(app.exec_())