【问题标题】:Qt/PyQt/PySide: Trouble when re-implementing undo framework for QLineEdit subclassQt/PyQt/PySide:重新实现 QLineEdit 子类的撤消框架时出现问题
【发布时间】:2014-03-14 14:57:00
【问题描述】:

我创建了一个自定义行编辑小部件,以便我可以将其上的撤消/重做命令合并到我的应用程序的一般撤消堆栈中(而不是使用 QLineEdit 小部件附带的内置撤消/重做工具)。撤消/重做逻辑相当简单:当行编辑小部件获得焦点时,其内容立即分配给实例变量(self.init_text);并且当行编辑小部件失去焦点时,如果文本内容与存储在 self.init_text 中的内容不同,则创建一个新的 QUndoCommand 对象。 undo() 方法会将内容重新设置为 self.init_text 中的内容,而 redo() 方法会将内容重新设置为行编辑小部件失去焦点时捕获的内容。 (在任何一种方法中,行编辑都将再次获得焦点,以便用户清楚撤消或重做命令实际影响的内容。)

它似乎工作得很好,但有一个例外:如果用户通过 QPushButtons 非常快速地循环执行撤消或重做命令,那么框架就会中断。我不能比这更好地描述它,因为我不确定 Qt 引擎盖下发生了什么,但是,例如,QUndoStack 的 count() 可能会完全改变。该应用程序继续运行,终端上没有报告任何错误,但它仍然是一个损坏的撤消堆栈。

我创建了一个小 QDialog 应用程序,因此您可以尝试重新创建问题。 (使用 Python 2.7.3/PySide 1.2.1 ...如果您安装了最近的 PyQt 绑定,我认为除了前两个 import 语句之外,您不需要替换任何内容。)例如,在第一个选项卡的 QLineEdit ,如果您输入“hello”,然后跳出,然后单击返回并输入“world”,然后再次跳出,尝试非常快速地单击撤消按钮(向下并超出撤消堆栈的底部)和重做按钮(直到和超出撤消堆栈的顶部)。对我来说,这足以打破它。

#!/usr/bin/python
#coding=utf-8
from PySide.QtCore import *
from PySide.QtGui import *
import sys

class CustomRightClick(QObject):

    customRightClicked = Signal()

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

    def eventFilter(self, obj, event):
        if event.type() == QEvent.ContextMenu:
            # emit signal so that your widgets can connect a slot to that signal
            self.customRightClicked.emit()
            return True
        else:
            # standard event processing
            return QObject.eventFilter(self, obj, event)

class CommandLineEdit(QUndoCommand):

    def __init__(self, line_edit, init_text, tab_widget, tab_index, description):
        QUndoCommand.__init__(self, description)
        self._line_edit = line_edit
        self._current_text = line_edit.text()
        self._init_text = init_text
        self._tab_widget = tab_widget
        self._tab_index = tab_index

    def undo(self):
        self._line_edit.setText(self._init_text)
        self._tab_widget.setCurrentIndex(self._tab_index)
        self._line_edit.setFocus(Qt.OtherFocusReason)

    def redo(self):
        self._line_edit.setText(self._current_text)
        self._tab_widget.setCurrentIndex(self._tab_index)
        self._line_edit.setFocus(Qt.OtherFocusReason)

class CustomLineEdit(QLineEdit):

    def __init__(self, parent, tab_widget, tab_index):
        super(CustomLineEdit, self).__init__(parent)
        self.parent = parent
        self.tab_widget = tab_widget
        self.tab_index = tab_index
        self.init_text = self.text()
        self.setContextMenuPolicy(Qt.CustomContextMenu)

        undoAction=QAction("Undo", self)
        undoAction.triggered.connect(self.parent.undo_stack.undo)

        self.customContextMenu = QMenu()
        self.customContextMenu.addAction(undoAction)

        custom_clicker = CustomRightClick(self)
        self.installEventFilter(custom_clicker)
        self.right_clicked = False
        custom_clicker.customRightClicked.connect(self.menuShow)

    def menuShow(self):
        self.right_clicked = True   # set self.right_clicked to True so that the focusOutEvent won't push anything to the undo stack as a consequence of right-clicking
        self.customContextMenu.popup(QCursor.pos())
        self.right_clicked = False

    # re-implement focusInEvent() so that it captures as an instance variable the current value of the text *at the time of the focusInEvent(). This will be utilized for the undo stack command push below
    def focusInEvent(self, event):
        self.init_text = self.text()
        QLineEdit.focusInEvent(self, event)

    # re-implement focusOutEvent() so that it pushes the current text to the undo stack.... but only if there was a change!
    def focusOutEvent(self, event):
        if self.text() != self.init_text and not self.right_clicked:
            print "Focus out event. (self.text is %s and init_text is %s). Pushing onto undo stack. (Event reason is %s)" % (self.text(), self.init_text, event.reason())
            command = CommandLineEdit(self, self.init_text, self.tab_widget, self.tab_index, "editing a text box")
            self.parent.undo_stack.push(command)
        QLineEdit.focusOutEvent(self, event)

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Z:
            if event.modifiers() & Qt.ControlModifier:
                self.parent.undo_stack.undo()
            else:
                QLineEdit.keyPressEvent(self, event)
        elif event.key() == Qt.Key_Y:
            if event.modifiers() & Qt.ControlModifier:
                self.parent.undo_stack.redo()
            else:
                QLineEdit.keyPressEvent(self, event)
        else:
            QLineEdit.keyPressEvent(self, event)

