文是原文的中文翻译, 并加入了自己的补充和修改, 并用<补充>标明

原文链接 http://www.binpress.com/tutorial/building-a-text-editor-with-pyqt-part-one/143

我一直喜欢对后台程序构建美观的GUI界面. 对于python, 我选择的GUI库是PyQT. 这个教程将指导你使用PyQT构建一个简单实用的富文本编辑器. 在第一阶段, 我们将会实现核心功能和程序骨架, 第二阶段, 我们会实现自动排版, 第三阶段, 我们将会添加一些有用的扩展, 如查找替换, 对于表格的支持等. 在这个系列的结尾我们将会看到下图的程序.

开发环境<补充>:

  • ubuntu 14.04 LTS
  • python2.7
sudo apt-get install Python-bs4 libxext6 libxext-dev libqt4-dev libqt4-gui libqt4-sql qt4-dev-tools qt4-doc qt4-designer qt4-qtconfig "python-qt4-*" python-qt4

新建一个空的canvas

我们从一个空的canvas开始, 实现一个最小的qt应用.

import sys
from PyQT4 import QtGui, QtCore
from PyQT4.QtCore import Qt

class Main(QtGui.QMainWindow):

def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self,parent)

self.initUI()

def initUI(self):

# x and y coordinates on the screen, width, height
self.setGeometry(100,100,1030,800)

self.setWindowTitle("Writer")

def main():

app = QtGui.QApplication(sys.argv)

main = Main()
main.show()

sys.exit(app.exec_())

if __name__ == "__main__":
main()

我们需要做的第一件事是导入sys模块, 用于启动我们的程序, 以及从PyQT库中导入所有必要的模块. 我们会调用一个叫main的类, 并让它继承于PyQT的QMainWindow类, 在__init__方法中, 我们初始化这个父类, 并进行UI的设置, 然后我们仅仅把它封装成initUI()方法. 这时, 我们需要做的设置只有屏幕上的相关位置, 窗口大小, 和窗口的标题. 对于前两个, 我们可以使用setGeometry()方法设置想x, y的坐标和窗口的宽度和高度. 我们还可以使用setWindowTitle()方法设置窗口的标题. 为了简单, 我们就把我们的标题叫做Writer.

接下来是文本

既然我们已经有了一个简单的PyQT应用, 我们开始让它看上去更像一个文本编辑器.

def initToolbar(self):

self.toolbar = self.addToolBar("Options")

# Makes the next toolbar appear underneath this one
self.addToolBarBreak()

def initFormatbar(self):

self.formatbar = self.addToolBar("Format")

def initMenubar(self):

menubar = self.menuBar()

file = menubar.addMenu("File")
edit = menubar.addMenu("Edit")
view = menubar.addMenu("View")

def initUI(self):

self.text = QtGui.QTextEdit(self)
self.setCentralWidget(self.text)

self.initToolbar()
self.initFormatbar()
self.initMenubar()

# Initialize a statusbar for the window
self.statusbar = self.statusBar()

# x and y coordinates on the screen, width, height
self.setGeometry(100,100,1030,800)

self.setWindowTitle("Writer")

我省略了所有我没改变的代码. 正如你所看到的, 在initUI()中, 我们首先创建了一个QTextEdit对象, 并且将它设置为窗口的中心物件(central widget), 这会使QTextEdit对象占据整个窗口的空间. 下一步, 我们需要创建三个方法:initToolbar(), initFormatbar() and initMenubar(). 前面两个方法创建了工具栏(toolbars), 并使他出现在窗口的最上方, 这个工具栏包含了文本编辑器的一些功能, 例如文件管理(打开文件, 保存文件), 文本格式化. 最后一个方法, initMenubar(), 在窗口最上方创建了一组下拉菜单.

截至目前, 这些方法只包含显示的代码. 对于initToolbar()和initFormatbar()方法, 他的意思是通过调用addToolBar()方法创建一个新的toolbar对象, 并且把名字的参数传递进去. 记得在initToolbar()方法中, 我们需要调用addToolBarBreak()方法, 这会使下一个toolbar(format bar)显示在toolbar的下面. 因为有menubar,我们调用menuBar()方法并且添加三个菜单进去, 分别是"File", "Edit"和"View". 一会我们会填充这些toolbar和menu.

最后, 在initUI()方法中, 我们还创建了一个状态栏(status bar). 他会显示在窗口最下方.

图标

在我们向文本编辑器中注入活力之前, 我们需要一些代表不同功能的图标. 如果你已经看过Github的仓库, 你会注意到一个全是图标的文件夹. 我建议你下载这个仓库(如果你还没有), 并且将图标的文件夹复制到你的工作目录. 这些图标来自于iconmonstr, 完全免费, 并且不需要表明出处.

文件管理

既然我们已经有了一个基本骨架, 现在我们可以填充一些内容了. 我们开始实现文件管理的功能.

__init__():

def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self,parent)

self.filename = ""

self.initUI()

initToolbar():

def initToolbar(self):

self.newAction = QtGui.QAction(QtGui.QIcon("icons/new.png"),"New",self)
self.newAction.setStatusTip("Create a new document from scratch.")
self.newAction.setShortcut("Ctrl+N")
self.newAction.triggered.connect(self.new)

