首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用撤消/重做功能执行树应用程序

使用撤消/重做功能执行树应用程序
EN

Code Review用户
提问于 2015-04-26 20:48:24
回答 2查看 2.6K关注 0票数 25

我正在用Python制作一个to树应用程序(Earlybird),它将在树视图中显示可检查的任务/子任务。它包括在没有复选框的块(例如,工作块、归属块)中分组的高级任务组。

看起来是这样的:

这是我第一个使用Python和面向对象编程的应用程序.我通常是一个Matlab程序员。因此,对我所做的任何程度的反馈都是非常有帮助的。我即将将应用程序放在Github上,并添加了大量的特性,所以现在是向它扔一些炸药的最佳时机。

应用程序有两个主要类:

  1. EarlybirdTree:核心树视图。这是来自QTreeView的子类,并定义了基本方法(加载和保存文件,向撤销堆栈添加已编辑的项,等等)。
  2. EarlybirdMainEarlybirdTree对象的简单包装器。这是QMainWindow的子类,允许用户使用菜单和工具栏与树的方法交互。

下面的代码包括上述两个py文件以及一个早期鸟类数据文件(testFile.eb)。我以json格式存储数据。为了方便起见,每个main函数还添加了一个单独的解堆栈视图。注意,为了防止这种情况发生,我还没有包括添加/删除项的功能以及许多其他的花哨功能。

earlybirdTree.py

代码语言:javascript
复制
# -*- coding: utf-8 -*-
"""
earlybirdTree.py
    Defines the EarlyBirdTree class, a QTreeView subclass that displays a 
    custom QStandardItemModel as a simple to-do tree. The data is saved
    as a custom json file.
"""

import sys
import os
import json
from PySide import QtGui, QtCore


class StandardItemModel(QtGui.QStandardItemModel):
    '''Items will emit this signal when edited'''
    itemDataChanged = QtCore.Signal(object, object, object, object)


class StandardItem(QtGui.QStandardItem):
    ''''Subclass QStandardItem to reimplement setData to emit itemDataChanged'''
    def setData(self, newValue, role=QtCore.Qt.UserRole + 1):
        #print "setData called with role ", role  #for debugging
        if role == QtCore.Qt.EditRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)
            model = self.model()
            if model is not None and oldValue != newValue:
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        if role == QtCore.Qt.CheckStateRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)            
            model = self.model()
            if model is not None and oldValue != newValue:                             
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        QtGui.QStandardItem.setData(self, newValue, role)


