看起来像这样:
这是我的第一个使用Python的应用程序,通常是面向对象的编程。我通常是Matlab程序员。因此,对我所做的任何水平的反馈都将非常有帮助。我打算将应用程序放置在Github上,并添加大量功能,因此现在是向其添加炸药的最佳时机。
该应用程序有两个主要类:
EarlybirdTree
:核心树视图。这是QTreeView
的子类,并定义了基本方法(加载和保存文件,将已编辑的项目添加到撤消堆栈等)。。EarlybirdMain
:EarlybirdTree
对象的简单包装。这是QMainWindow
的子类,它允许用户使用菜单和工具栏与树的方法进行交互。以下代码包括以上两个
py
文件以及一个Earlybird数据文件(testFile.eb
)。我以json
格式存储数据。为了方便起见,每个main
函数还添加了一个单独的撤消堆栈视图。请注意,为防止这种情况发生,我没有包含用于添加/删除项目以及许多其他铃声的功能。earlybirdTree.py
# -*- 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
# -*- 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
{
"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"
}
]
}
最困难的部分找出/撤消/重做功能的应用程序。在最终确定上述策略之前,我已经在SO提出了三个问题。
#1 楼
从您的回答到我的评论,我将删除预期的Python 3段落(即使您在最新版本中可能喜欢一些东西,例如取代旧pathlib
模块的新os.path
),并直接讲到重点。 :Qt Designer是解决之道
编写用户界面代码是没人喜欢的事情。通过在Qt Designer中设计主UI并使用
pyuic4
或pyside-uic
生成无聊的代码,您可以认真节省大量代码行。因此,您可以专注于代码的“模型和控制”部分。国际化
尽管它可能并不重要,但即使您现在不设置任何翻译,它也不会花费多一些
self.tr()
来告诉您可以翻译哪些字符串。但是,如果有一天您想翻译所有内容,您将很高兴不必重新阅读整个项目来知道需要翻译哪些字符串。也就是说,Qt Designer生成的代码中会自动标记许多要翻译的字符串。关键是一致性
阅读以下代码时:
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)
评论
\ $ \ begingroup \ $
感谢您的分析。我在初始化时使用父级的目标是设置有问题的小部件的父级,将所有权传递给父级,这样(例如)当我关闭父级时,子级会自动关闭。 Wrt QtDesigner,尽管我有很多朋友以我为我取笑,但是我很喜欢手写代码。我敢肯定,这可以节省我的时间:以前我曾将它用于简单的事情,但从未爱上它。最后,我绝对应该复审PEP8,我可能经常违反它。
\ $ \ endgroup \ $
–神经网络
15年4月29日在19:27
\ $ \ begingroup \ $
@neuronet嗯,那太糟糕了,我可以肯定父问题是错字.___。我可以告诉您,您无论如何都不会违反PEP8 :)
\ $ \ endgroup \ $
–莫文
15年4月30日在7:27
\ $ \ begingroup \ $
现在您再次提到它,我认为我不需要第二个parent = None,因为它已经作为默认值传递给__init__。
\ $ \ endgroup \ $
–神经网络
15年4月30日在13:35
#2 楼
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)
你有一堆重复的代码。最好改成适当的数据结构,然后隐式地遍历它们。
评论
\ $ \ begingroup \ $
不幸的是,通常来说,对于UI代码来说,它不能很好地工作。通常情况下,事情已经足够令人讨厌了,但是当您尝试制作一些通用代码来处理它们时,事情又相去甚远,有太多特殊情况:/
\ $ \ endgroup \ $
–莫文
15年4月29日在14:03
\ $ \ begingroup \ $
是的,我认为您需要4次以上的重复操作,然后才能在操作中使用for操作保存任何行:menu.addAction(action)一种解决方案。如果您定期进行小批量操作,则可以创建一个辅助函数,但是我常常最终倾向于更靠近库并且不要过度抽象UI代码。
\ $ \ endgroup \ $
–flutefreak7
18-09-21在4:38
评论
您被迫使用Python 2.7还是可以使用更新的版本?我对Python 3过敏,但理论上可以使用它。 :)我进行了大量的科学计算,因此一直在等待Python 2直到一切都切换了。