self.openAction = QtGui.QAction(QtGui.QIcon("icons/open.png"),"Open file",self)
self.openAction.setStatusTip("Open existing document")
self.openAction.setShortcut("Ctrl+O")
self.openAction.triggered.connect(self.open)

self.saveAction = QtGui.QAction(QtGui.QIcon("icons/save.png"),"Save",self)
self.saveAction.setStatusTip("Save document")
self.saveAction.setShortcut("Ctrl+S")
self.saveAction.triggered.connect(self.save)

self.toolbar = self.addToolBar("Options")

self.toolbar.addAction(self.newAction)
self.toolbar.addAction(self.openAction)
self.toolbar.addAction(self.saveAction)

self.toolbar.addSeparator()

# Makes the next toolbar appear underneath this one
self.addToolBarBreak()

initMenubar():

file.addAction(self.newAction)
file.addAction(self.openAction)
file.addAction(self.saveAction)

initUI() 方法中添加:

def new(self):

spawn = Main(self)
spawn.show()

def open(self):

# Get filename and show only .writer files
self.filename = QtGui.QFileDialog.getOpenFileName(self, 'Open File',".","(*.writer)")

if self.filename:
with open(self.filename,"rt") as file:
self.text.setText(file.read())

def save(self):

# Only open dialog if there is no filename yet
if not self.filename:
self.filename = QtGui.QFileDialog.getSaveFileName(self, 'Save File')

# Append extension if not there yet
if not self.filename.endswith(".writer"):
self.filename += ".writer"

# We just store the contents of the text file along with the
# format in html, which Qt does in a very nice way for us
with open(self.filename,"wt") as file:
file.write(self.text.toHtml())

正如你注意到的, 我们为文本编辑器所创建的actions都遵循以下的代码模式:

  • 创建一个QAction并且为它传递一个图标和名字
  • 创建一个状态提示, 它会在状态栏显示一段信息, 或者当鼠标掠过这个action时显示一个工具提示
  • 创建一个快捷键
  • 将一个QAction信号与槽函数连接

一旦你对new、open和save完成了以上工作, 你可以用toolbar里的addAction()方法将他们添加到工具栏中. 同时确保你使用了addSeparator()方法, 它在不同的toolbar actions之间使用分割线. 因为这三个actions意味着文件管理, 我们在这儿添加一个分割. 同样的, 我们想将这三个actions添加进"file"菜单, 所以在initMenubar()方法中, 我们在适当的位置添加这三个actions.

下一步, 我们需要在initToolbar()方法中, 创建三个槽函数连接到我们的actions. new()方法很简单, 我们要做的只是创建一个窗口实例, 并调用show()方法去显示它.

在我们创建最后两个方法前, 让我们注意我们需要使用".writer"作为我们文件的拓展名.

现在, 对于open(),我们需要创建PyQT的getOpenFileName对话框. 这会打开一个文件对话框, 并返回用户所打开的文件的文件名. 这里的参数有三个, 第一个是我们要传递对话框的标题(这个例子中是"Open File"),第二个是打开的目录(我们这里是"."(当前目录)), 最后一个是文件过滤(我们这里只显示".writer"文件). 如果用户选择了文件, 我们就打开这个文件并把它的文本显示在当前的文本编辑框中.

最后, 是save()方法. 我们首先检查当前文件是否已经关联了一个文件, 因为可能这个文件是用open()方法打开或者之前已经保存过. 如果是一个新的文本文件, 我们打开getSaveFileName对话框, 这也会返回一个文件名. 一旦我们有了文件名, 我们需要检查用户是否输入了拓展名. 如果没有, 我们加上拓展名. 最后, 我们用QTextEdit的toHTML()方法将文件保存为HTML格式.

打印

接下来, 我们会创建一些action用于打印和预览我们的文档.

initToolbar():

self.printAction = QtGui.QAction(QtGui.QIcon("icons/print.png"),"Print document",self) self.printAction.setStatusTip("Print document")
self.printAction.setShortcut("Ctrl+P")
self.printAction.triggered.connect(self.print)

self.previewAction = QtGui.QAction(QtGui.QIcon("icons/preview.png"),"Page view",self) self.previewAction.setStatusTip("Preview page before printing") self.previewAction.setShortcut("Ctrl+Shift+P") self.previewAction.triggered.connect(self.preview)

接下来:

self.toolbar.addAction(self.printAction)
self.toolbar.addAction(self.previewAction)

self.toolbar.addSeparator()

initMenubar():

file.addAction(self.printAction)
file.addAction(self.previewAction)

initUI() 方法中添加:

def preview(self):

# Open preview dialog
preview = QtGui.QPrintPreviewDialog()

# If a print is requested, open print dialog
preview.paintRequested.connect(lambda p: self.text.print_(p))

preview.exec_()

def print(self):

# Open printing dialog
dialog = QtGui.QPrintDialog()

if dialog.exec_() == QtGui.QDialog.Accepted:
self.text.document().print_(dialog.printer())

