翻译:《用python和Qt进行GUI编程》——第五章(翻译中)

对话框

基本上任何一个GUI程序都有至少一个对话框,多数的GUI程序又一个主窗口和一沓对话框。当某些重要消息放在状态栏或者日志文件里显得略微不醒目的话,对话框可以对用户声明这个消息。这种情况下,这些对话框通常只有一个用于显示信息的label,以及供用户点击确定的OK按钮。大部分对话框是用来对用户提问题的。另外一些对话框是用来给用户提供某些选择的——例如,哪个文件,文件夹,颜色,字体,他们希望选择使用。以上的这些东西,PyQt都提供了内置的对话框支持。

我们这一章的目的是创建自定义对话框,这样当我们询问用户需求的时候就不怕内置对话框不合适了。

又一个问题我们要声明一下,对于本章的学习,我们并不特别选择对适合和的widget,例如,当我们希望用户对3个选项做出选择的时候,我们可以提供3个单选按钮,或者一个有3个元素的下拉列表,或者一个有3个元素的checkbox。这并不是所有的可能性。对于GUI编程的新手,附录B提供了PyQt某些widget的简短描述以及截图。

Qt提供了Qt Designer这个可视化的设计工具,可以让我们直接拖动widget实现布局,使我们加速开发进度。他也可以设置某些对话框的行为。在第七章我们介绍Qt Designer。在这一章,我们创造的对话框都是使用代码产生的。有些程序员喜欢使用代码,另外的可能喜欢用Qt Designer。本书两种方法都进行讲解,那么你就可以选择你更喜欢的编程方式。

一种划分对话框的方式是按照他们的“智能程度”,分为”dumb“,"标准型","聪慧型",划分标准是他们是如何通过自己的数据改变应用程序行为的。按照这种划分依据,我们对每一种对话框都提供了一小节来介绍。每一节开始是介绍这种分来,并且通过具体的例子在解释支持者与反对者的理由。

除了智能划分外,对话框还可以被划为为模态与非模态两种。模态对话框又分程序模态与窗口模态两种,程序模态是指,一旦调用,用户不能与程序的其他部分进行交互,除非用户关闭了对话框,否则用户不能使用程序的其他部分。不过用户可以与其他的应用程序进行交互。

窗口模态与程序模态很相似,他阻止的是与对话框的父窗口与祖先窗口的交互。对只有一个顶级窗口的程序来说,窗口模态与程序模态没神马区别。当我们指明”模态“窗口的时候,如果没有太多的说明,默认指窗口模态。

与之对应的是非模态对话框。当这种对话框调用的时候,用户可以与对话框以及程序的其他部分交互。这会影响到我们如何设计我们的代码,因为用户可以从对话框与主窗口两边更改我们程序的状态,有可能造成数据的不一致。

另一个重要的方面是我们如果进行验证。尽可能的选择适合的widget去设置相关的属性,而不要自己手写验证代码。例如,当我们需要一个整数的时候,我们可以选择QSpinBox并且使用它的setRange()方法去将数值设置为我们需要的范围。对于widget型的验证,我们称之为"widget级"验证;数据库程序员通常称之为”field(属性)级“验证。有时候我们可能需要更加高级的验证,尤其是存在依赖关系的时候。例如:电影院订票系统可能有两个下拉框,一个是行一个是列,如果行是A-R,列是M-T,那么通常情况下是只有某些特定的行列组合才是正确的。这种情况写我们必须编写'form级别'的验证,数据库程序员通常称之为"record(记录)级别"验证。

另一个验证的问题是何时进行验证。理想的情况下,用户根本无法输入不正确的数据,有时候确实可以通过一些技巧来实现。我们将验证分为两种:”post-mortem“,这种验证时刻是用户想要希望他们设置的数据被程序接受的时候;"preventative(预防性)",这是在用户编辑的时候进行的(编辑widget)。

因为对话框有不同等级的智能,3种模态方式以及几种认证策略,他们的组合有多种选择方式。在实际应用中,我们的选择基本每次都差不多。例如,大多数情况下我们选择dumb与标准型模态对话框,聪明型非模态对话框。对于验证,只能看具体情况选择适合的验证方案。本章我们将看到大部分使用时的可能性,本书剩下的章节我们会见到各种各样的对话框。

Dumb 对话框

