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

这是我第一个使用Python和面向对象编程的应用程序.我通常是一个Matlab程序员。因此,对我所做的任何程度的反馈都是非常有帮助的。我即将将应用程序放在Github上,并添加了大量的特性,所以现在是向它扔一些炸药的最佳时机。
应用程序有两个主要类:
EarlybirdTree:核心树视图。这是来自QTreeView的子类,并定义了基本方法(加载和保存文件,向撤销堆栈添加已编辑的项,等等)。EarlybirdMain:EarlybirdTree对象的简单包装器。这是QMainWindow的子类,允许用户使用菜单和工具栏与树的方法交互。下面的代码包括上述两个py文件以及一个早期鸟类数据文件(testFile.eb)。我以json格式存储数据。为了方便起见,每个main函数还添加了一个单独的解堆栈视图。注意,为了防止这种情况发生,我还没有包括添加/删除项的功能以及许多其他的花哨功能。
# -*- 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()# -*- 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(){
"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"
}
]
}发布于 2015-04-29 15:46:42
从您对我的评论的回答中,我将删除预期的Python3段(即使在最近的版本中有一些您可能喜欢的东西,比如取代旧的pathlib模块的新os.path ),并直接讲到要点:
的方法
编写用户界面代码是没有人喜欢做的事情。通过在Qt设计器中设计主UI并使用pyuic4或pyside-uic生成无聊的代码,您可以认真地节省大量代码行,这样您就可以专注于代码的模型和控制部分。
虽然它可能并不重要,但它只需花费几个self.tr()就可以判断哪些字符串可以被翻译,即使您目前还没有设置任何翻译。但是,如果有一天你想要翻译所有的东西,你会很高兴不必重新阅读你的整个项目,以知道哪些字符串需要翻译。也就是说,许多要翻译的字符串在Qt生成的代码中自动标记为该字符串。
的关键
阅读以下代码时:
def __init__(self, parent=None, filename = None):
QtGui.QTreeView.__init__(self, parent=None)有两件事让我印象深刻:
parent转发给QTreeView.__init__?如果是这样的话,我恐怕这不是这里正在做的事情。=周围的空格: def __init__(self,parent=None,filename=None):QtGui.QTreeView.__init__(self,parent)发布于 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)
你有一堆重复的代码。相反,最好是建立适当的数据结构,然后隐式地循环它们。
https://codereview.stackexchange.com/questions/88065
复制相似问题