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

原文链接 http://www.binpress.com/tutorial/developing-a-pyqt-text-editor-part-2/145

在我先前的《用PyQt构建一个文本编辑器(1)》中,我们创建了文本编辑器的基本骨架并且增加了一些有用的特性,例如文件管理、打印、插入列表等等。在这部分,我们将着重构建一个格式导航栏,它将由一系列特性构成,包括改变字体的actions、背景颜色、对齐等。

字体

我们由与字体有关的actions开始,这意味着用户将可以做以下事情:

  • 改变字体
  • 调整字号
  • 设置字体颜色
  • 选择背景颜色

现在开始写代码。就像上次一样,我将仅仅展示一些与先前代码相似的函数:
initFormatbar():

fontBox = QtGui.QFontComboBox(self)
fontBox.currentFontChanged.connect(self.fontFamily)

fontSize = QtGui.QComboBox(self)
fontSize.setEditable(True)

# Minimum number of chars displayed
fontSize.setMinimumContentsLength(3)

fontSize.activated.connect(self.fontSize)

# Typical font sizes
fontSizes = ['6','7','8','9','10','11','12','13','14',
'15','16','18','20','22','24','26','28',
'32','36','40','44','48','54','60','66',
'72','80','88','96']

for i in fontSizes:
fontSize.addItem(i)

fontColor = QtGui.QAction(QtGui.QIcon("icons/font-color.png"),"Change font color",self)
fontColor.triggered.connect(self.fontColor)

backColor = QtGui.QAction(QtGui.QIcon("icons/highlight.png"),"Change background color",self)
backColor.triggered.connect(self.highlight)

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

self.formatbar.addWidget(fontBox)
self.formatbar.addWidget(fontSize)

self.formatbar.addSeparator()

self.formatbar.addAction(fontColor)
self.formatbar.addAction(backColor)

self.formatbar.addSeparator()

接下来 initUI():

def fontFamily(self,font):
self.text.setCurrentFont(font)

def fontSize(self, fontsize):
self.text.setFontPointSize(int(fontsize))

def fontColor(self):

# Get a color from the text dialog
color = QtGui.QColorDialog.getColor()

# Set it as the new text color
self.text.setTextColor(color)

def highlight(self):

color = QtGui.QColorDialog.getColor()

self.text.setTextBackgroundColor(color)

注意到我们刚刚创建的这些actions与我上次描述的actions的代码模式不一样,我们没有创建这些actions的类成员,因为我们仅仅需要在initFormatbar()范围内创建并使用它们。我们也没有为它们添加工具提示和快捷键(当然,你也可以自己添加).

我们由创建一个QFontComboBox开始,这是一个非常方便的combo box,它可以把系统所有的字体自动的包括进来。我们实例化并且把currentFontChanged信号连接到我们将在initUI()方法中定义的槽函数self.fontFamily()。正如你看到的,我们同样给这个槽函数一个second parameter font,所以PyQt将会把用户选择的QFont对象传递给我们的函数,减少将这个字体设置为文本编辑器字体的工作。

接下来,我们需要一个字号的combo box。PyQt本身没有这个东西,所以我们需要自己创建一个。通过实例化一个普通的combo box(这里是fontSize)来解决这个问题是很容易的,我们把fontSize设置为可编辑,意味着用户可以设置任意他们想要的字号。在把这个信号与槽函数连接起来以后,我们为这个combo box添加一些常用的字号。对于这个槽函数,我们再一次设置一个second parameter,字号,当用户从combo box里面选择了一个字号时,PyQt将它传递给我们,或者,用户输入一个特定的字号。

最后两个actions非常相似。在两种情况下,我们创建两个actions,当actions被激活时打开一个QColorDialog对于fontColor,我们将选中的颜色设置为字体颜色。对于backColor,我们设置当前文本编辑器的背景颜色。

Bold Move

现在,我们为编辑器添加几个actions:

  • 加粗
  • 斜体
  • 下划线
  • 删除线
  • 上标
  • 下标 这部分的代码也是很简单的:

initFormatbar():

boldAction = QtGui.QAction(QtGui.QIcon("icons/bold.png"),"Bold",self)
boldAction.triggered.connect(self.bold)

italicAction = QtGui.QAction(QtGui.QIcon("icons/italic.png"),"Italic",self)
italicAction.triggered.connect(self.italic)

underlAction = QtGui.QAction(QtGui.QIcon("icons/underline.png"),"Underline",self)
underlAction.triggered.connect(self.underline)

strikeAction = QtGui.QAction(QtGui.QIcon("icons/strike.png"),"Strike-out",self)
strikeAction.triggered.connect(self.strike)

superAction = QtGui.QAction(QtGui.QIcon("icons/superscript.png"),"Superscript",self)
superAction.triggered.connect(self.superScript)

subAction = QtGui.QAction(QtGui.QIcon("icons/subscript.png"),"Subscript",self)
subAction.triggered.connect(self.subScript)