我们为"dumb"对话框做如下定义:这个对话框的中widget的初始值由对话框的调用者决定,最终值直接取自对话框中的widget,也是受调用者所影响。"dumb"对话框对于widget中现在编辑的数据没有任何感觉。我们可以在"dumb"对话框中应用一些基本的验证方法,不过对一些相互依赖的widget添加验证通常可能不行,换言之,form级别的验证方案在"dumb"对话框中通常是没有的。"dumb"对话框通常是模态对话框,有一个接受(OK)和拒绝(Cancel)按钮。

使用"dumb"对话框最主要的优点是简单,我们并不需要通过API来写多余的代码,之所以有这些好处是因为他们的widget都是public的,可以直接获得。最大的不好之处是,因为我们通过widget来访问数据,因此代码与界面链接在一起,我们很难写出复杂的验证方法,并且如果要多次应用这个对话框的话,"dumb"对话框不如标准对话框以及聪明对话框方便。

我们从一个具体的例子开始。假设我们有一个画图的应用程序,我们想要用户可以设置钢笔的属性,例如钢笔的鼻头宽度,样式,或者说画线的时候有没有一些beveled edges。

在此输入图片描述

在这个例子中,我们不需要“live”(一边设置,钢笔属性立即变化),所以一个模态对话框就足够了。由于验证方法很简单,所以我们使用"dumb"对话框对可以。

我们可以设置一个槽来调用这个对话框,将这个槽链接在一个菜单项,工具栏,或者是一个按钮上,当用户点击的时候,弹出我们的模态对话框。如果用户点击OK,我们就更新我们的钢笔属性,如果用户点击取消,我们就神马也不做。下面是我们的槽:

def setPenProperties(self):
    dialog = PenPropertiesDlg(self)
    dialog.widthSpinBox.setValue(self.width)
    dialog.beveledCheckBox.setChecked(self.beveled)
    dialog.styleComboBox.setCurrentIndex(
            dialog.styleComboBox.findText(self.style))
    if dialog.exec_():
        self.width = dialog.widthSpinBox.value()
        self.beveled = dialog.beveledCheckBox.isChecked()
        self.style = unicode(dialog.styleComboBox.currentText())
        self.updateData()

开始时,我们创建了一个PenPropertiesDlg对话框,稍后我们会见到这个对话框的细节,现在我们只需要知道他有一个width的spinbox,一个beveled的checkbox,以及一个style的combobox。我们将self作为对话框的parent,这样我们就能得到一些优点,PyQt会将对话框放置在他parent的中央,并且在taskbar里不会添加一个新的入口(不解?)。我们之后直接来使用其中的widget,将他们的值传递地调用的form中去。QComboBox.findText()方法返回一个对应文本的下标地址。

当我们对对话框调用exec_()方法时,对话框将会以模态来显示。这意味着对话框的父窗口以及兄弟窗口都会被阻塞,直到这个对话框关闭位置。只用当用户关闭对话框(点击确定或者取消)时,exec_()方法才会返回,如果用户点击确定的话,返回值为True,否则为False。如果用户点击确定,那么我们知道用户想要应用他们的设置,所以我们读取对话框的数据中的数据,然后调用updateData()方法去更新我们程序中的画笔。

setPenProperties()方法最后PenPropertiesDlg将会超出这个方法的范围而变成一个垃圾。所以,每次调用这个方法的时候,我们都需要仙剑一个对话框,然后弹出里面的widget。对于一个像这样的小对话框,上面的方法是可以的,不过之后我们将会看到另一种可以选择的方法,来避免每次都要新建和销毁对话框。

使用"dumb"对话框意味着对话框于程序的耦合度很低。我们可以将label作为一个变量,然后我们可以使用PenPropertiesDlg类来编辑任何需要一个spinbox,一个checkbox以及一个combobox的对话框,我们要做的仅仅是改变对应的label。例如一个天气有关的程序,可能需要“Temperature” spinbox,一个 “Is raining” checkbox, 以及一个“Cloud cover”combobox。

现在我们意见看到如何建立对话框了,接下来看看我们是怎么实现这个对话框的。这个对话框只有一个__init__()方法。

class PenPropertiesDlg(QDialog):

    def __init__(self, parent=None):
        super(PenPropertiesDlg, self).__init__(parent)