我们创建这些action的思路和文件管理是一样的, 把他们加入工具栏和"file"菜单. preview()方法打开一个QPrintPreviewDialog. print()方法打开了QPrintDialog, 根据用户设置打印文档.

复制/粘贴 - 撤销/重做

这些action会让我们复制, 剪切, 粘贴, 还有撤销/重做:

initToolbar():

self.cutAction = QtGui.QAction(QtGui.QIcon("icons/cut.png"),"Cut to clipboard",self)
self.cutAction.setStatusTip("Delete and copy text to clipboard")
self.cutAction.setShortcut("Ctrl+X")
self.cutAction.triggered.connect(self.text.cut)

self.copyAction = QtGui.QAction(QtGui.QIcon("icons/copy.png"),"Copy to clipboard",self)
self.copyAction.setStatusTip("Copy text to clipboard")
self.copyAction.setShortcut("Ctrl+C")
self.copyAction.triggered.connect(self.text.copy)

self.pasteAction = QtGui.QAction(QtGui.QIcon("icons/paste.png"),"Paste from clipboard",self)
self.pasteAction.setStatusTip("Paste text from clipboard")
self.pasteAction.setShortcut("Ctrl+V")
self.pasteAction.triggered.connect(self.text.paste)

self.undoAction = QtGui.QAction(QtGui.QIcon("icons/undo.png"),"Undo last action",self)
self.undoAction.setStatusTip("Undo last action")
self.undoAction.setShortcut("Ctrl+Z")
self.undoAction.triggered.connect(self.text.undo)

self.redoAction = QtGui.QAction(QtGui.QIcon("icons/redo.png"),"Redo last undone thing",self)
self.redoAction.setStatusTip("Redo last undone thing")
self.redoAction.setShortcut("Ctrl+Y")
self.redoAction.triggered.connect(self.text.redo)

接下来:

self.toolbar.addAction(self.cutAction)
self.toolbar.addAction(self.copyAction)
self.toolbar.addAction(self.pasteAction)
self.toolbar.addAction(self.undoAction)
self.toolbar.addAction(self.redoAction)

self.toolbar.addSeparator()

initMenubar():

edit.addAction(self.undoAction)
edit.addAction(self.redoAction)
edit.addAction(self.cutAction)
edit.addAction(self.copyAction)
edit.addAction(self.pasteAction)

正如你看到的, 我们不需要任何额外的槽函数, 因为QTextEdit对象已经有非常便利的方法. 记得在initMenubar()方法中, 我们把这些action加到"Edit"菜单和"File"菜单.

列表

最后, 我们加入两个actions用来插入列表. 一个是numberList, 另一个是bulletList.

initToolbar():

bulletAction = QtGui.QAction(QtGui.QIcon("icons/bullet.png"),"Insert bullet List",self)
bulletAction.setStatusTip("Insert bullet list")
bulletAction.setShortcut("Ctrl+Shift+B")
bulletAction.triggered.connect(self.bulletList)

numberedAction = QtGui.QAction(QtGui.QIcon("icons/number.png"),"Insert numbered List",self)
numberedAction.setStatusTip("Insert numbered list")
numberedAction.setShortcut("Ctrl+Shift+L")
numberedAction.triggered.connect(self.numberList)

然后:

self.toolbar.addAction(bulletAction)
self.toolbar.addAction(numberedAction)

initUI() 方法中添加:

def bulletList(self):

cursor = self.text.textCursor()

# Insert bulleted list
cursor.insertList(QtGui.QTextListFormat.ListDisc)

def numberList(self):

cursor = self.text.textCursor()

# Insert list with numbers
cursor.insertList(QtGui.QTextListFormat.ListDecimal)

你可以看到, 我们没有让这些action成为类的成员, 因为我们在类的其他地方不需要使用他们. 我们只需要在initToolbar()内创建和使用他们.

关于槽函数, 我们有QTextEdit的QTextCursor, 它有很多有用的方法, 例如insertList(). 对于bulletList(),我们通过QTextListFormat.ListDisc插入. 对于numberList(), 通过QTextListFormat.ListDecimal插入.

最后的修改

为了完成我们的 用PyQT构建一个文本编辑器 的第一章, 我们最后修改一下initUI()方法.

因为PyQT的tab宽度很奇怪, 我建议你重新设置QTextEdit的tab缩进宽度. 我的观点是, 8个空格, 33像素左右(这可能与你习惯不同).

self.text.setTabStopWidth(33)

我们再给窗口加个图标:

self.setWindowIcon(QtGui.QIcon("icons/icon.png"))

通过绑定QTextEdit的cursorPositionChanged信号到一个函数, 我们可以在状态栏中显示当前光标所在行数和列数

self.text.cursorPositionChanged.connect(self.cursorPosition)

initUI()后面添加:

def cursorPosition(self):

cursor = self.text.textCursor()

# Mortals like 1-indexed things
line = cursor.blockNumber() + 1
col = cursor.columnNumber()

self.statusbar.showMessage("Line: {} | Column: {}".format(line,col))

我们首先获得QTextEdit的QTextCursor, 然后将行列数显示在状态栏中.

以上就是这一系列的第一章. 下一章将是文本格式化!