接下来:

self.formatbar.addAction(boldAction)
self.formatbar.addAction(italicAction)
self.formatbar.addAction(underlAction)
self.formatbar.addAction(strikeAction)
self.formatbar.addAction(superAction)
self.formatbar.addAction(subAction)

self.formatbar.addSeparator()

initUI()里面添加:

def bold(self):

if self.text.fontWeight() == QtGui.QFont.Bold:

self.text.setFontWeight(QtGui.QFont.Normal)

else:

self.text.setFontWeight(QtGui.QFont.Bold)

def italic(self):

state = self.text.fontItalic()

self.text.setFontItalic(not state)

def underline(self):

state = self.text.fontUnderline()

self.text.setFontUnderline(not state)

def strike(self):

# Grab the text's format
fmt = self.text.currentCharFormat()

# Set the fontStrikeOut property to its opposite
fmt.setFontStrikeOut(not fmt.fontStrikeOut())

# And set the next char format
self.text.setCurrentCharFormat(fmt)

def superScript(self):

# Grab the current format
fmt = self.text.currentCharFormat()

# And get the vertical alignment property
align = fmt.verticalAlignment()

# Toggle the state
if align == QtGui.QTextCharFormat.AlignNormal:

fmt.setVerticalAlignment(QtGui.QTextCharFormat.AlignSuperScript)

else:

fmt.setVerticalAlignment(QtGui.QTextCharFormat.AlignNormal)

# Set the new format
self.text.setCurrentCharFormat(fmt)

def subScript(self):

# Grab the current format
fmt = self.text.currentCharFormat()

# And get the vertical alignment property
align = fmt.verticalAlignment()

# Toggle the state
if align == QtGui.QTextCharFormat.AlignNormal:

fmt.setVerticalAlignment(QtGui.QTextCharFormat.AlignSubScript)

else:

fmt.setVerticalAlignment(QtGui.QTextCharFormat.AlignNormal)

# Set the new format
self.text.setCurrentCharFormat(fmt)

到现在为止,在initFormatbar()里面的变化是容易理解的了。我们创建actions兵器人把信号连接到槽函数,然后我们把actions添加到格式导航条中。

bold()中,我们改变现在文本字体的粗细。如果是粗体,我们把它设置为常规粗度。如果是常规粗度,我们把它设置为粗体。

对于italic()underline(),我们的QTextEdit对象有可以设置和获取文本状态的函数。因此,我们仅仅需要抓取现在文本的状态并且反转它。

strike()函数有一点点不同。我们获取现在文本的字符格式(currentCharFormat),反转fontStrikeOut属性的状态,并且把文本的字符格式设置为我们新的字符格式。

最后,在superScript()subScript()中,我们再次获取当前字符的格式,切换verticalAlignment属性(就像我们在bold()中做的一样),并且设置新的字符格式来使我们的改变显现。

对齐

对齐很简单,因为PyQt提供给我们这些必须的方法:

initFormatbar():

alignLeft = QtGui.QAction(QtGui.QIcon("icons/align-left.png"),"Align left",self)
alignLeft.triggered.connect(self.alignLeft)

alignCenter = QtGui.QAction(QtGui.QIcon("icons/align-center.png"),"Align center",self)
alignCenter.triggered.connect(self.alignCenter)

alignRight = QtGui.QAction(QtGui.QIcon("icons/align-right.png"),"Align right",self)
alignRight.triggered.connect(self.alignRight)

alignJustify = QtGui.QAction(QtGui.QIcon("icons/align-justify.png"),"Align justify",self)
alignJustify.triggered.connect(self.alignJustify)

接下来:

self.formatbar.addAction(alignLeft)
self.formatbar.addAction(alignCenter)
self.formatbar.addAction(alignRight)
self.formatbar.addAction(alignJustify)

self.formatbar.addSeparator()

在initUI()方法中添加:

def alignLeft(self):
self.text.setAlignment(Qt.AlignLeft)

def alignRight(self):
self.text.setAlignment(Qt.AlignRight)

def alignCenter(self):
self.text.setAlignment(Qt.AlignCenter)

def alignJustify(self):
self.text.setAlignment(Qt.AlignJustify)

initFormatbar()方法中的改变和先前的模式一样并且槽函数同样很简单。我们用QTextEdit中的setAlignment方法来改变文本的对齐方式,并传递一个QT命名空间中特定的成员给它,例如QT.AlignCenter

增加缩进 - 减少缩进

增加减少缩进有一点复杂,因为PyQt没有给我们提供有效的方法来调整已选中区域的tabble,意味着我们需要用自己的方法处理:

initFormatbar():

indentAction = QtGui.QAction(QtGui.QIcon("icons/indent.png"),"Indent Area",self)
indentAction.setShortcut("Ctrl+Tab")
indentAction.triggered.connect(self.indent)