class Form(QDialog):

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

        self.undo_stack = QUndoStack()

        self.tab_widget = QTabWidget()

        self.line_edit1 = CustomLineEdit(self, self.tab_widget, 0)
        self.line_edit2 = CustomLineEdit(self, self.tab_widget, 1)
        self.undo_counter = QLineEdit()

        tab1widget = QWidget()
        tab1layout = QHBoxLayout()
        tab1layout.addWidget(self.line_edit1)
        tab1widget.setLayout(tab1layout)

        tab2widget = QWidget()
        tab2layout = QHBoxLayout()
        tab2layout.addWidget(self.line_edit2)
        tab2widget.setLayout(tab2layout)

        self.tab_widget.addTab(tab1widget, "Tab 1")
        self.tab_widget.addTab(tab2widget, "Tab 2")

        self.undo_button = QPushButton("Undo")
        self.redo_button = QPushButton("Redo")
        layout = QGridLayout()
        layout.addWidget(self.tab_widget, 0, 0, 1, 2)
        layout.addWidget(self.undo_button, 1, 0)
        layout.addWidget(self.redo_button, 1, 1)
        layout.addWidget(QLabel("Undo Stack Counter"), 2, 0)
        layout.addWidget(self.undo_counter)
        self.setLayout(layout)

        self.undo_button.clicked.connect(self.undo_stack.undo)
        self.redo_button.clicked.connect(self.undo_stack.redo)
        self.undo_stack.indexChanged.connect(self.changeUndoCount)

    def changeUndoCount(self, index):
        self.undo_counter.setText("%s / %s" % (index, self.undo_stack.count()))

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

这是一个 Qt 错误吗? PySide 错误?还是我的重新实现有问题?任何帮助表示赞赏!

(我在查看我的代码时突然想到,我不妨重新实现 contextMenuEvent 而不是安装事件过滤器,但我认为这与问题无关。)

【问题讨论】:

    标签: python qt pyqt pyside


    【解决方案1】:

    出现问题是因为您在撤消/重做期间设置了QLineEdit 的焦点。 documentation 表示当命令推送到QUndoStack 时会调用redo,因此一旦您从QLineEdit 移除焦点(例如单击撤消时),焦点立即恢复 通过自动呼叫redo。在此之后,undo 命令运行(由我刚才提到的按钮单击触发)。 由于小部件已经有了焦点,当从undo 调用_line_edit.setFocus() 时,行编辑的focusInEvent 方法不运行,所以_line_edit.init_text 是没有适当更新。这意味着当您单击重做按钮时,行编辑失去焦点,并且新命令排队,因为focusOutEventif 语句中的比较被破坏,因为init_text 存储了不正确的值。然后将一个新命令添加到撤消堆栈中,该命令会覆盖您尝试恢复的命令!

    这有意义吗?

    一个简单的解决方案是在设置_line_edit 的文本后,将以下行添加到CommandLineEdit 的撤消/重做方法中。

    def undo(self):
        self._line_edit.setText(self._init_text)
        self._line_edit.init_text = self._line_edit.text()
        self._tab_widget.setCurrentIndex(self._tab_index)
        self._line_edit.setFocus(Qt.OtherFocusReason)
    
    def redo(self):
        self._line_edit.setText(self._current_text)
        self._line_edit.init_text = self._line_edit.text()
        self._tab_widget.setCurrentIndex(self._tab_index)
        self._line_edit.setFocus(Qt.OtherFocusReason)
    

    然后您可以删除您对focusInEvent 的重新实现。

    一旦您解决了这个问题,可能值得从头开始构建撤消框架的架构,而不是尝试实施我的“hacky”解决方案,因为可能有更简洁的解决方法!

    【讨论】:

    • 这很有道理,谢谢!事后看来,简单地在 undo 和 redo 方法中重新设置 init_text 似乎是一个明显的选择。我觉得自己像个傻瓜,没有意识到我的努力是多么不必要(而且,对于程序来说,是致命的)复杂。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2015-02-06
    • 2016-04-09
    • 1970-01-01
    • 2015-05-11
    • 2015-06-14
    • 1970-01-01
    • 2019-03-08
    • 2015-12-22
    相关资源
    最近更新 更多