没有什么特别奇怪的地方,我们的对话框是QDialog的子类,我们使用我们之间见过的方式来初始化他。

        widthLabel = QLabel("&Width:")
        self.widthSpinBox = QSpinBox()
        widthLabel.setBuddy(self.widthSpinBox)
        self.widthSpinBox.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
        self.widthSpinBox.setRange(0, 24)
        self.beveledCheckBox = QCheckBox("&Beveled edges")
        styleLabel = QLabel("&Style:")
        self.styleComboBox = QComboBox()
        styleLabel.setBuddy(self.styleComboBox)
        self.styleComboBox.addItems(["Solid", "Dashed", "Dotted",
                                     "DashDotted", "DashDotDotted"])
        okButton = QPushButton("&OK")
        cancelButton = QPushButton("Cancel")

对于每一个可以编辑的widget,我们创建了一个与之对应的label,已告知用户编辑的是哪个属相。当我们在labe中放入一个(&)符号的时候,他有两种意义。一时可能仅仅只是字面上的&。另一种意思,这个&不会被显示不过紧跟的字面后面会有一个下划线。例如,在本例的widthLabel中,他的文本是"&Width:",这将会显示为 Width:(w底下应该有条下滑线),那么他对应这个快捷键Alt+W.在Mac Os X上,默认行为是忽视这个快捷键,因此,PyQt不会再这种平台上显示下划线。 字面上&于快捷键&的区别在于,快捷键版的&对应的label有一个“伙伴”。当快捷键按下的时候,PyQt会将键盘的焦点移到对应的伙伴widget上去。所以当用户按下Alt+W的时候,键盘的焦点会转换到widthSpinBox上去。这个转变意味着用户按下上下键或者PageUp、PageDown键的时候,会影响到widthSpinBox的值。

对于一个按钮来说,按钮文字上的下滑线对应着一个快捷键。所以在这个例子中,okButton的文字是,"&OK",显示为“OK”(O线面有下划线),用户可以通过鼠标,Tab键盘,快捷键Alt+O按下这个按钮。通常情况下,不需要给Cancel或者Close按钮设置快捷键,因为通常情况下这都与对话框的reject()槽链接在一起,并且QDialog提供了Esc快捷键。CheckBox和 radio button提供了于按钮类似的快捷键机制。例如,beveled checkbox有一个有下划线的B,快捷键为Alt+B。

不过像这样建立按钮(OK于Cancel)有个不好之处,当将他们布局的时候,我们需要一个特殊的顺序。例如,我们可能把OK放在Cancel的左边,不过在Windows中这个顺序是错误的(相反)。PyQt提供了一个解决方案,就是Dialog Button Layout。

我们将spinbox的数字右对齐,垂直居中,并且将其范围设置为0~24.在PyQt中,钢笔的width中0代表1 pix。

通过设置spinbox的取值范围,我们避免了pen的错误取值。通常,我们需要做的是选择合适的widget,并且设置合适的属性,这就提供了widget级别的验证。这样,对于我们的beveled checkbox来说,钢笔只能画出带有beveled边或者不带的。同理,线条样式的combobox也是用了相同的验证方式。

        buttonLayout = QHBoxLayout()
        buttonLayout.addStretch()
        buttonLayout.addWidget(okButton)
        buttonLayout.addWidget(cancelButton)
        layout = QGridLayout()
        layout.addWidget(widthLabel, 0, 0)
        layout.addWidget(self.widthSpinBox, 0, 1)
        layout.addWidget(self.beveledCheckBox, 0, 2)
        layout.addWidget(styleLabel, 1, 0)
        layout.addWidget(self.styleComboBox, 1, 1, 1, 2)
        layout.addLayout(buttonLayout, 2, 0, 1, 3)
        self.setLayout(layout)

为了得到我们想要的布局,我们使用了两个layout,一个内嵌在另一个中 。开始时,我们将button水平放置,并且添加了一个“stretch”。stretch会消耗尽量多的空白,这样就可以将两个按钮尽可能的放在右边,并且空间上依然适合。

在此输入图片描述

        self.connect(okButton, SIGNAL("clicked()"),
                     self, SLOT("accept()"))
        self.connect(cancelButton, SIGNAL("clicked()"),
                     self, SLOT("reject()"))
        self.setWindowTitle("Pen Properties")