dedentAction = QtGui.QAction(QtGui.QIcon("icons/dedent.png"),"Dedent Area",self)
dedentAction.setShortcut("Shift+Tab")
dedentAction.triggered.connect(self.dedent)

接下来:

self.formatbar.addAction(indentAction)
self.formatbar.addAction(dedentAction)

在initUI()中添加:

def indent(self):

# Grab the cursor
cursor = self.text.textCursor()

if cursor.hasSelection():

# Store the current line/block number
temp = cursor.blockNumber()

# Move to the selection's last line
cursor.setPosition(cursor.selectionEnd())

# Calculate range of selection
diff = cursor.blockNumber() - temp

# Iterate over lines
for n in range(diff + 1):

# Move to start of each line
cursor.movePosition(QtGui.QTextCursor.StartOfLine)

# Insert tabbing
cursor.insertText("\t")

# And move back up
cursor.movePosition(QtGui.QTextCursor.Up)

# If there is no selection, just insert a tab
else:

cursor.insertText("\t")

def dedent(self):

cursor = self.text.textCursor()

if cursor.hasSelection():

# Store the current line/block number
temp = cursor.blockNumber()

# Move to the selection's last line
cursor.setPosition(cursor.selectionEnd())

# Calculate range of selection
diff = cursor.blockNumber() - temp

# Iterate over lines
for n in range(diff + 1):

self.handleDedent(cursor)

# Move up
cursor.movePosition(QtGui.QTextCursor.Up)

else:
self.handleDedent(cursor)
def handleDedent(self,cursor):

cursor.movePosition(QtGui.QTextCursor.StartOfLine)

# Grab the current line
line = cursor.block().text()

# If the line starts with a tab character, delete it
if line.startswith("\t"):

# Delete next character
cursor.deleteChar()

# Otherwise, delete all spaces until a non-space character is met
else:
for char in line[:8]:

if char != " ":
break

cursor.deleteChar()

initFormatbar()中的变化和先前讨论的一样。

我们来一步一步的看indent()函数。首先,我们要获取当前文本的QTextCursor对象。我们检查当前用户是否选择了一些文本。如果没有的话,我们仅仅插入一个tab。如果选择了一些文本,我们的处理有点特别。我们需要找出用户一共选择了多少行,并且在每一行开头插入一个tab。

我们获取已选中文本首行的行号,然后移动光标到选中文本的末尾,获取尾行号,并用此减去首行号。这为我们之后的迭代提供了行范围。对于每一次迭代,我们把光标移动到行首,插入一个tab,并向上移动一行,直到我们到达行首。(需要注意的是,在我们开始迭代之前,我们为了找出选中文本的尾行号,我们已经将光标移动到了末尾。)

取消缩进的dedent()方法和它十分相似,不同之处在于,我们不仅要处理tab,还需要处理多余的空格。这就是handleDedent()的目的。我们在获取当前行文本之后,再次将光标置于每行的开头。如果行以tab开头,我们仅仅删除它就完成了我们的工作。如果不是,我们需要检查是否有多余的空格(最多8个空格,这与一个tab相同),如果有多余的空格就删除。这确保了两件事情:

-照顾那些相比用tab而更喜欢用8个空格的人。
-删除那些会烦恼到你的多余的空格。

其它个性化选项

现在,我们的工具栏、格式栏和状态栏已经具备了,我们可以增加一些个性化选项来切换这三种菜单的可视与否:

initMenubar():

# Toggling actions for the various bars
toolbarAction = QtGui.QAction("Toggle Toolbar",self)
toolbarAction.triggered.connect(self.toggleToolbar)

formatbarAction = QtGui.QAction("Toggle Formatbar",self)
formatbarAction.triggered.connect(self.toggleFormatbar)

statusbarAction = QtGui.QAction("Toggle Statusbar",self)
statusbarAction.triggered.connect(self.toggleStatusbar)

view.addAction(toolbarAction)
view.addAction(formatbarAction)
view.addAction(statusbarAction)

initUI()中添加:

def toggleToolbar(self):

state = self.toolbar.isVisible()

# Set the visibility to its inverse
self.toolbar.setVisible(not state)

def toggleFormatbar(self):

state = self.formatbar.isVisible()

# Set the visibility to its inverse
self.formatbar.setVisible(not state)

def toggleStatusbar(self):

state = self.statusbar.isVisible()

# Set the visibility to its inverse
self.statusbar.setVisible(not state)

我们在initMenubar()方法中创建了三个actions...
toolbarAction
formatbarAction
statusbarAction
...并且把他们与槽函数连接。注意,我们没有把这些actions加到任何工具栏中,而是仅仅加到了屏幕上方的下拉菜单中。

在槽函数中,我们做之前写格式函数一样的事情:我们反转不同导航条的可视状态,并且把他们设置为相反状态。

这是这部分的全部内容,期待我们即将到来的《用PyQt构建一个文本编辑器》的最后一章,这部分我们将会增加一些有趣的查找和替换、插入图片以及其他actions。