class EarlybirdTree(QtGui.QTreeView):
    '''The earlyBird to do tree view, the core class for the application.'''

    def __init__(self, parent=None, filename = None):
        QtGui.QTreeView.__init__(self, parent=None)
        self.parent = parent
        self.filename = filename
        self.model = StandardItemModel()
        self.rootItem = self.model.invisibleRootItem()        
        self.setModel(self.model)
        self.makeConnections()
        self.undoStack = QtGui.QUndoStack(self)
        self.setStyleSheet("QTreeView::item:hover{background-color:#999966;}")  
        self.headerLabels = ["Task"]
        self.model.setHorizontalHeaderLabels(self.headerLabels) 
        if self.filename:
            self.loadEarlybirdFile(self.filename)

    def makeConnections(self):
        '''Connect all the signals-slots needed.'''
        self.model.itemDataChanged.connect(self.itemDataChangedSlot)

    def itemDataChangedSlot(self, item, oldValue, newValue, role):
        '''Slot used to push changes of existing items onto undoStack'''
        if role == QtCore.Qt.EditRole:
            command = CommandTextEdit(self, item, oldValue, newValue,
                "Text changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True
        if role == QtCore.Qt.CheckStateRole:
            command = CommandCheckStateChange(self, item, oldValue, newValue, 
                "CheckState changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True  

    def clearModel(self):
        '''Clears data from model,clearing the view, but repopulates headers/root.
        Used whenever an .eb file is loaded, or newFile method instantiated'''
        self.model.clear()
        self.model.setHorizontalHeaderLabels(self.headerLabels)
        self.rootItem = self.model.invisibleRootItem()   

    def newFile(self):
        '''Creates blank tree'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.saveTodoData()
        self.filename = None
        self.clearModel()
        self.undoStack.clear()

    def closeEvent(self, event):
        '''Typically closeevent is called by a QMainWindow wrapper, 
        but sometimes we do view these guys standalone'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.fileSave()
        self.close()     

    '''
    ***
    Next five methods are part of mechanics for loading .eb files
    ***
    '''
    def loadEarlybirdFile(self, filename = None):
        '''Opens todo tree file (.eb) and populates model with data.'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.saveTodoData() 
        directoryName = os.path.dirname(filename) if filename else "."
        if not filename:
            filename, foo = QtGui.QFileDialog.getOpenFileName(None,
                    "Load earlybird file", directoryName, 
                    "(*.eb)")          
        if filename:
            with open(filename) as f:
                fileData = json.load(f)
            if self.populateModel(fileData, filename):
                self.expandAll()
                self.filename = filename
                self.undoStack.clear()
                return True        
        return False   

    def populateModel(self, fileData, filename):
        '''Verify that top-level items are blocks, and call methods to load data.'''
        if "taskblocks" not in fileData:
            print "Warning: Cannot load {0}.\n"\
                  "Top level must contain taskblocks.".format(filename)
            return False 
        if "tasks" in fileData:
            print "Warning: only reads taskblocks from top level.\n"\
                  "Igorning top-level tasks in {0}.".format(filename)
        taskblockList = fileData["taskblocks"]
        self.clearModel()
        return self.loadTaskblocks(taskblockList)

    def loadTaskblocks(self, taskblockList):  
        '''Load task blocks into the model'''
        for (blockNum, taskblock) in enumerate(taskblockList): 
            blockNameItem = StandardItem(taskblock["blockname"])       
            self.rootItem.appendRow(blockNameItem)
            if "tasks" in taskblock:
                taskList = taskblock["tasks"]
                self.loadTasks(taskList, blockNameItem) 
        return True      

    def loadTasks(self, taskList, parentItem):
        '''Recursively load tasks until we hit a base task (a task w/o any subtasks).'''
        for (taskNum, task) in enumerate(taskList):
            taskNameItem = StandardItem(task["name"])
            taskNameItem.setCheckable(True)
            #print "task and done", task["name"], task["done"]
            if task["done"]:
                taskNameItem.setCheckState(QtCore.Qt.Checked)           
            else:
                taskNameItem.setCheckState(QtCore.Qt.Unchecked)
            parentItem.appendRow(taskNameItem) #add children only to column 0  
            if "tasks" in task:
                subtaskList = task["tasks"]
                return self.loadTasks(subtaskList, taskNameItem) 

    '''
    ****
    Next seven methods are part of the saving mechanics
    ***
    '''
    def saveCheck(self):
        '''If the document has been changed since last clean state, ask if the user
        wants to save the changes.'''
        if QtGui.QMessageBox.question(self,
                "Earlybird save check",
                "Save unsaved changes first?",
                QtGui.QMessageBox.Yes|QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
            return True
        else:
            return False

    def saveTodoData(self): 
        '''Save data from the tree in json format'''
        if self.filename:
            dictModel = self.modelToDict()
            with open(self.filename, 'w') as fileToWrite:
                json.dump(dictModel, fileToWrite, indent=2)
        else:
            self.saveTodoDataAs()
        self.undoStack.clear()

    def saveTodoDataAs(self):
        '''Save data in model as...x'''
        dir = os.path.dirname(self.filename) if self.filename is not None else "."
        self.filename, flt = QtGui.QFileDialog.getSaveFileName(None,
                "EarlyBird: Load data file", dir, "EarlyBird data (*.eb)")           
        if self.filename:
            print "Saving: ", self.filename #for debugging
            dictModel = self.modelToDict()
            with open(self.filename, 'w') as fileToWrite:
                json.dump(dictModel, fileToWrite, indent=2)
        self.undoStack.clear()

    def modelToDict(self):  #def modelToDict(self, parentItem = self.rootItem):
        '''Takes model presently in view, and saves all data as dictionary.
        Called by self.saveTodoData() and self.saveTodoDataAs()'''
        dictModel = {}       
        if self.rootItem.rowCount():           
            dictModel["taskblocks"]= self.createTaskblockList(self.rootItem)
            return dictModel

    def createTaskblockList(self, parentItem):
        '''Creates list of task blocks, and their tasks (latter using createTasklist).
        Called by modelToDict which is used to save the model as a dictionary'''
        numChildren = parentItem.rowCount()
        if numChildren:
            taskblockList = [None] * numChildren
            childList = self.getChildren(parentItem)
            for childNum in range(numChildren):
                childItem = childList[childNum]
                childTaskblockData = {}
                childTaskblockData["blockname"]=childItem.text()               
                #now see if the block has children (tasks)
                if childItem.rowCount():
                    childTaskblockData["tasks"] = self.createTaskList(childItem)
                taskblockList[childNum] = childTaskblockData
            return taskblockList
        else:
            return None

    def createTaskList(self, parentItem):
        '''Recursively traverses model creating list of tasks to
        be saved as json'''
        numChildren = parentItem.rowCount()
        if numChildren:
            taskList = [None] * numChildren
            childList = self.getChildren(parentItem)
            for childNum in range(numChildren):
                childItem = childList[childNum]
                childTaskData = {}
                childTaskData["name"] = childItem.text()
                childTaskData["done"] = True if childItem.checkState() else False
                #now see if the present child has children
                if childItem.rowCount():
                    childTaskData["tasks"] = self.createTaskList(childItem)
                taskList[childNum] = childTaskData
            return taskList
        else:
            return None

    def getChildren(self, parentItem):
        '''Returns list of child items of parentItem. Used when converting
        model to dictionary for saving as json'''
        numChildren = parentItem.rowCount()
        if numChildren > 0:
            childItemList = [None] * numChildren
            for childNum in range(numChildren):
                childItemList[childNum] = parentItem.child(childNum, 0)
        else:
            childItemList = None
        return childItemList


class CommandTextEdit(QtGui.QUndoCommand):
    '''Command for undoing/redoing text edit changes, to be placed in undostack'''
    def __init__(self, earlybirdTree, item, oldText, newText, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = earlybirdTree
        self.oldText = oldText
        self.newText = newText

    def redo(self):      
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.newText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 

    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.oldText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 


class CommandCheckStateChange(QtGui.QUndoCommand):
    '''Command for undoing/redoing check state changes, to be placed in undostack'''
    def __init__(self, earlybirdTree, item, oldCheckState, newCheckState, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = earlybirdTree
        self.oldCheckState = QtCore.Qt.Unchecked if oldCheckState == 0 else QtCore.Qt.Checked
        self.newCheckState = QtCore.Qt.Checked if oldCheckState == 0 else QtCore.Qt.Unchecked

    def redo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setCheckState(self.newCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 

    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)
        self.item.setCheckState(self.oldCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 


def main():
    ebApp = QtGui.QApplication(sys.argv)
    firstEb = EarlybirdTree(filename = "testFile.eb")
    firstEb.show()
    undoView = QtGui.QUndoView(firstEb.undoStack)
    undoView.show()
    sys.exit(ebApp.exec_())


if __name__ == "__main__":
    main()

earlybirdMain.py

代码语言:javascript
复制
# -*- coding: utf-8 -*-
"""
earlybirdMain.py: 
    A wrapper for the EarlybirdTree class (defined in earlybirdTree.py).
    The wrapper allows for simple menu/toolbar-based user interaction
    with an earlybird to do tree. Includes undo/redo functionality.
"""
import sys
import os
from PySide import QtGui, QtCore
from earlybirdTree import EarlybirdTree

class EarlybirdMain(QtGui.QMainWindow):
    '''Main window to wrap an EarlybirdTree'''
    def __init__(self, filename = None):
        QtGui.QMainWindow.__init__(self)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 
        self.view = EarlybirdTree(self, filename) 
        self.model = self.view.model
        self.windowTitleSet()
        self.setCentralWidget(self.view)
        self.createStatusBar()
        self.createActions()
        self.createToolbars()
        self.createMenus()

    def createToolbars(self):
        '''Create toolbars for actions on files and items'''
        self.fileToolbar = self.addToolBar("File actions")
        self.fileToolbar.addAction(self.fileNewAction)
        self.fileToolbar.addAction(self.fileOpenAction)
        self.fileToolbar.addAction(self.fileSaveAction)
        self.fileToolbar.addAction(self.fileSaveAsAction)
        self.itemToolbar = self.addToolBar("Item actions")
        self.itemToolbar.addAction(self.undoAction)
        self.itemToolbar.addAction(self.redoAction)

    def closeEvent(self, event):
        '''If data has been changed, ask user if they want to save it'''
        if not self.view.undoStack.isClean() and self.view.saveCheck():
            self.view.fileSave()
        self.close()

    def createMenus(self):
        '''Create menu for actions on files'''
        self.fileMenu = self.menuBar().addMenu("&File")
        self.fileMenu.addAction(self.fileOpenAction)    
        self.fileMenu.addAction(self.fileNewAction)
        self.fileMenu.addAction(self.fileSaveAction)
        self.fileMenu.addAction(self.fileSaveAsAction)

    def createActions(self):
        '''Create all actions to be used in toolbars/menus: calls createAction()'''
        #File actions
        self.fileNewAction = self.createAction("&New", slot = self.newFile,
                shortcut = QtGui.QKeySequence.New, tip = "New file",
                status = "Create a new file")
        self.fileOpenAction = self.createAction("&Open...", slot = self.fileOpen,
                shortcut = QtGui.QKeySequence.Open, tip = "Open file",
                status = "Open an existing earlybird tree")
        self.fileSaveAction = self.createAction("&Save", slot = self.fileSave,
                shortcut = QtGui.QKeySequence.Save, tip = "Save file",
                status = "Save file")
        self.fileSaveAsAction = self.createAction("Save &As", slot = self.fileSaveAs,
                shortcut = QtGui.QKeySequence.SaveAs, tip = "Save file as", status = "Save file as")
        #Item actions
        self.undoAction = self.createAction("Undo", slot = self.view.undoStack.undo,
               shortcut = QtGui.QKeySequence.Undo, tip = "Undo",
               status = "Undo changes")    
        self.redoAction = self.createAction("Redo", slot = self.view.undoStack.redo,
               shortcut = QtGui.QKeySequence.Redo, tip = "Redo",
               status = "Redo changes")

    def createAction(self, text, slot=None, shortcut=None, 
                     tip=None, status = None):
        '''Function called to create each individual action'''
        action = QtGui.QAction(text, self)
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
        if status is not None:
            action.setStatusTip(status)
        if slot is not None:
            action.triggered.connect(slot)
        return action 

    def createStatusBar(self):                          
        self.status = self.statusBar()
        self.status.setSizeGripEnabled(False)
        self.status.showMessage("Ready")        

    def fileSaveAs(self):
        self.view.saveTodoDataAs()
        self.windowTitleSet()

    def fileSave(self):
        if self.view.filename:        
            self.view.saveTodoData()
        else:
            self.view.saveTodoDataAs()
            self.windowTitleSet()

    def fileOpen(self):
        '''Load earlybird file from memory.'''
        if self.view.loadEarlybirdFile():
            self.model = self.view.model  
            self.windowTitleSet()
            if self.view.filename:
                filenameNopath = QtCore.QFileInfo(self.view.filename).fileName()
                self.status.showMessage("Opened file: {0}".format(filenameNopath))

    def newFile(self):
        '''Opens new blank earlybird file'''
        self.view.newFile()
        self.windowTitleSet()

    def windowTitleSet(self):
        '''Displays filename as window title, if it exists.'''
        if self.view.filename:
            self.setWindowTitle("Earlybird - {}[*]".format(os.path.basename(self.view.filename)))
        else:
            self.setWindowTitle("Earlybird - <untitled>")


def main():   
    ebApp = QtGui.QApplication(sys.argv)
    mainEb = EarlybirdMain(filename = None)#"simpleTodo.eb"
    mainEb.show()
    undoView = QtGui.QUndoView(mainEb.view.undoStack)
    undoView.show()
    sys.exit(ebApp.exec_())


if __name__ == "__main__":
    main()

testFile.eb

代码语言:javascript
复制
{
  "taskblocks": [
    {
      "tasks": [
        {
          "done": false, 
          "name": "Rake leaves"
        }, 
        {
          "done": true, 
          "name": "Eat dinner"
        }
      ], 
      "blockname": "Home"
    }, 
    {
      "tasks": [
        {
          "done": false, 
          "name": "Analysis"
        }, 
        {
          "tasks": [
            {
              "done": false, 
              "name": "Start Github project"
            }, 
            {
              "done": true, 
              "name": "Write readme.md"
            }, 
            {
              "done": false, 
              "name": "Implement functions"
            }
          ], 
          "done": false, 
          "name": "Graphing project"
        }
      ], 
      "blockname": "Work"
    }
  ]
}

应用程序中最难理解的部分是撤销/重做功能。在最后决定上述策略之前,我已经了解到了已询问 问题的情况。

EN

回答 2

Code Review用户

发布于 2015-04-29 15:46:42

从您对我的评论的回答中,我将删除预期的Python3段(即使在最近的版本中有一些您可能喜欢的东西,比如取代旧的pathlib模块的新os.path ),并直接讲到要点:

Qt设计器是实现

的方法

编写用户界面代码是没有人喜欢做的事情。通过在Qt设计器中设计主UI并使用pyuic4pyside-uic生成无聊的代码,您可以认真地节省大量代码行,这样您就可以专注于代码的模型和控制部分。

Internationalization

虽然它可能并不重要,但它只需花费几个self.tr()就可以判断哪些字符串可以被翻译,即使您目前还没有设置任何翻译。但是,如果有一天你想要翻译所有的东西,你会很高兴不必重新阅读你的整个项目,以知道哪些字符串需要翻译。也就是说,许多要翻译的字符串在Qt生成的代码中自动标记为该字符串。

一致性是

的关键

阅读以下代码时:

代码语言:javascript
复制
def __init__(self, parent=None, filename = None):
    QtGui.QTreeView.__init__(self, parent=None)

有两件事让我印象深刻:

  • 你是否打算将parent转发给QTreeView.__init__?如果是这样的话,我恐怕这不是这里正在做的事情。
  • 以不一致的方式声明参数的默认值的方式。不过,我敢打赌,这是一个疏忽,因为大多数代码在样式方面似乎都是一致的。如果我们遵循PEP8 (我想我们应该这样做),我们应该删除默认参数=周围的空格: def __init__(self,parent=None,filename=None):QtGui.QTreeView.__init__(self,parent)
票数 5
EN

Code Review用户

发布于 2015-04-29 13:38:14

self.fileToolbar = self.addToolBar("File actions") self.fileToolbar.addAction(self.fileNewAction) self.fileToolbar.addAction(self.fileOpenAction) self.fileToolbar.addAction(self.fileSaveAction) self.fileToolbar.addAction(self.fileSaveAsAction) self.itemToolbar = self.addToolBar("Item actions") self.itemToolbar.addAction(self.undoAction) self.itemToolbar.addAction(self.redoAction) . self.fileMenu.addAction(self.fileOpenAction) self.fileMenu.addAction(self.fileNewAction) self.fileMenu.addAction(self.fileSaveAction) self.fileMenu.addAction(self.fileSaveAsAction)

你有一堆重复的代码。相反,最好是建立适当的数据结构,然后隐式地循环它们。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/88065

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档