__init__()方法最后我们进行必要的连接。我们将OK按钮的clicked()信号连接到对话框的accept()槽上:这个槽将会关闭对话框并且返回一个True。cancel对话框也一样。最后,我们设置标题。

对于一个小型的"dumb"对话框来说,他可能仅仅从一个地方调用,我们可以不建立一个dialog类。这样的话,我们仅仅将需要调用的widget放在调用的方法中,将这些widget布局,进行适合的连接,然后调用exec_()方法。如果该方法返回True,我们就从widget中取回数据。在文件 chap05/pen.pyw中包涵了本章街上的方法,而其中的setPenInline()方法则是本段所介绍的方法。

"dumb"对话框容易理解和使用,对于一个只有几个值需要setting与getting的对话框来说,是很容易的方法。我们首先演示这是对话框是对dialog做一个介绍——如何创建、布局、连接widget的方法。下一节我们将介绍standart对话框,包括模态与非模态两种。

Standard对话框

我们定义一个Standard对话框是这样的,他的初始值是由他的构造方法决定的,最终值是根据调用的方法或者是实例的变量来决定的,不是直接通过dialog的widget。一个Standard对话框可以拥有widget级别以及form级别的验证。Standard对话框可以是模态的,包括accept按钮与reject按钮,或者是非模态的,包括apply按钮与close按钮,通过连接更改对话框的状态。

使用Standard对话框的好处是,调用者不需要知道他是如何实现的,只需要知道如何设定他的初始值,以及当用户点击OK时怎样得到得到结果的值。另一个优点是,至少对于模态的是这样,用户不能与对话框的其他部分交互,保证了在对话框后面程序的状态不会改变。主要的缺点是,当需要处理大量不同的数据项时,可能需要很对行代码。

像之前的一章,我们将会通过例子来讲解。这个例子在这一节和下一节都会用到,我们会看到Standard对话框与smart对话框的不同之处。

我们假设我们的程序需要通过表格的形式展示一些浮点型数据,而我们想给予用户一些控制浮点型样式的能力。有几种可以实现的方法是使用菜单项,toolbar按钮或者是一个可以调用设置浮点样式对话框的快捷键。下图展示了一个浮点样式对话框。

在此输入图片描述

我们希望用户可以控制对话框的数据项实在主form中初始化的。

    self.format = dict(thousandsseparator=",", decimalmarker=".",
            decimalplaces=2, rednegatives=False)

这样使用字典是很方便的,并且容易添加元素。

我们将对话框放入他们各自的文件中,numberformatdlg1.py,numbers.pyw通过import将他们导入。数字1是区分下一节中另外两个版本的对话框。

模态 OK/Cancel型对话框

我们从对话框是如何使用入手,我们假定setNumberFormat1()方法处理某些用户操作。

def setNumberFormat1(self):
    dialog = numberformatdlg1.NumberFormatDlg(self.format, self)
    if dialog.exec_():
        self.format = dialog.numberFormat()
        self.refreshTable()

我们首先创建了一个对话框,将format字典与self当参数传入,这样就将dialog与调用他的form绑定在一起,PyQt默认会将它剧中显示。

我们之前提到过,当调用exec_()方法的时候,会弹出一个模态对话框,用户想与程序的其他部分交互时必须点击确定或者取消。在下一节中,我们将会使用非模态版本的对话框。

如果用户点击确定,我们将format字典中的值设置为用户设定的值,并且更新table,这样表格中显示的浮点数就能以新的形势显示在程序中。如果用户点击取消,我们什么都不做。在方法的最后,对话框超出作用域,变成一个垃圾。

为了节省空间以及避免无谓的重复,从现在开始,我们将不会展示任何import语句,除非他的含义并不明显。所以,这个例子中,我们就不展示from PyQt4.QtCore import * or the PyQt4.QtGui这些了。

现在我们来看对话框是如何实现的。

