简体   繁体   English

Qt / PyQt / PySide:重新实现QLineEdit子类的撤销框架时遇到麻烦

[英]Qt/PyQt/PySide: Trouble when re-implementing undo framework for QLineEdit subclass

I have created a custom line edit widget so that I can incorporate undo/redo commands on it into my application's general undo stack (rather than use the built-in undo/redo facilities that come with QLineEdit widgets). 我创建了一个自定义行编辑小部件,以便我可以将undo / redo命令合并到我的应用程序的一般撤消堆栈中(而不是使用QLineEdit小部件附带的内置撤消/重做工具)。 The undo/redo logic is fairly straightforward: when the line edit widget receives focus, its content is immediately assigned to an instance variable (self.init_text); 撤消/重做逻辑非常简单:当行编辑窗口小部件获得焦点时,其内容立即分配给实例变量(self.init_text); and when the line edit widget loses focus, if the text content differs from that stored in self.init_text, then a new QUndoCommand object is created. 当行编辑窗口小部件失去焦点时,如果文本内容与self.init_text中存储的文本内容不同,则会创建新的QUndoCommand对象。 The undo() method will re-set the content to whatever is in self.init_text, while the redo() method will re-set the content to whatever was captured when the line edit widget lost focus. undo()方法会将内容重新设置为self.init_text中的内容,而redo()方法会将内容重新设置为行编辑窗口小部件失去焦点时捕获的内容。 (In either method, the line edit will again receive focus so that it will be obvious to the user what the undo or redo command actually affected.) (在任一方法中,行编辑将再次获得焦点,以便用户明白撤消或重做命令实际受到什么影响。)

It seems to work just fine with one exception: if the user very quickly cycles through undo or redo commands through the QPushButtons, then the framework just breaks. 它似乎工作正常,但有一个例外:如果用户通过QPushButtons快速循环撤消或重做命令,那么框架就会中断。 I can't describe it much better than that because I'm not sure what's going on under the Qt hood, but, for example, the count() of the QUndoStack may be changed completely. 我无法描述它,因为我不确定Qt引擎盖下发生了什么,但是,例如,QUndoStack的count()可能会完全改变。 The app continues to run with no errors reported on the terminal, but it is nonetheless a broken undo stack. 应用程序继续运行,终端上没有报告错误,但它仍然是一个损坏的撤消堆栈。

I have created a little QDialog app so you can try to re-create the issue. 我创建了一个小QDialog应用程序,因此您可以尝试重新创建该问题。 (Using Python 2.7.3/PySide 1.2.1 ... if you have a recent PyQt binding installed, I don't think you should need to replace anything except the first two import statements.) For example, in the first tab's QLineEdit, if you type 'hello', then tab out, then click back in and type 'world', then tab out again, try very swiftly clicking the undo button (down to and beyond the bottom of the undo stack) and redo button (up to and beyond the top of the undo stack). (使用Python 2.7.3 / PySide 1.2.1 ...如果你安装了最近的PyQt绑定,我认为除了前两个import语句之外你不需要替换任何东西。)例如,在第一个选项卡的QLineEdit中,如果你输入'hello',然后选择标签,然后点击返回并键入'world',然后再次标签,尝试非常快速地点击撤销按钮(向下和超出撤销堆栈的底部)和重做按钮(直到和超出撤销堆栈的顶部)。 For me, that was enough to break it. 对我来说,这足以打破它。

#!/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_()

Is this a Qt bug? 这是一个Qt错误吗? A PySide bug? 一个PySide错误? Or is there a problem in my re-implementation? 或者我的重新实施有问题吗? Any help is appreciated! 任何帮助表示赞赏!

(It just occurred to me when reviewing my code that I might as well re-implement the contextMenuEvent rather than install the event filter, but I suppose that has no bearing on the issue.) (在审查我的代码时我刚想到我可能会重新实现contextMenuEvent而不是安装事件过滤器,但我认为这与此问题无关。)

The issue is arising because you are setting the focus of the QLineEdit during undo/redo. 出现此问题是因为您在撤消/重做期间设置了QLineEdit的焦点。 The documentation indicates that redo is called when the command is pushed to the QUndoStack , so as soon as you remove focus from the QLineEdit (say when clicking undo), focus is immediately restored by the automatic call to redo . 文档指示在将命令推送到QUndoStack时调用redo ,因此只要从QLineEdit删除焦点(例如,单击撤消时), 就会立即通过自动调用redo恢复焦点 After this, the undo command runs (triggered by the button click I just mentioned). 在此之后, undo命令运行(由我刚刚提到的按钮单击触发)。 As the widget already has focus , the focusInEvent method of the line edit does not run when _line_edit.setFocus() is called from undo , so the _line_edit.init_text is not updated appropriately. 由于窗口小部件已经具有焦点 ,因此当从undo调用_line_edit.setFocus()时,行编辑的focusInEvent方法不会运行 ,因此_line_edit.init_text未正确更新。 This means when you click the redo button, the line-edit loses focus, and a new command is queued up because the comparison in the if statement of focusOutEvent is broken as init_text has the incorrect value stored. 这意味着当您单击重做按钮时,行编辑失去焦点,并且新命令排队,因为focusOutEventif语句中的比较被破坏,因为init_text存储了不正确的值。 A new command is then added to the undo stack which overwrites the one you were trying to restore! 然后在撤消堆栈中添加一个新命令,覆盖您尝试恢复的命令!

Does that make sense? 那有意义吗?

An easy solution is to add the following line to the undo/redo methods in CommandLineEdit after you set the text of the _line_edit . 一个简单的解决方案是在设置_line_edit的文本后, _line_edit添加到CommandLineEdit的undo / redo方法。

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)

you can then remove your re-implementation of focusInEvent . 然后,您可以删除focusInEvent重新实现。

Once you've wrapped your head around the problem, it might be worth starting the architecture of your undo framework from scratch rather than trying to implement my "hacky" solution as there is probably a cleaner way of fixing it! 一旦你解决了问题,可能值得从头开始构建你的撤销框架,而不是试图实现我的“hacky”解决方案,因为可能有一种更清晰的方法来解决它!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM