第3章 基础窗口部件——QWidget
在前面的章节中,学习了直接继承Qt对话框类QDialog实现自定义的对话框,并通过手写代码实现了子窗口部件的添加和排布。从本章开始,将学习使用Qt的GUI界面设计器(Qt Designer)进行界面的绘制和布局,学习如何在应用程序中加载和使用Qt设计器绘制的窗口部件;学习Qt的基础窗口部件——QWidget的基本特性和功能,并深入地介绍Qt元对象系统、属性系统和对象树机制。
3.1 Qt设计器绘制窗口部件
Qt设计器是Qt GUI编程语言一系列工具中的一个,该工具提供了Qt基本的可绘制窗口部件,比如QWidget、QLabel、QPushButton、QVBoxLayout等等。在设计器中通过鼠标直接拖放这些窗口部件,能够高效、快速地实现GUI界面的设计,界面直观形象,所见所得。
本节将从绘制一个自定义的QWidget窗口部件来学习Qt设计器的使用,同时也将了解Qt的资源文件(*.qrc)、ui文件(*.ui)以及相应的编译工具——资源编译器(Resource Compiler, rcc)和用户界面编译器(User Interface Compiler, uic)。
3.1.1 Qt设计器基础
首先,将Qt设计器打开。在控制台执行命令
designer&
其中,“&”表示将在后台运行Qt设计器,这样就可以继续使用该控制台运行其他的命令。进入Qt设计器的默认启动界面,如图3-1所示。
在“New Form”对话框中,可以选择要设计的顶层窗口部件类型(顶层窗口部件是其他子窗口部件的载体):
● Dialog with Buttons Bottom,底部具有“确定”和“取消”按钮的对话框;
● Dialog with Buttons Right,右侧具有“确定”和“取消”按钮的对话框;
● Main Window,应用程序主窗口界面;
● Widget,Qt的基础窗口部件QWidget。
“New Form”对话框的右侧是窗口部件类型(template/forms)的预览区。
进入Qt设计器主界面后,就可以使用Qt的标准窗口部件进行GUI可视化设计了。在Qt设计器的左侧“Widget Box”栏列出了经常使用的Qt标准窗口部件,单击鼠标左键选中相应的窗口部件图标,可以将它们直接拖放在顶层窗口部件的界面上。同时,也可以将自己设计的窗口部件组合(通过使用布局管理器对Qt标准窗口部件进行布局和组合)或放置了其他窗口部件的Qt容器类(见“Widget Box”栏的“Containers”组)直接拖放到“Widget Box”栏中,Qt设计器会自动在“Widget Box”栏中生成“Scratchpad”组,并生成新的自定义的窗口部件。以后可以像使用Qt提供的标准窗口部件一样使用自己创建的窗口部件。
图3-1 Qt设计器初始界面
图3-2 Qt设计器中的部件
选中Qt设计器Tools菜单中的全部复选框,在Qt设计器的右侧可以看到设计器提供的一些编辑工具,如图3-3所示。
● 对象监视器(Object Inspector):列出了界面中的所有窗口部件,以及各窗口部件的父子关系和包容关系。
● 属性编辑器(Property Editor):列出了窗口部件可编辑的属性。
● 动作编辑器(Action Eidtor):列出了为窗口部件设计的QAction动作,通过“添加”或“删除”按钮可以新建一个可命名的QAction动作或删除指定的QAction动作。
● 信号/槽编辑器(Signal/Slot Editor):列出了在Qt设计器中关联的信号和槽,通过双击列中的对象或信号/槽,可以进行对象和信号/槽的选择。
● 资源编辑器(Resource Editor):列出了程序使用的资源文件(*.qrc)以及相应的资源,在该编辑器中可以创建或删除资源文件,也可以添加或取消资源。
图3-3 Qt设计器提供的编辑器
此外,通过Qt设计器的“Edit”菜单,可以打开Qt设计器的4种GUI窗口部件编辑模式:
● Widget编辑模式(Widget Editing Mode):可以在Qt设计器中添加GUI窗口部件,以及修改它们的属性和外观。
● 信号和槽编辑模式(Signals and Slots Editing Mode):可以在Qt设计器中的窗口部件上关联Qt已经定义好的信号和槽。
● 伙伴编辑模式(Buddy Editing Mode):可以在Qt设计器中的窗口部件上建立QLabel标签和其他窗口部件的伙伴关系。
● Tab编辑模式(Tab Order Editing Mode):可以在Qt设计器中的窗口部件上设置Tab键在窗口部件上的焦点顺序。
扩展阅读
QLabel标签和伙伴(buddy)窗口部件
一个标签QLabel和一个窗口部件具有伙伴关系,即指当用户激活标签的快捷键时,鼠标/键盘的焦点将会转移到它的伙伴窗口部件上。只有QLabel标签对象才可以有伙伴窗口部件,也只有该QLabel对象具有快捷键(在显示文本的某个字符前面添加一个前缀“&”,就可以定义快捷键)时,
伙伴关系才有效。例如,
QLineEdit* priceLineEdit = new QLineEdit(this); QLabel* priceLabel = new QLabel("&Price", this); priceLabel->setBuddy(priceLineEdit);
代码定义了priceLabel标签的快捷键为“Alt + P”,并将行编辑框priceLineEdit设为它的伙伴窗口部件。所以当用户按键“Alt+P”时,焦点将会跳至行编辑框priceLineEdit中。
Qt设计器提供了伙伴编辑模式,可以通过鼠标拖放操作快捷地建立标签QLabel和其他窗口部件的伙伴关系。
对于Qt设计器提供的上述工具和编辑模式,将在下一节学习。在此,只对Qt资源系统(Qt Resource System)略加介绍。
Qt资源系统是平台无关的,它以二进制代码保存可执行应用程序运行时使用的文件(如图标文件)。在编译Qt应用程序时,Qt资源编译器rcc会对Qt资源文件进行编译,并生成相应的二进制代码文件(最后由连接程序将该二进制代码文件连接为应用程序的一部分),而该资源文件一旦通过编译就不能再改变,除非重新编译Qt应用程序。因此,需要运行时改变的文件是不能放置在Qt的资源系统中的(例如,在程序运行期间需要编辑修改的XML文件)。
Qt资源文件*.qrc是XML格式的文本文件,它记录了Qt应用程序使用的资源。资源文件及其记录的资源是应用程序源码的一部分,一个应用程序可以有多个资源文件。注意,资源文件*.qrc中记录资源的路径是资源相对于资源文件*.qrc的位置。
对于上述目录结构描述的资源,其资源文件hello.qrc的内容为:
<!DOCTYPE RCC> <RCC version="1.0"> <qresource> <file>lines/dotline.png</file> <file>lines/dashdotline.png</file> <file>lines/solidline.png</file> <file>lines/dashline.png</file> </qresource> </RCC>
这是一个XML格式的文本文件。
第1行XML语句声明了XML文档的类型为RCC,即Qt资源。
<RCC version="1.0">引入了XML文档的第一个元素RCC,并声明了RCC的版本为1.0。
元素<qresource>指示其子元素是资源元素。四行<file>元素记录了应用程序使用的资源,这些资源放置在lines子目录下(如图3-4所示)。
使用资源系统之前,必须将定义的资源文件 *.qrc引入到qmake工程文件中。对于hello.qrc则必须在qmake工程文件中加入下列一行:
RESOURCES += hello.qrc
图3-4 资源文件及其资源
它表示应用程序用到了资源文件hello.qrc,并告知qmake工具。
接下来,就可以在应用程序启动的时候通过使用Q_INIT_RESOURCE()函数进行资源的初始化。
int main(int argc, char *argv[]) { QApplication app(argc, argv); Q_INIT_RESOURCE(hello); ... return app.exec(); }
宏Q_INIT_RESOURCE() 初始化hello.qrc记录的资源,“hello”是资源文件的名称。之后,就可以在应用程序中引用资源了(应用程序的资源将在应用程序启动的时候自动加载)。例如,创建一个带有实线图标的动作对象:
QAction* act = new QAction(QIcon(“:/lines/solidline.png”),tr(“实线”), this);
在上述创建QAction动作对象的时候,使用了QAction(const QIcon& icon, const QString&text, QObject* parent)构造函数,它的形参icon和text都是对常量对象的引用,因此可以传入不具名的QIcon(”:/lines/solidline.png”) 对象作为实参和临时QString对象tr(”实线”)作为实参。而对于非常量对象的引用,不具名的对象、临时对象和具体数值是不能够作为实参的。例如,如果其构造函数定义为QAction(QIcon& icon, QString& text, QObject* parent),那么上述QAction对象的一个合法的构造方法是:
QIcon icon(”:/lines/solidline.png”); QString str(tr(”实线”)); QAction* act = new QAction(icon, str, this);
对于C++内嵌的数值性数据类型亦是如此。不过,很少有程序员将一个数值作为引用来传递,毕竟内嵌的简单数据类型的传值参数的开销是非常小的。
在定义好应用程序的资源/资源文件以及在程序中添加了初始化和加载资源的代码后,运行qmake生成Makefile文件,接着运行make命令编译/连接应用程序,gcc编译器自动调用Qt的rcc资源编译器编译hello.qrc及其资源并生成C++源文件qrc_hello.cpp,最后gcc编译qrc_hello.cpp文件并同其他的目标代码连接成可执行的应用程序,如图3-5所示。
图3-5 Qt资源的编译/链接
此外,应用程序也可以使用外部的二进制资源(External Binary Resources),外部二进制资源不作为应用程序的二进制代码的一部分。这种方式脱离于应用程序,可以在需要的时候进行动态加载。
要使用外部二进制资源,首先根据资源文件将资源编译成二进制的资源数据(文件的扩展名通常为.rcc)。例如,上述的hello.qrc资源文件及其资源,将编译为二进制文件hello.rcc。在控制台执行下列命令:
rcc -binary hello.qrc -o hello.rcc
然后,在应用程序中注册资源(假设二进制资源文件位于/home/lcf/book/rcc/目录下):
QResource::registerResource(“/home/lcf/mybooks/rcc/hello.rcc”);
接着就可以在应用程序中使用了,使用的方法同前面讲过的使用方法相同。
如果资源文件*.qrc中记录的资源不存在,在编译的时候则会报错。而如果在应用程序中引用有错误时,编译不会报错,但在运行应用程序时会导致一些问题,例如无法显示图标等。
3.1.2 绘制窗口部件
上一节,讲到了Qt设计器的一些基础知识。现在,使用Qt设计器绘制一个实现查找文件功能的GUI窗口部件。
图3-6 查找文件界面
进入Qt设计器界面后,在“New Form”对话框的“template/forms”选项卡中选择“Widget”,单击“create”按钮。Qt设计器将生成一个顶层的窗口部件,并以网格的方式显示该窗口部件。
将窗口部件的对象名字(objectName)属性修改为“FindFileForm”(Qt的uic编辑器将根据顶层窗口部件的对象名字生成该界面的窗口部件类,将在下面讲到)。将窗口拉伸到一个合适的大小,或者通过设置窗口部件的“geometry”属性值改变窗口的大小。接下来,添加必要的子窗口部件,如图3-6所示。
按如下步骤添加各窗口部件:
添加“名为”、“包含文本”、“查找位置”QLabel标签以及相关的编辑框(由上到下依次为QComboBox、QLineEdit和QComboBox),添加一个“浏览...”QPushButton按钮;设置它们的属性(如表3-1所示,通过属性编辑器设置),调整大小和位置,然后选中所有这些窗口部件(按shift键的同时用鼠标左键点选窗口部件),在选中的窗口部件上单击鼠标右键,选择上下文菜单“layout”的菜单命令“Layout in a Grid”,把它们放置在一个网格布局管理器中(或直接单击布局管理器工具栏中的“网格布局管理器”工具按钮,如图3-7所示)。
表3-1 窗口部件属性表(按从左到右、从上至下顺序,不包括布局管理器)
图3-7 布局管理器工具栏
排布后的各窗口部件如图3-8所示。
图3-8 使用网格布局管理器组织输入窗口部件
添加两个QCheckBox复选框窗口部件:“区分大小写”复选框sensitiveCheckBox(对象名字)和“包含子文件夹”复选框subfolderCheckBox,并放置在一个水平布局管理器QHBoxLayout中,如图3-9所示。
图3-9 使用水平布局管理器组织复选框窗口部件
为了界面的美观,使用了一些Sapcer窗口部件(蓝色的像弹簧状的),选中这些Spacer可以在属性编辑器中设置它的sizeType属性。在Qt设计器中将Spacer加入到一个布局管理器,有两种办法:
● 选中各子窗口部件的同时也选中Spacer,把它们件加入到布局管理器中;
● 将各子窗口部件加入一个布局管理器后,将Spacer拖放到该布局管理器的适当位置。
设置Spacer窗口部件的属性:
● 左侧Spacer的sizeType属性为QSizePolicy::Fixed(其他采用默认值);
● 右侧Spacer的属性全部采用默认值。
添加一个容器QFrame子窗口部件(属性采用默认值),并将步骤、生成的窗口部件组合依次拖放到该QFrame子窗口部件中,并设置它的布局管理器为垂直布局管理器,效果如图3-10所示。
图3-10 使用QFrame容器重新组织窗口部件
添加三个QPushButton窗口部件:“查找”按钮findPushBtn、“停止”按钮stopPushBtn和“关闭”按钮closePushBtn,并放置在一个水平布局管理器中;然后放入4个Spacer窗口部件,属性采用默认值,如图3-11所示。
图3-11 使用水平布局管理器组织按钮窗口部件
添加“查找结果”QLabel标签和QTableWidget表窗口部件resultTableWidget,在表窗口部件中单击鼠标右键,在弹出的上下文菜单中选择“Edit Items...”命令,在出现的“Edit Table Widget”对话框中添加表列:名称、大小、修改时间和权限,如图3-12所示。
图3-12 编辑表格窗口部件
添加“状态”标签statusLabel和“查找结果”标签resultLabel,设置它们的sziePolicy属性(如表3-2所示),并把它们放置在一个水平布局管理器中。
表3-2 窗口部件属性表
排布好的“状态“和”“查找结果”标签如图3-13所示。
图3-13 使用水平布局管理器组织状态显示标签
最后,选中顶层窗口部件(即最初建立的QWidget窗口部件),然后将整个窗口部件放置在一个垂直布局管理器中(如图3-6所示)。
现在使用Qt资源文件为应用程序用户界面的标题栏加一个标题和图标。
选择“Tools”菜单的“Resource Eidtor”命令,打开资源编辑器。
在“Current Resource”下拉框中选择“New...”选项(如图3-14所示),打开“新建资源文件”对话框。
在“新建资源文件”对话框中,定位到资源文件放置的目录(本例为chapter03/findfile/),输入资源文件的名字findfile.qrc,保存之,如图3-15所示。
图3-14 资源编辑器
图3-15 新建资源文件
在“资源编辑器”对话框中单击“+”操作,添加一个前缀,可以根据自己的需要修改,在此采用默认值,如图3-16所示。
单击“Add Files...”按钮,打开“Open File”对话框,选择需要添加的图标资源,并单击“打开”按钮,如图3-17所示。
图3-16 编辑资源文件
图3-17 为资源文件打开一个资源
现在findfile.png图标文件就加入到了资源文件findfile.qrc中,其添加资源后的状态如图3-18所示。
在对象监视器中选择顶层窗口部件FindFileForm对象,并在属性编辑器中选择“windowTitle”属性,将“Form”修改为“查找文件”;选择“ windowIcon ”属性选项,并单击下拉框右侧的“打开”按钮,如图3-19所示。
图3-18 添加资源后的资源文件状态
图3-19 设置窗口部件的窗口图标
在“Find icon”对话框中有两个单选项:选项“Specify image file”表示通过绝对路径来定位图标文件;选项“Specify resource”表示通过资源文件来定位资源。笔者推荐使用选项“Specify resource”,因为采用绝对路径来定位图标文件,往往会导致应用程序在安装后系统无法找到该图标文件。选中“Specify resource”,这时候“Current Resource”列表中会列出已经创建的资源文件以及在文件中添加的资源。选中刚刚添加的图标资源findfile.png,单击“确定”按钮。如图3-20所示。
图3-20 指定图标资源
在Qt设计器中预览GUI用户界面(可以通过快捷键“Ctrl+R”激活预览命令),可以看到图标已经添加到顶层窗口部件“FindFileForm”用户界面的标题栏上了。
绘制好GUI用户界面后,保存为“findfileform.ui”文件(该ui文件在源代码中的路径为“chapter03/findfile/ findfileform.ui”),该文件是XML格式的文本文档。一个比较小的ui文件的内容如下所示。
<ui version="4.0" > <class>Dialog</class> <widget class="QDialog" name="Dialog" > <property name="geometry" > <rect> <x>0</x> <y>0</y> <width>400</width> <height>300</height> </rect> </property> <property name="windowTitle" > <string>Dialog</string> </property> <widget class="QPushButton" name="pushButton" > <property name="geometry" > <rect> …… </rect> </property> …… </widget> …… </widget> <resources/> <connections/> </ui>
有时候,在Qt设计器中修改窗口部件的类型和属性是比较麻烦的,比如修改顶层窗口部件的类型(例如,将QDialog改为QWidget)。而直接修改ui文件很容易就做到了(在Qt设计器中无法做到,必须要重新建立一个新的QDialog顶层窗口部件)。有兴趣的读者可以对照Qt设计器中的界面和属性,对Qt的ui文件仔细分析一下,在此不再赘述。
在Qt设计器中绘制好GUI界面后,就可以利用Qt提供的qmake工具和uic编译工具将ui文件编译生成C++源文件。该源文件默认的命名规则是:
ui_<ui文件名>.h
对于绘制的GUI用户界面文件findfileform.ui,uic编译工具将生成名为“ui_findfileform.h” 的C++代码文件。通过在qmake工程文件中配置FORMS选项,可以让uic编辑工具自动完成上述过程(在下一节进行详细描述)。
现在在控制台上执行uic命令,看一下生成的ui_findfileform.h文件 (也可以生成自己喜欢的名字或直接输出在控制台):
uic -o ui_findfileform.h findfileform.ui
或者
uic findfileform.ui(直接输出到控制台)
生成的C++源文件的内容如下所示(由于文件过大,部分省略)。
#ifndef UI_FINDFILEFORM_H #define UI_FINDFILEFORM_H #include <QtCore/QVariant> #include <QtGui/QAction> #include <QtGui/QApplication> …… class Ui_FindFileForm { public: QVBoxLayout *vboxLayout; QGridLayout *gridLayout; QLineEdit *txtLineEdit; QPushButton *browsePushBtn; …… void setupUi(QWidget *FindFileForm) { FindFileForm->setObjectName(QString::fromUtf8("FindFileForm")); vboxLayout = new QVBoxLayout(FindFileForm); vboxLayout->setSpacing(6); vboxLayout->setMargin(9); vboxLayout->setObjectName(QString::fromUtf8("vboxLayout")); gridLayout = new QGridLayout(); gridLayout->setSpacing(6); gridLayout->setMargin(0); gridLayout->setObjectName(QString::fromUtf8("gridLayout")); txtLineEdit = new QLineEdit(FindFileForm); txtLineEdit->setObjectName(QString::fromUtf8("txtLineEdit")); gridLayout->addWidget(txtLineEdit, 1, 1, 1, 2); …… retranslateUi(FindFileForm); QSize size(438, 387); size = size.expandedTo(FindFileForm->minimumSizeHint()); FindFileForm->resize(size); QMetaObject::connectSlotsByName(FindFileForm); } // setupUi void retranslateUi(QWidget *FindFileForm) { FindFileForm->setWindowTitle( QApplication::translate("FindFileForm", "Form", 0, QApplication::UnicodeUTF8)); …… Q_UNUSED(); } // retranslateUi }; namespace Ui { class FindFileForm: public Ui_FindFileForm {}; } // namespace Ui #endif // UI_FINDFILEFORM_H
该ui_findfileform.h文件定义了一个POD(Plain Old Data)类Ui_FindFileForm,该类的名字来自Qt设计器绘制的Qt GUI用户界面的顶层窗口部件(“objectName”属性为“FindFileForm”的窗口部件)的对象名。在类Ui_FindFileForm定义的公共区,包含了在Qt设计器中添加的所有子窗口部件的指针,并定义了几个内联函数。其中,最重要的是公共函数setupUi(),该函数对传入的Qt标准窗口部件QWidget对象的标题、大小等进行设置。它的参数类型“QWidget *”决定了使用的顶层窗口部件(或主容器)的类型是QWidget,如果在Qt设计器中使用QMainWindow作为顶层窗口部件,那么setupUi()的参数类型将是“QMainWindow *”。
静态函数QMetaObject::connectSlotsByName(QObject * object)将递归地搜寻传入的Qt对象object的所有子对象,并把所有匹配的子对象的信号关联到object对象的符合下列规则的槽函数(将在本章的3.3节详细描述):
void on_<窗口部件名称>_<信号名称>(<信号参数>)
setObjectName() 函数设置对象的名称,以便能通过QObject::findChild() 以及QObject::objectName()函数获取这些对象的引用。
注意,uic工具生成的Ui_FindFileForm类并不是一个QWidget类,也不是一个QObject对象,它的作用仅仅是构建和部署Qt设计器中绘制的窗口部件。