class NumberFormatDlg(QDialog):

    def __init__(self, format, parent=None):
        super(NumberFormatDlg, self).__init__(parent)

        thousandsLabel = QLabel("&Thousands separator")
        self.thousandsEdit = QLineEdit(format["thousandsseparator"])
        thousandsLabel.setBuddy(self.thousandsEdit)
        decimalMarkerLabel = QLabel("Decimal &marker")
        self.decimalMarkerEdit = QLineEdit(format["decimalmarker"])
        decimalMarkerLabel.setBuddy(self.decimalMarkerEdit)
        decimalPlacesLabel = QLabel("&Decimal places")
        self.decimalPlacesSpinBox = QSpinBox()
        decimalPlacesLabel.setBuddy(self.decimalPlacesSpinBox)
        self.decimalPlacesSpinBox.setRange(0, 6)
        self.decimalPlacesSpinBox.setValue(format["decimalplaces"])
        self.redNegativesCheckBox = QCheckBox("&Red negative numbers")
        self.redNegativesCheckBox.setChecked(format["rednegatives"])

        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|
                                     QDialogButtonBox.Cancel)

        self.format = format.copy()

        grid = QGridLayout()
        grid.addWidget(thousandsLabel, 0, 0)
        grid.addWidget(self.thousandsEdit, 0, 1)
        grid.addWidget(decimalMarkerLabel, 1, 0)
        grid.addWidget(self.decimalMarkerEdit, 1, 1)
        grid.addWidget(decimalPlacesLabel, 2, 0)
        grid.addWidget(self.decimalPlacesSpinBox, 2, 1)
        grid.addWidget(self.redNegativesCheckBox, 3, 0, 1, 2)
        grid.addWidget(buttonBox, 4, 0, 1, 2)
        self.setLayout(grid)

        self.connect(buttonBox, SIGNAL("accepted()"),
                     self, SLOT("accept()"))
        self.connect(buttonBox, SIGNAL("rejected()"),
                     self, SLOT("reject()"))
        self.setWindowTitle("Set Number Format (Modal)")

对于任何一个我们希望用户可以控制的format,我们都设置了一个标签,这样用户就能知道他们编辑的是哪一个属性。其中的format参数,我们假设他包含了我们需要的全部值,所以我们使用它来初始化编辑widget。我们也使用了setBuddy()方法去支持用户获得键盘焦点。

我们在这里设置唯一的验证是限制spinbox为demcimal。我们选择的是提交后验证的方式,就是说,当用户输入数据点击OK时,触发验证方法。在下一节,我们将会看到预防式验证,这样不合理的数据将不能输入。

		self.format = format.copy()

我们需要复制一份传入的format字典参数,因为我们想要改变对话框内的字典但是又不影响原来的字典。

		grid = QGridLayout()
		grid.addWidget(thousandsLabel, 0, 0)
		grid.addWidget(self.thousandsEdit, 0, 1)
		grid.addWidget(decimalMarkerLabel, 1, 0)
		grid.addWidget(self.decimalMarkerEdit, 1, 1)
		grid.addWidget(decimalPlacesLabel, 2, 0)
		grid.addWidget(self.decimalPlacesSpinBox, 2, 1)
		grid.addWidget(self.redNegativesCheckBox, 3, 0, 1, 2)
		grid.addWidget(buttonBox, 4, 0, 1, 2)
		self.setLayout(grid)

这个布局的外观与我们之前见到的Pen Properties对话框很相似,除了QDialogButtonBox widget替换了原来的按钮布局。

在此输入图片描述

“red negatives”checkbox与button box都被布局为一行两列。行和列的span是通过QGridLayout’s addWidget()addLayout()最后两个参数传递的。

		self.connect(buttonBox, SIGNAL("accepted()"),
		self, SLOT("accept()"))
		self.connect(buttonBox, SIGNAL("rejected()"),
		self, SLOT("reject()"))
		self.setWindowTitle("Set Number Format (Modal)")

设置窗口标题与进行连接于Pen Properties对话框很相似,只不过这次我们使用的是button box,而不是直接与按钮连接。

def numberFormat(self):
	return self.format	

如果用户点击OK,对话框将会accept并且返回一个True值。这样,通过调用numberFormat()方法,调用者覆盖了对话框的format字典。由于我们并没有将self.format属性设置为私有属性(设置为__format),所以我们可以在form之外直接引用到他;下一个例子我们将会用到那个方法。

当用户点击OK后,应为我们使用的是提交式验证,这样,可能有一些widget会包含不合适的数据。为了处理这些,我们重新了QDialog.accept()方法,并且在这里提供了我们的验证方式。由于这个方法有些长,我们一段一段的看。

def accept(self):
    class ThousandsError(Exception): pass
    class DecimalError(Exception): pass
    Punctuation = frozenset(" ,;:.")

开始时,我们创建了两个异常类,这两个类我们在accept()方法内部会用到。这将会保持我们的代码尽可能的干劲整洁。我们也创建了千位和小数点可以接受的字符集合。

我们与验证连接的两个编辑widget是两个line edit。因为小数部分有spinbox来限制到合适的范围内,而“red negatives”的checkbox也只有选和不选两种选项。

    thousands = unicode(self.thousandsEdit.text())
    decimal = unicode(self.decimalMarkerEdit.text())
    try:
        if len(decimal) == 0:
            raise DecimalError, ("The decimal marker may not be "
                                 "empty.")
        if len(thousands) > 1:
            raise ThousandsError, ("The thousands separator may "
                                   "only be empty or one character.")
        if len(decimal) > 1:
            raise DecimalError, ("The decimal marker must be "
                                 "one character.")
        if thousands == decimal:
            raise ThousandsError, ("The thousands separator and "
                          "the decimal marker must be different.")
        if thousands and thousands not in Punctuation:
            raise ThousandsError, ("The thousands separator must "
                                   "be a punctuation symbol.")
        if decimal not in Punctuation:
            raise DecimalError, ("The decimal marker must be a "
                                 "punctuation symbol.")
    except ThousandsError, e:
        QMessageBox.warning(self, "Thousands Separator Error",
                            unicode(e))
        self.thousandsEdit.selectAll()
        self.thousandsEdit.setFocus()
        return
    except DecimalError, e:
        QMessageBox.warning(self, "Decimal Marker Error",
                            unicode(e))
        self.decimalMarkerEdit.selectAll()
        self.decimalMarkerEdit.setFocus()
        return

我们从得到两个line edit的文本开始。尽管尽管千位的分隔符有可能没有,但是小数分隔符是一定要有的,所以首先我们要检查decimalMarkerEdit最少有一个字符。如果没有,我们就抛出DecimalError异常,并提供合适的错误信息。如果没有分隔符或者两个分隔符相同,又或者分隔符不知我们设置的Punctuation集合中,我们也抛出异常。if语句中不同的地方是千位分隔符可以为空值,而小数位不行。

在此输入图片描述

    self.format["thousandsseparator"] = thousands
    self.format["decimalmarker"] = decimal
    self.format["decimalplaces"] = (
            self.decimalPlacesSpinBox.value())
    self.format["rednegatives"] = (
            self.redNegativesCheckBox.isChecked())
    QDialog.accept(self)

如果没有raise异常的话,任何一个支路的的return语句都不会被执行,这样就会到达最后的accept()方法。这里我们我们利用editing widget获取的数据更新format字典。 之后这个form将会被关闭(实际上是隐藏起来了),并且exec_()语句返回True.正如我们之前看到的,如果调用者接受到了Ture,那么他将通过`numberFormat()·来检索format信息。

我们为什么不使用super()来调用基类的accept()方法呢?简单的回答是,在这里使用super()不能的到应有的效果。详细解释请翻阅PyQt文档的pyqt4ref.html中的““super and PyQt classes”。

尽管当用户点击确定或者取消时对话框隐藏起来的,不过当他超出作用域时,他会变成一个垃圾被收集,这个例子中在setNumberFormat1()的最后对话框变成垃圾。

创建一个像这样的模态对话框是很直接的,与之在一起的一些事情是进行布局和验证,就像我们这里做的。

在一些例子中,用户想要能够看到他们选择后的结果,可能会一直改变选择直到满意为止。在这些情况中模态对话框就不是很方便了,应为用户必须调用对话框,进行编辑,确定,看到结果后在一遍一遍的重复 这个过程,直到他们对最终的效果感到满意。如果是一个非模态对话框,这样用户就可以一直更新程序的状态而不需要关闭对话框,这样用户就可以只调用一次对话框,进行编辑,看到效果,然后再编辑,一直到满意 为止,这样比以前方便快捷多了。我们将会在下一节看到如何做到这一点,并且我们也将会看到一个更加简单和有交互性的验证策略,防御式验证。

聪明的对话框

我们这样定义一个聪明的对话框:聪明的对话框在他的构造方法中使用传入的数据引用或者数据结构初始化他的部件,并且他可以直接相应用户的交互。聪明的对话框可以同时拥有widget级别 的验证和form级别的验证。聪明的对话框通常是非模态的,拥有"应用"和"关闭"按钮,可以直接通过得到的数据反应到部件中。聪明的非模态对话框使用"应用"按钮连接信号槽通知发生了状态的变化。

使用非模态聪明对话框的主要好处可以在使用时清晰的看到。当对话框创建时,他给予了调用它的form一个引用,使他可以方便的更新数据与数据结构。这样对话框必须拥有得到正确 数据的能力,这样就能根据正确的数据来更新部件的信息。而非模态对话框又一个风险:因为用户可以操作多处,有可能造成对话框和应用程序中的数据不同步。

这一节我们将继续研究number format对话框,这样我们可以与之前学过的方法进行对比。

非模态 应用/关闭型 对话框

如果我们希望用户可以重复更改数字的格式,并且能马上看到效果,那么用户可以持续调用这个对话框将会变得非常方便。解决方案是使用非模态对话框,这样用户就可以按照他们喜欢的方式 持续更改数据,验证效果。与模特的确定/取消式对话框不同的是,在模特对话框中,如果用户点击取消,什么都不会发生,程序会保持之前的状态;而在这里,用户一旦点击应用,改变 就会发生,不能撤销到之前的状态,当然,我们可以设置一个撤销按钮,不过这需要更多的工作。

从表面上看,模态与非模态对话框的不同之处好像只是按钮上文字的区别,实际上有两个更重要的区别:调用对话框的form在创建和调用对话框上有区别,对话框在关闭时一定要确保被 删除而不是隐藏。让我们从如何调用对话框开始:

def setNumberFormat2(self):
    dialog = numberformatdlg2.NumberFormatDlg(self.format, self)
    self.connect(dialog, SIGNAL("changed"), self.refreshTable)
    dialog.show()

我们使用与模态相同的方式创建对话框,之后我们将Python的changed信号与对话框的refreshTable()方法进行连接,然后我么show()对话框。当调用show()方法时,对话框会以 非模态的状态显示,程序继续执行,用户可以同时与程序以及对话框交互。

只要对话框发出changed信号,主form的refreshTable()方法就会被调用,这将会利用format字典中的数据重新格式化表格中的数字格式。现在我们可以这样认为,当用户点击应用按钮的时候,format字典将会更新,并且发出changed信号。不久之后我们就会看到这到底是如何实现的。

尽管dialog变量超出了作用域,PyQt会非常智能的保留这个非模态对话框的引用,所以dialog依然存在。不过当用户点击关闭按钮的时候,对话框通常是隐藏起来了,所以如果用户一遍一遍的调用对话框,这样就会有很多对话框被create但是却没有删除,造成很多不必要的内存浪费。一个解决办法是确保对话框关闭后是被删除而不是被隐藏(在介绍“live”对话框时,我们将会看到另一种解决方案)。现在我们从__init__()方法开始看起。

def __init__(self, format, parent=None):
    super(NumberFormatDlg, self).__init__(parent)
    self.setAttribute(Qt.WA_DeleteOnClose)

我们调用完super()方法后,调用setAttribute方法来确保对话框关闭后是被删除而不是仅仅被隐藏。

    punctuationRe = QRegExp(r"[ ,;:.]")

    thousandsLabel = QLabel("&Thousands separator")
    self.thousandsEdit = QLineEdit(format["thousandsseparator"])
    thousandsLabel.setBuddy(self.thousandsEdit)
    self.thousandsEdit.setMaxLength(1)
    self.thousandsEdit.setValidator(
            QRegExpValidator(punctuationRe, self))

    decimalMarkerLabel = QLabel("Decimal &marker")
    self.decimalMarkerEdit = QLineEdit(format["decimalmarker"])
    decimalMarkerLabel.setBuddy(self.decimalMarkerEdit)
    self.decimalMarkerEdit.setMaxLength(1)
    self.decimalMarkerEdit.setValidator(
            QRegExpValidator(punctuationRe, self))
    self.decimalMarkerEdit.setInputMask("X")

    decimalPlacesLabel = QLabel("&Decimal places")
    self.decimalPlacesSpinBox = QSpinBox()
    decimalPlacesLabel.setBuddy(self.decimalPlacesSpinBox)
    self.decimalPlacesSpinBox.setRange(0, 6)
    self.decimalPlacesSpinBox.setValue(format["decimalplaces"])

    self.redNegativesCheckBox = QCheckBox("&Red negative numbers")
    self.redNegativesCheckBox.setChecked(format["rednegatives"])

    buttonBox = QDialogButtonBox(QDialogButtonBox.Apply|
                                 QDialogButtonBox.Close)

创建form部件的方法与我们之前看到过的方法非常类似,与之前不同的是,这次我们使用的是防御式验证。我们将千位和小数位的分割符设置为一位长度,并且两个部分都设置了一个QRegExpValidator。一个validator只会允许用户输入合法的字符集合,而一个正则的validator只会允许用户输入符合设置的正则表达式的字符。PyQt使用Python的re模块来进行正则表达式的操作。

QRegExpValidator初始化时需要两个参数——正则表达式、parent,所以我们将self添加到了构造函数中。

在这个例子中,我们将验证的正则表达式设置为“[ ,;:.]”。这是一个character class这意味着只有在方括号里面的字符(空格,逗号,分号,冒号,句号)是合适的字符。注意正则表达式的字符串前面有一个'r',这意味着这这是一个rawstring,也就是说string中的每一个字符都是字面上的意思,没有转义。这样就避免了类似\的字符被转义,因此,如果没有特殊情况,在使用正则表达式时,我们总是在表达式前加一个'r'。

尽管我们可以很高兴的接受一个空白的千位分隔符,但是小数位我们必须有一个分隔符。所以我们使用了一个input mask。一个“X”mask表示这里需要一个字符,我们不必考虑输入的字符是否合法,因为前面设置的正则表达式就会帮我们验证。在QLinEdit.inputMask文档的属性中有关于format mask的解释。

另一个与模态对话框版本的不同之处在于,我们使用的是应用关闭按钮,而不是确定去取消按钮。

    self.format = format

在模态对话框中我们使用的是format字典的一个拷贝;这里我们直接使用format字典的引用,这样我们就可以在对话框中直接更改他了。

由于这个对话框的布局与之前介绍的模态对话框完全相同,我们就不再列出布局的代码。

    self.connect(buttonBox.button(QDialogButtonBox.Apply),
                 SIGNAL("clicked()"), self.apply)
    self.connect(buttonBox, SIGNAL("rejected()"),
                 self, SLOT("reject()"))
    self.setWindowTitle("Set Number Format (Modeless)")

我们创建了两个信号槽。第一个链接了应用按钮的clicked()信号与apply()方法。为了实现这个链接,我们必须检索button box中button的引用,通过调用buttonBox的button方法,并且传递一个QDialogButtonBox.Apply参数。

链接reject()方法将会把对话框关闭,因为我们之前设置了Qt.WA_DeleteOnClose属性,对话框将会直接被删除,而不是隐藏起来。因为我们没有链接对话框自己的accept()的槽,唯一能关闭对话框的方法就是关闭这个对话框(以前点确定也可以关闭)。如果用户点击了应用按钮,这将会调用我们下面显示的apply方法。最后,我们设置了一个窗口的标题。

这个类里面最后的一个方法是apply(),我们将会分两部分讲解:

def apply(self):
    thousands = unicode(self.thousandsEdit.text())
    decimal = unicode(self.decimalMarkerEdit.text())
    if thousands == decimal:
        QMessageBox.warning(self, "Format Error",
                "The thousands separator and the decimal marker "
                "must be different.")
        self.thousandsEdit.selectAll()
        self.thousandsEdit.setFocus()
        return
    if len(decimal) == 0:
        QMessageBox.warning(self, "Format Error",
                "The decimal marker may not be empty.")
        self.decimalMarkerEdit.selectAll()
        self.decimalMarkerEdit.setFocus()
        return

    self.format["thousandsseparator"] = thousands
    self.format["decimalmarker"] = decimal
    self.format["decimalplaces"] = (
            self.decimalPlacesSpinBox.value())
    self.format["rednegatives"] = (
            self.redNegativesCheckBox.isChecked())
    self.emit(SIGNAL("changed"))

转载于:https://my.oschina.net/duoduo3369/blog/109966

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值