整理日期: 2010年4月9日
本文是学习笔记之Qt从入门到精通(一)的接续
来自于FinderCheng 的Qt 学习之路。简介:在本系列文章中,FinderCheng
使用Qt4 进行C++ GUI 的开发。我是参照着《C++ GUI Programming with Qt4》一书进行学习的。其实,我也只是初学Qt4,在这里将这个学习笔记记下来,希望能够方便更多的朋友学习Qt4。我是一个Java 程序员,感觉 Qt4 的一些命名规范以及约束同Java 有异曲同工之妙,因而从Java 迁移到Qt4 似乎困难不大。不过,这也主要是因为Qt4 良好的设计等等。
Qt 是一个著名的C++库——或许并不能说这只是一个GUI 库,因为Qt 十分庞大,并不仅仅是GUI。使用Qt,在一定程序上你获得的是一个“一站式”的 服务:
不再需要研究STL,不再需要C++的,因为Qt 有它自己的QString 等等。或许这样说很偏激,但Qt 确实是一个 “伟大的C++库”。
我们所使用的Qt,确切地说也就是它的GUI 编程部分。C++的GUI 编程同Java不同:GUI 并不是C++标准 的一部分。所以,如果使用Java,那么你最好的选择就是AWT/Swing,或者也可以使SWT/JFace,但是,C++的GUI 编程给了你更多的选 择:wxWidget, gtk++以及Qt。这几个库我都有接触,但是接触都不是很多,只能靠一些资料和自己的一点粗浅的认识说一下它们之间的区别(PS: 更详尽的比较在前面的文章中有)。
首 先说wxWidget,这是一个标准的C++库,和Qt 一样庞大。它的语法看上去和MFC 类似,有大量的宏。据说,一个MFC 程序员可以很容易的转换到 wxWidget上面来。wxWidget 有一个很大的优点,就是它的界面都是原生风格的。这是其他的库所不能做到的。wxWidget 的运行效率很高, 据说在Windows 平台上比起微软自家的MFC 也不相上下。
gtk++其实是一个C 库,不过由于C++和C 之间的关系,这点并没有很大 的关系。
但是,gtk++是一个使用C 语言很优雅的实现了面向对象程序设计的范例。不过,这也同样带来了一个问题——它的里面带有大量的类型转换的宏来模 拟多态,并且它的函数名“又臭又长(不过这点我倒是觉得无所谓,因为它的函数名虽然
很长,但是同样很清晰)”,使用下划线分割单词,看上去和Linux 如 出一辙。
由于它是C 语言实现,因此它的运行效率当然不在话下。gtk++并不是模拟的原生界面,而有它自己的风格,所以有时候就会和操作系统的界面显得格 格不入。
再来看Qt,和wxWidget 一样,它也是一个标准的C++库。但是它的语法很类似于Java 的Swing,十分清晰,而且 SIGNAL/SLOT 机制使得程序看起来很明白——
这也是我首先选择Qt 的一个很重要的方面,因为我是学Java 出身的 。不过,所谓“成也萧何,败也萧何”,这种机制虽然很清楚,但是它所带来的后果是你需要使用Qt 的qmake 对程序进行预处理,才能够再使用make 或者 nmake 进行编译。并且它的界面也不是原生风格的,尽管Qt 使用style 机制十分巧妙的模拟了本地界面。另外值得一提的是,Qt 不仅仅运行在桌面环境 中,Qt 已经被Nokia 收购,它现在已经会成为Symbian 系列的主要界面技术——Qt 是能够运行于嵌入式平台的。
以往人们对Qt 的 授权多有诟病。因为Qt 的商业版本价格不菲,开源版本使用的是GPL 协议。但是现在Qt 的开源协议已经变成LGPL。这意味着,你可以将Qt作为一个库连 接到一个闭源软件里面。可以说,现在的Qt 协议的争议已经不存在了——因为wxWidgets 或者gtk+同样使用的是类似的协议发布的。
在 本系列文章中,我们将使用Qt4 进行C++ GUI 的开发。我是参照着《C++ GUIProgramming with Qt4》一书进行学习的。其实,我也只是初学Qt4,在这里将这个学习笔记记下来,希望能够方便更多的朋友学习Qt4。我是一个Java 程序员,感觉 Qt4 的一些命名规范以及约束同Java 有异曲同工之妙,因而从Java迁移到Qt4 似乎困难不大。不过,这也主要是因为Qt4 良好的设计等等。
闲话少说,还是尽快开始下面的学习吧!
任何编程技术的学习第一课基本上都会是Hello, world!,我也不想故意打破这
个惯例——照理说,应该首先回顾一下Qt 的历史,不过即使不说这些也并无大碍。
或许有人总想知道,Qt 这个单词是什么意思。其实,这并不是一个缩写词,仅仅是因为它的发明者,TrollTech 公司的 CEO,Haarard Nord 和Trolltech 公司的总裁Eirik Chambe-Eng 在联合发明Qt 的时候并没有一个很好的名字。在这里,字母Q 是Qt 库中所有类的前缀——这仅仅是因为在Haarard 的emacs 的 字体中,这个字母看起来特别的漂亮;而字母t 则代表“toolkit”,这是在Xt( Xtoolkit )中得到的灵感。
顺便说句,Qt 原始的公司就是上面提到的Trolltech,貌似有一个中文名字是奇趣科技——不过现在已经被Nokia 收购了。因此,一些比较旧的文章里面会提到Trolltech 这个名字。
好了,闲话少说,先看看Qt 的开发吧!事先说明一下,我是一个比较懒的人,不喜欢配置很多的东西,而Qt 已经提供了一个轻量级的IDE,并且它的网站上也有for Eclipse 和 VS 的开发插件,不过在这里我并不想用这些大块头。
Qt 有两套协议——商业版本和开源的LGPL 版本。不同的是前者要收费,而后者免费,当然,后者还要遵循LGPL 协议的规定,这是题外话。
Qt 的网址是https://qt.nokia.com/downloads, 不过我打开这个站点总是很
慢,不知道为什么。你可以找到大大的 LGPL/Free 和 Commercial,好了,我选的是LGPL 版本的,下载包蛮大,但是下载并不会很慢。下载完成后安装就可以了,其它不用管了。这样,整个Qt 的开发环 境就装好了——如果你需要的话,也可以把qmake 所在的目录添加进环境变量,不过我就不做了。
安装完成后会有个Qt Creator 的东西,这就是官方提供的一个轻量级IDE,不
过它的功能还是蛮强大的。运行这个就会发现,其实Qt 不仅仅是Linux KDE 桌面的底层实现库。而且是这个IDE 的实现 这个IDE 就是用Qt 完成的。
Qt Creator 左面从上到下依次是Welcome(欢迎页面,就是一开始出现的那个);
Edit(我们的代码编辑窗口);Debug(调试窗 口);Projects(工程窗口);Help(帮助,这个帮助完全整合的Qt 的官方文档,相当有用);Output(输出窗口)。
下面我们来试试我们的 Hello, world! 吧!
在Edit 窗口空白处点右键,有 New project… 这里我们选第三项,Qt Gui
Application。
然后点击OK,来到下一步,输入工程名字和保存的位置
点击Next,来到选择库的界面。这里我们系统默认为我们选择了Qt core 和 GUI,还记得我们建的是Gui Application 吗?嗯,就是这里啦,它会自动为我们加上gui 这个库。现在应该就能看出,Qt 是多么庞大的一个库,它不仅仅有Gui,而且有 Network,OpenGL,XML 之类。不过,现在在这里我们不作修改,直接Next。
下一个界面需要我们定义文件名,我们不修改默认的名字,只是为了清除起见,把generate form 的那个勾去掉即可。
Next 之后终于到了Finish 了——漫长的一系列啊!检查无误后Finish 就好啦!
之后可以看到,IDE 自动生成了四个文件,一个.pro 文件,两个.cpp 和一个.h。
这里说明一下,.pro 就是工程文件(project),它是 qmake 自动生成的用于生产makefile 的配置文件。这里我们先不去管它。main.cpp 里面就是一个main 函数,其他两个文件就是先前我们曾 经指定的文件名的文件。
现在,我们把main.cpp 中的代码修改一下:
#include "QtGui/QApplication"
#include "QLabel"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel *label = new QLabel("Hello, world!");
label->show();
return a.exec();
}
好了!我们的第一个Qt 程序已经完成了。
PS:截了很多图,说得详细些,以后可就没这么详细的步骤啦,嘿嘿…相信很多朋友应该一下子就能看明白这个IDE 应该怎么使用了的,无需我多费口舌。呵呵。
下一篇中,将会对这个Hello, world!做一番逐行解释!
下面来逐行解释一下前面的那个Hello, world!程序,尽管很简单,但却可以对
Qt 程序的结构有一个清楚的认识。现在再把代码贴过来:
#include "QApplication"
#include "QLabel"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel *label = new QLabel("Hello, world!");
label->show();
return app.exec();
}
第1 行和第2 行就是需要引入的头文件。和普通的C++程序没有什么两样,如果要使用某个组件,就必须要引入相应的头文件,这类似于Java 的import 机制。
值得说明的是,Qt 中头文件和类名是一致的。也就是说,如果你要使用某个类的话,它的类名就是它的头文件名。
第3 行是空行
第4 行是main 函数函数头。这与普通的C++程序没有什么两样,学过C++的都明白。因此你可以看到,实际上,Qt 完全通过普通的main 函数进入,这不同于wxWidgets,因为wxWidgets 的Hello, world 需要你继承它的一个wxApp 类,并覆盖它的wxApp::OnInit 方法,系统会自动将OnInit 编译成入口函数。不过在Qt 中,就不需要这些了。
第5 行,噢噢,大括号…
第6 行,创建一个QApplication 对象。这个对象用于管理应用程序级别的资源。
QApplication 的构造函数要求两个参数,分别来自main 的那两个参数,因此,Qt 在一定程度上是支持命令行参数的。
第7 行,创建一个QLabel 对象,并且能够显示Hello, world!字符串。和其他
库的Label 控件一样,这是用来显示文本的。在Qt 中,这被称为一个widget(翻译出来是小东西,不过这个翻译并不好…),它等同于Windows 技术里面的控件(controls)和容器(containers)。也就是说,widget 可以放置其他的 widget,就像Swing 的组件。大多数Qt 程序使用QMainWindow 或者QDialog 作为顶级组件,但Qt 并不强制要求这点。在这个例子中,顶级组件就是一个QLabel。
第8 行,使这个label 可见。组件创建出来之后通常是不可见的,要求我们手动的使它们可见。这样,在创建出组建之后我们就可以对它们进行各种定制,以避免出现之后在屏幕上面会有闪烁。
第9 行,将应用程序的控制权移交给Qt。这时,程序的事件循环就开始了,也就是说,这时可以相应你发出的各种事件了。这类似于gtk+最后的一行
gtk_main()。
第10 行,大括号……程序结束了。
注意,我们并没有使用delete 去删除创建的QLabel,因为在程序结束后操作系统会回收这个空间——这只是因为这个QLabel 占用的内存比较小,但有时候这么做会引起麻烦的,特别是在大程序中,因此必须小心。
好了,程序解释完了。按照正常的流程,下面应该编译。前面也提过,Qt 的编译不能使用普通的make,而必须先使用qmake 进行预编译。所以,第一步应该是在工程目录下使用
qmake -project
命令创建.pro 文件(比如说是叫helloworld.pro)。然后再在.pro 文件目录下使
用qmake helloworld.pro (make)
或者
qmake -tp vc helloworld.pro (nmake)
生成makefile,然后才能调用make 或者是nmake 进行编译。不过因为我们使用的是IDE,所以这些步骤就不需要我们手动完成了。
值得说明一点的是,这个qmake 能够生成标准的makefile 文件,因此完全可以利用qmake 自动生成makefile——这是题外话。
好了,下面修改一下源代码,把QLabel 的创建一句改成
QLabel *label = new QLabel("
Hello, world!
");
运行一下:
同Swing 的JLabel 一样,Qt 也是支持HTML 解析的。
好了,这个Hello, world 就说到这里!明确一下Qt 的程序结构,在一个Qt 源
代码中,一下两条语句是必不可少的:
QApplication app(argc, argv);
//...
return app.exec();
看过了简单的Hello, world! 之后,下面来看看Qt 最引以为豪的信号槽机制!
所谓信号槽,简单来说,就像是插销一样:一个插头和一个插座。怎么说呢?当某种事件发生之后,比如,点击了一下鼠标,或者按了某个按键,这时,这个组件就会发出一个信号。就像是广播一样,如果有了事件,它就漫天发声。这时,如果有一个槽,正好对应上这个信号,那么,这个槽的函数就会执行,也就是回调。就像广播发出了,如果你感兴趣,那么你就会对这个广播有反应。干巴巴的解释很无力,还是看代码:
#include "QtGui/QApplication"
#include "QtGui/QPushButton"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton *button = new QPushButton("Quit");
QObject::connect(button, SIGNAL(clicked()), &a,
SLOT(quit()));
button->show();
return a.exec();
}
这是在Qt Creator 上面新建的文件,因为前面已经详细的说明怎么新建工程,所以这里就不再赘述了。这个程序很简单,只有一个按钮,点击之后程序退出。
(顺便说一句,Qt 里面的button 被叫做QPushButton,真搞不明白为什么一个简单的button 非得加上push 呢?呵呵)
主要是看这一句:
QObject::connect(button, SIGNAL(clicked()), &a,
SLOT(quit()));
QObject 是所有类的根。Qt 使用这个QObject 实现了一个单根继承的C++。它里
面有一个connect 静态函数,用于连接信号槽。
当一个按钮被点击时,它会发出一个clicked 信号,意思是,向周围的组件们声明:我被点击啦!当然,其它很多组件都懒得理他。如果对它感兴趣,就告诉QObject 说,你帮我盯着点,只要button 发出clicked 信号,你就告诉我——
想了想之后,说,算了,你也别告诉我了,直接去执行我的某某某函数吧!就这样,一个信号槽就形成了。具体来说呢,这个例子就是QApplication 的实例a说,如果button 发出了clicked 信号,你就去执行我的quit 函数。所以,当我们点击button 的时候,a 的quit 函数被调用,程序退出了。所以,在这里,clicked()就是一个信号,而quit()就是槽,形象地说就是把这个信号插进这个槽里面去。
Qt 使用信号槽机制完成了事件监听操作。这类似与Swing 里面的listener 机制,只是要比这个listener 简单得多。以后我们会看到,这种信号槽的定义也异常的简单。值得注意的是,这个信号槽机制仅仅是使用的QObject 的connect 函数,其他并没有什么耦合——也就是说,完全可以利用这种机制实现你自己的信号监听!不过,这就需要使用qmake 预处理一下了!
细心的你或许发现,在Qt Creator 里面,SIGNAL 和SLOT 竟然变颜色了!没错,Qt 确实把它们当成了关键字!实际上,Qt 正是利用它们扩展了C++语言,因此才需要使用qmake 进行预处理,比便使普通的C++编译器能够顺利编译。另外,这里的signal 和Unix 系统里面的signal 没有任何的关系!哦哦,有一点关系,那就是名字是一样的!
信号槽机制是Qt 关键部分之一,以后我们还会再仔细的探讨这个问题的。
同Swing 类似,Qt 也提供了几种组件定位的技术。其中就包括绝对定位和布局定位。
顾名思义,绝对定位就是使用最原始的定位方法,给出这个组件的坐标和长宽值。
这样,Qt 就知道该把组件放在哪里,以及怎么设置组件的大小了。但是这样做的一个问题是,如果用户改变了窗口大小,比如点击了最大化或者拖动窗口边缘,这时,你就要自己编写相应的函数来响应这些变化,以避免那些组件还只是静静地呆在一个角落。或者,更简单的方法是直接禁止用户改变大小。
不过,Qt 提供了另外的一种机制,就是布局,来解决这个问题。你只要把组件放入某一种布局之中,当需要调整大小或者位置的时候,Qt 就知道该怎样进行调整。这类似于Swing 的布局管理器,不过Qt 的布局没有那么多,只有有限的几个。
来看一下下面的例子:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
window->setWindowTitle("Enter your age");
QSpinBox *spinBox = new QSpinBox;
QSlider *slider = new QSlider(Qt::Horizontal);
spinBox->setRange(0, 130);
slider->setRange(0, 130);
QObject::connect(slider, SIGNAL(valueChanged(int)),
spinBox, SLOT(setValue(int)));
QObject::connect(spinBox, SIGNAL(valueChanged(int)),
slider, SLOT(setValue(int)));
spinBox->setValue(35);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(spinBox);
layout->addWidget(slider);
window->setLayout(layout);
window->show();
return app.exec();
}
这里使用了两个新的组件:QSpinBox 和QSlider,以及一个新的顶级窗口
QWidget。QSpinBox 是一个有上下箭头的微调器,QSlider 是一个滑动杆,只要运行一下就会明白到底是什么东西了。
代码并不是那么难懂,还是来简单的看一下。首先创建了一个QWidget 的实例,调用setWindowTitle 函数来设置窗口标题。然后创建了一个QSpinBox 和QSlider,分别设置了它们值的范围,使用的是setRange 函数。然后进行信号槽的链接。这点后面再详细说明。然后是一个QHBoxLayout,就是一个水平布局,按照从左到右的顺序进行添加,使用addWidget 添加好组件后,调用QWidget的 setLayout 把QWidget 的layout 设置为我们定义的这个Layout,这样,程序就完成了!
编译运行一下,可以看到效果:
如果最大化的话:
虽然我并没有添加任何代码,但是那个layout 就已经明白该怎样进行布局。
或许你发现,那两个信号槽的链接操作会不会产生无限递归?因为steValue 就会引发valueChanged 信号!答案是不会。这两句语句实现了,当spinBox 发出valueChanged 信号的时候,会回调slider 的setValue,以更新slider 的值;
而slider 发出 valueChanged 信号的时候,又会回调slider 的setValue。但是,如果新的value 和旧的value 是一样的话,是不会发出这个信号的,因此避免了无限递归。
迷糊了吧?举个例子看。比如下面的spinBox->setValue(35)执行的时候,首先,spinBox 会将自己的值设为35,这样,它的值与原来的不一样了(在没有setValue之前的时候,默认值是0),于是它发出了valueChanged 信号。slider 接收到这个信号,于是回调自己的setValue 函数,将它的值也设置成35,它也发出了valueChanged 信号。当然,此时spinBox 又收到了,不过它发现,这个35 和它本身的值是一样的,于是它就不发出信号,所以信号传递就停止了。
那么,你会问,它们是怎么知道值的呢?答案很简单,因为你的信号和槽都接受了一个int 参数!新的值就是通过这个进行传递的。实际上,我们利用Qt 的信号槽机制完成了一个数据绑定,使两个组件或者更多组件的状态能够同步变化。
Qt 一共有三种主要的layout,分别是:
QHBoxLayout- 按照水平方向从左到右布局;
QVBoxLayout- 按照竖直方向从上到下布局;
QGridLayout- 在一个网格中进行布局,类似于HTML 的table。
layout 使用addWidget 添加组件,使用addLayout 可以添加子布局,因此,这就有了无穷无尽的组合方式。
我是在Windows 上面进行编译的,如果你要是在其他平台上面,应用程序就会有不同的样子:
还记得前面曾经说过,Qt 不是使用的原生组件,而是自己绘制模拟的本地组件的样子,不过看看这个截图,它模拟的不能说百分百一致,也可说是惟妙惟肖了…
今天来说一下有关Qt API 文档的使用。因为Qt 有一个商业版本,因此它的文档十分健全,而且编写良好。对于开发者来说,查看文档时开发必修课之一——没有人能够记住那么多API 的使用!
在Qt 中查看文档是一件很简单的事情。如果你使用QtCreator,那么左侧的Help按钮就是文档查看入口。否则的话,你可以在Qt 的安装目录下的bin 里面的assistant.exe 中看到Qt 的文档。在早期版本中,Qt 的文档曾以HTML 格式发布,不过在2009.03 版中我没有找到 HTML 格式的文档,可能Qt 已经把它全部换成二进制格式的了吧?——当然,如果你全部安装了Qt 的组件,是可以在开始菜单中找到assistant 的!
assistant 里面的文档有很多项:
其中,第一个是帮助的帮助:-);第二个是Qt Designer 的帮助;第三个是Qt
Linguist 的帮助;第四个是QMake 的帮助;最后一个是Qt 的API 文档,在
QtCreator 中默认打开的就是这部分。
不过,关于文档的内容这里实在不好阐述,因为整个文档太大了,我也并没有看过多少,很多时候都是随用随查,就好像是字典一样——谁也不会天天没事抱着本字典去看不是?还有就是这里的文档都是英文的,不过如果是做开发的话,了解一些英文还是很有帮助的,不是吗?
首先说明一点,在C++ GUI Programming with Qt4, 2nd 中,这一章连同以后的若干章一起,完成了一个比较完整的程序——一个模仿Excel 的电子表格。不过这个程序挺大的,而且书中也没有给出完整的源代码,只是分段分段的——我不喜欢这个样子,我想要看到我写出来的是什么东西,这是最主要的,而不是慢慢的过上几章的内容才能看到自己的作品。所以,我打算换一种方式,每章只给出简单的知识,但是每章都能够运行出东西来。好了,扯完了,下面开始!
以前说的主要是一些基础知识,现在我们来真正做一个东西——一个查找对话框。什么?什么叫查找对话框?唉唉,先看看我们的最终作品吧!
好了,首先新建一个工程,就叫FindDialog 吧!嗯,当然还是Qt Gui Application,然后最后一步注意,Base Dialog 选择QDialog,而不是默认的QMainWindow,因为我们要学习建立对话框嘛!名字随便起,不过我就叫finddialog 啦!
Ganarate form 还是不要的。然后Finish 就好了。
打开finddialog.h,开始编写头文件。
class QCheckBox;
class QLabel;
class QLineEdit;
class QPushButton;
class FindDialog : public QDialog
{
Q_OBJECT
public:
FindDialog(QWidget *parent = 0);
~FindDialog();
signals:
void findNext(const QString &str, Qt::CaseSensitivity
cs);
void findPrevious(const QString &str,
Qt::CaseSensitivity cs);
private slots:
void findClicked();
void enableFindButton(const QString &text);
private:
QLabel *label;
QLineEdit *lineEdit;
QCheckBox *caseCheckBox;
QCheckBox *backwardCheckBox;
QPushButton *findButton;
QPushButton *closeButton;
};
大家都是懂得C++的啊,所以什么#ifndef,#define 和#endif 的含义和用途就
不再赘述了。
首先,声明四个用到的类。这里做的是前向声明,否则的话是编译不过的,因为编译器不知道这些类是否存在。简单来说,所谓前向声明就是告诉编译器,我要用这几个类,而且这几个类存在,你就不要担心它们存不存在的问题啦!
然后是我们的FindDialog,继承自QDialog。
下面是一个重要的东西:Q_OBJECT。这是一个宏。凡是定义信号槽的类都必须声明这个宏。至于为什么,我们以后再说。
然后是public 的构造函数和析构函数声明。
然后是一个signal:,这是Qt 的关键字——还记得前面说过的嘛?Qt 扩展了C++语言,因此它有自己的关键字——这是对信号的定义,也就是说,FindDialog
有两个public 的信号,它可以在特定的时刻发出这两个信号,就这里来说,如果用户点击了Find 按钮,并且选中了Search backward,就会发出
findPrevious(),否则发出findNext()。
紧接着是private slots:的定义,和前面的signal 一样,这是私有的槽的定义。
也就是说,FindDialog 具有两个槽,可以接收某些信号,不过这两个槽都是私有的。
为了slots 的定义,我们需要访问FindDialog 的组件,因此,我们把其中的组
件定义为成员变量以便访问。正是因为需要定义这些组件,才需要对它们的类型进行前向声明。因为我们仅仅使用的是指针,并不涉及到这些类的函数,因此并不需要include 它们的头文件——当然,你想直接引入头文件也可以,不过那样的话编译速度就会慢一些。
好了,头文件先说这些,下一篇再说源代码啦!休息,休息一下!
接着前一篇,下面是源代码部分:
FindDialog::FindDialog(QWidget *parent)
: QDialog(parent)
{
label = new QLabel(tr("Find &what:"));
lineEdit = new QLineEdit;
label->setBuddy(lineEdit);
caseCheckBox = new QCheckBox(tr("Match &case"));
backwardCheckBox = new QCheckBox(tr("Search
&backford"));
findButton = new QPushButton(tr("&Find"));
findButton->setDefault(true);
findButton->setEnabled(false);
closeButton = new QPushButton(tr("Close"));
connect(lineEdit, SIGNAL(textChanged(const
QString&)), this, SLOT(enableFindButton(const
QString&)));
connect(findButton, SIGNAL(clicked()), this,
SLOT(findClicked()));
connect(closeButton, SIGNAL(clicked()), this,
SLOT(close()));
QHBoxLayout *topLeftLayout = new QHBoxLayout;
topLeftLayout->addWidget(label);
topLeftLayout->addWidget(lineEdit);
QVBoxLayout *leftLayout = new QVBoxLayout;
leftLayout->addLayout(topLeftLayout);
leftLayout->addWidget(caseCheckBox);
leftLayout->addWidget(backwardCheckBox);
QVBoxLayout *rightLayout = new QVBoxLayout;
rightLayout->addWidget(findButton);
rightLayout->addWidget(closeButton);
rightLayout->addStretch();
QHBoxLayout *mainLayout = new QHBoxLayout;
mainLayout->addLayout(leftLayout);
mainLayout->addLayout(rightLayout);
setLayout(mainLayout);
setWindowTitle(tr("Find"));
setFixedHeight(sizeHint().height());
}
FindDialog::~FindDialog()
{
}
void FindDialog::findClicked()
{
QString text = lineEdit->text();
Qt::CaseSensitivity cs = caseCheckBox->isChecked() ?
Qt::CaseInsensitive : Qt::CaseSensitive;
if(backwardCheckBox->isChecked()) {
emit findPrevious(text, cs);
} else {
emit findNext(text, cs);
}
}
void FindDialog::enableFindButton(const QString &text)
{
findButton->setEnabled(!text.isEmpty());
}
CPP 文件要长一些哦——不过,它们的价钱也会更高,嘿嘿——嗯,来看代码,第一行include 的是QtGui。Qt 是分模块的,记得我们建工程的时候就会问你,使用哪些模块?QtCore?QtGui?QtXml?等等。这里,我们引入QtGui,它包括了QtCore 和QtGui 模块。不过,这并不是最好的做法,因为QtGui 文件很大,包括了GUI 的所有组件,但是很多组件我们根本是用不到的——就像Swing 的import,你可以import 到类,也可以使用*,不过都不会建议使用*,这里也是一样的。我们最好只引入需要的组件。不过,那样会把文件变长,现在就先用QtGui 啦,只要记得正式开发时不能这么用就好啦!
构造函数有参数初始化列表,用来调用父类的构造函数,相当于Java 里面的
super()函数。这是C++的相关知识,不是Qt 发明的,这里不再赘述。
然后新建一个QLabel。还记得前面的Hello, world!里面也使用过QLabel 吗?
那时候只是简单的传入一个字符串啊!这里怎么是一个函数tr()?函数tr()全
名是 QObject::tr(),被它处理的字符串可以使用工具提取出来翻译成其他语
言,也就是做国际化使用。这以后还会仔细讲解,只要记住,Qt 的最佳实践:
如果你想让你的程序国际化的话,那么,所有用户可见的字符串都要使用
QObject::tr()!但是,为什么我们没有写QObject::tr(),而仅仅是tr()呢?
原来,tr()函数是定义在Object 里面的,所有使用了Q_OBJECT 宏的类都自动具有tr()函数。
字符串中的&代表快捷键。注意看下面的findButton 的&Find,它会生成Find
字符串,当你按下 Alt+F 的时候,这个按钮就相当于被点击——这么说很难受,相信大家都明白什么意思。同样,前面label 里面也有一个&,因此它的快捷键就是 Alt+W。不过,这个label 使用了setBuddy 函数,它的意思是,当label获得焦点时,比如按下Alt+W,它的焦点会自动传给它的 buddy,也就是lineEdit。看,这就是伙伴的含义(buddy 英文就是伙伴的意思)。
后面几行就比较简单了:创建了两个QCheckBox,把默认的按钮设为findButton,把findButton 设为不可用——也就是变成灰色的了。
再下面是三个connect 语句,用来连接信号槽。可以看到,当lineEdit 发出
textChanged(const QString&)信号时,FindDialog 的enableFindButton(const
QString&)函数会被调用——这就是回调,是有系统自动调用,而不是你去调用——当findButton 发出clicked()信号时,FindDialog 的findClicked()函数
会被调用;当closeButton 发出clicked()信号时,FindDialog 的 close()函数
会被调用。注意,connect()函数也是QObject 的,因为我们继承了QObject,所以能够直接使用。
后面的很多行语句都是layout 的使用,虽然很复杂,但是很清晰——编写layout布局最重要一点就是思路清楚,想清楚哪个套哪个,就会很好编写。这里我们的对话框实际上是这个样子的:
注意那个spacer 是由rightLayout 的addStretch()添加的,就像弹簧一样,把
上面的组件“顶起来”。
最后的setWindowTitle()就是设置对话框的标题,而setFixedHeight()是设置
成固定的高度,其参数值sizeHint()返回“最理想”的大小,这里我们使用的
是height()函数去到“最理想”的高度。
好了,下面该编写槽了——虽然说是slot,但实际上它就是普通的函数,既可
以和其他函数一样使用,又可以被系统回调。
先看findClicked()函数。首先取出lineEdit 的输入值;然后判断caseCheckBox是不是选中,如果选中就返回 Qt::CaseInsensitive,否则返回
Qt::CaseSensitive,用于判断是不是大小写敏感的查找;最后,如果
backwardCheckBox 被选中,就emit(发出)信号findPrevious(),否则emit 信
号findNext。
enableFindButton()则根据lineEdit 的内容是不是变化——这是我们的
connect 连接的——来设置findButton 是不是可以使用,这个很简单,不再说
了。
这样,FindDialog.cpp 也就完成了。下面编写main.cpp——其实QtCreator 已
经替我们完成了——
#include "finddialog.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
FindDialog *dialog = new FindDialog;
dialog->show();
return app.exec();
}
运行一下看看我们的成果吧!
虽然很简单,也没有什么实质性的功能,但是我们已经能够制作对话框了——Qt的组件成百上千,不可能全部介绍完,只能用到什么学什么,更重要的是,我们已经了解了其编写思路,否则的话,即便是你拿着全世界所有的砖瓦,没有设计图纸,你也不知道怎么把它们组合成高楼大厦啊!
嘿嘿,下回见!
信号槽机制是Qt 编程的基础。通过信号槽,能够使Qt 各组件在不知道对方的情形下能够相互通讯。这就将类之间的关系做了最大程度的解耦。
槽函数和普通的C++成员函数没有很大的区别。它们也可以使virtual 的;可以被重写;可以使public、protected 或者 private 的;可以由其它的C++函数调用;参数可以是任何类型的。如果要说区别,那就是,槽函数可以和一个信号相连接,当这个信号发生时,它可以被自动调用。
connect()语句的原型类似于:
connect(sender, SIGNAL(signal), receiver, SLOT(slot));
这里,sender 和receiver 都是QObject 类型的,singal 和slot 都是没有参数
名称的函数签名。SINGAL()和SLOT()宏用于把参数转换成字符串。
深入的说,信号槽还有更多可能的用法,如下所示。
一个信号可以和多个槽相连:
connect(slider, SIGNAL(valueChanged(int)),
spinBox, SLOT(setValue(int)));
connect(slider, SIGNAL(valueChanged(int)),
this, SLOT(updateStatusBarIndicator(int)));
注意,如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
多个信号可以连接到一个槽:
connect(lcd, SIGNAL(overflow()),
this, SLOT(handleMathError()));
connect(calculator, SIGNAL(divisionByZero()),
this, SLOT(handleMathError()));
这是说,只要任意一个信号发出,这个槽就会被调用。
一个信号可以连接到另外的一个信号:
connect(lineEdit, SIGNAL(textChanged(const QString &)),
this, SIGNAL(updateRecord(const QString
&)));
这是说,当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
槽可以被取消链接:
disconnect(lcd, SIGNAL(overflow()),
this, SLOT(handleMathError()));
这种情况并不经常出现,因为当一个对象delete 之后,Qt 自动取消所有连接到这个对象上面的槽。
为了正确的连接信号槽,信号和槽的参数个数、类型以及出现的顺序都必须相同,
例如:
connect(ftp, SIGNAL(rawCommandReply(int, const QString
&)),
this, SLOT(processReply(int, const QString
&)));
这里有一种例外情况,如果信号的参数多于槽的参数,那么这个参数之后的那些
参数都会被忽略掉,例如:
connect(ftp, SIGNAL(rawCommandReply(int, const QString
&)),
this, SLOT(checkErrorCode(int)));
这里,const QString &这个参数就会被槽忽略掉。
如果信号槽的参数不相容,或者是信号或槽有一个不存在,或者在信号槽的连接中出现了参数名字,在Debug 模式下编译的时候,Qt 都会很智能的给出警告。
在这之前,我们仅仅在widgets 中使用到了信号槽,但是,注意到connect()函数其实是在QObject 中实现的,并不局限于GUI,因此,只要我们继承QObject类,就可以使用信号槽机制了:
class Employee : public QObject
{
Q_OBJECT
public:
Employee() { mySalary = 0; }
int salary() const { return mySalary; }
public slots:
void setSalary(int newSalary);
signals:
void salaryChanged(int newSalary);
private:
int mySalary;
};
在使用时,我们给出下面的代码:
void Employee::setSalary(int newSalary)
{
if (newSalary != mySalary) {
mySalary = newSalary;
emit salaryChanged(mySalary);
}
}
这样,当setSalary()调用的时候,就会发出salaryChanged()信号。注意这里
的if 判断,这是避免递归的方式!还记得前面提到的循环连接吗?如果没有if,当出现了循环连接的时候就会产生无限递归。
前面说过,Qt 使用的是自己的预编译器,它提供了对C++的一种扩展。利用Qt的信号槽机制,就可以把彼此独立的模块相互连接起来,不需要实现知道模块的任何细节。
为了达到这个目的,Qt 提出了一个Meta-Object 系统。它提供了两个关键的作用:信号槽和内省。
面向对象程序设计里面会讲到Smalltalk 语言有一个元类系统。所谓元类,就是这里所说的Meta-Class。如果写过HTML,会知道 HTML 标签里面也有一个,这是用于说明页面的某些属性的。同样,Qt 的Meta-Object 系统也是类似的作用。
内省又称为反射,允许程序在运行时获得类的相关信息,也就是
meta-information。什么是meta-information 呢?举例来说,像这个类叫什么
名字?它有什么属性?有什么方法?它的信号列表?它的槽列表?等等这些信息,就是这个类的meta-information,也就是“元信息”。这个机制还提供了
对国际化的支持,是QSA(Qt Script for Application)的基础。
标准C++并没有Qt 的meta-information 所需要的动态meta-information。所以,Qt 提供了一个独立的工具,moc,通过定义Q_OBJECT 宏实现到标准C++函数的转变。moc 使用纯C++实现的,因此可以再任何编译器中使用。
这种机制工作过程是:
首先,Q_OBJECT 宏声明了一些QObject 子类必须实现的内省的函数,如
metaObject(),tr(),qt_metacall()等;
第二,Qt 的moc 工具实现Q_OBJECT 宏声明的函数和所有信号;
第三,QObject 成员函数connect()和disconnect()使用这些内省函数实现信号
槽的连接。
以上这些过程是qmake,moc 和QObject 自动处理的,你不需要去考虑它们。如果实现好奇的话,可以通过查看QMetaObject 的文档和moc 的源代码来一睹芳容。
尽管Qt 提供了很方便的快速开发工具QtDesigner 用来拖放界面元素,但是现在我并不打算去介绍这个工具,原因之一在于我们的学习大体上是依靠手工编写代码,过早的接触设计工具并不能让我们对Qt 的概念突飞猛进……
前面说过,本教程很大程度上依照的是《C++ GUI Programming with Qt4, 2ndEdition》这本书。但是,这本书中接下来的部分用了很大的篇幅完成了一个简单的类似Excel 的程序。虽然最终效果看起来很不错,但我并不打算完全 依照这个程序来,因为这个程序太大,以至于我们在开始之后会有很大的篇幅接触不到能够运行的东西,这无疑会严重打击学习的积极性——至少我是如此,看不到做的东西很难受——所以,我打算重新组织一下这个程序,请大家按照我的思路试试看吧!
闲话少说,下面开始新的篇章!
就像Swing 的顶层窗口一般都是JFrame 一样,Qt 的GUI 程序也有一个常用的顶层窗口,叫做MainWindow。好了,现在我们新建一个Gui Application 项目MyApp,注意在后面选择的时候选择Base Class 是QMainWindow。
然后确定即可。此时,QtCreator 已经为我们生成了必要的代码,我们只需点击一下Run,看看运行出来的结果。
一个很简单的窗口,什么都没有,这就是我们的主窗口了。
MainWindow 继承自QMainWindow。QMainWindow 窗口分成几个主要的区域:
最上面是Window Title,用于显示标题和控制按钮,比如最大化、最小化和关
闭等;下面一些是Menu Bar,用于显示菜单;再下面一点事Toolbar areas,用于显示工具条,注意,Qt 的主窗口支持多个工具条显示,因此这里是ares,你可以把几个工具条并排显示在这里,就像Word2003 一样;工具条下面是Dockwindow areas,这是停靠窗口的显示区域,所谓停靠窗口就是像Photoshop 的工具箱一样,可以在主窗口的四周显示;再向下是Status Bar,就是状态栏;中间最大的Central widget 就是主要的工作区了。
好了,今天的内容不多,我们以后的工作就是要对这个MainWindow 进行修改,以满足我们的各种需要。
在前面的QMainWindow 的基础之上,我们开始着手建造我们的应用程序。虽然现在已经有一个框架,但是,确切地说我们还一行代码没有写呢!下面的工作就不那么简单了!在这一节里面,我们要为我们的框架添加菜单和工具条。
就像Swing 里面的Action 一样,Qt 里面也有一个类似的类,叫做QAction。顾名思义,QAction 类保存有关于这个动作,也就是action 的信息,比如它的文本描述、图标、快捷键、回调函数(也就是信号槽),等等。神奇的是,QAction
能够根据添加的位置来改变自己的样子 ——如果添加到菜单中,就会显示成一
个菜单项;如果添加到工具条,就会显示成一个按钮。这也是为什么要把菜单和按钮放在一节里面。下面开始学习!
首先,我想添加一个打开命令。那么,就在头文件里面添加一个私有的QAction
变量:
class QAcion;
//...
private:
QAction *openAction;
//...
注意,不要忘记QAction 类的前向声明哦!要不就会报错的!
然后我们要在cpp 文件中添加QAction 的定义。为了简单起见,我们直接把它定义在构造函数里面:
openAction = new QAction(tr("&Open"), this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip(tr("Open a file."));
第一行代码创建一个QAction 对象。QAction 有几个重载的构造函数,我们使用的是
QAction(const QString &text, QObject* parent)
这一个。它有两个参数,第一个text 是这个动作的文本描述,用来显示文本信息,比如在菜单中的文本;第二个是parent,一般而言,我们通常传入this 指针就可以了。我们不需要去关心这个parent 参数具体是什么,它的作用是指明这个QAction 的父组件,当这个父组件被销毁时,比如 delete 或者由系统自动销毁,与其相关联的这个QAction 也会自动被销毁。
如果你还是不明白构造函数的参数是什么意思,或者说想要更加详细的了解
QAction 这个类,那么就需要自己翻阅一下它的API 文档。前面说过有关API 的使用方法,这里不再赘述。这也是学习Qt 的一种方法,因为Qt 是一个很大的库,我们不可能面面俱到,因此只为说道用到的东西,至于你自己想要实现的功能,就需要自己去查文档了。
第二句,我们使用了setShortcut 函数。shortcut 是这个动作的快捷键。Qt 的
QKeySequence 已经为我们定义了很多内置的快捷键,比如我们使用的Open。你可以通过查阅API 文档获得所有的快捷键列表,或者是在QtCreator 中输入::
后会有系统的自动补全功能显示出来。这个与我们自己定义的有什么区别呢?简单来说,我们完全可以自己定义一个tr(“Ctrl+O”)来实现快捷键。原因在于,这是Qt 跨平台性的体现。比如PC 键盘和Mac 键盘是不一样的,一些键在PC 键盘上有,而Max 键盘上可能并不存在,或者反之,所以,推荐使用QKeySequence类来添加快捷键,这样,它会根据平台的不同来定义不同的快捷键。
第三句是setStatusTip 函数。这是添加状态栏的提示语句。状态栏就是主窗口最下面的一条。现在我们的程序还没有添加状态栏,因此你是看不到有什么作用的。
下面要做的是把这个QAction 添加到菜单和工具条:
QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
QMainWindow 有一个menuBar()函数,会返回菜单栏,也就是最上面的那一条。
如果不存在会自动创建,如果已经存在就返回那个菜单栏的指针。直接使用返回值添加一个菜单,也就是addMenu,参数是一个QString,也就是显示的菜单名字。然后使用这个QMenu 指针添加这个 QAction。类似的,使用addToolBar 函数的返回值添加了一个工具条,并且把这个QAction 添加到了上面。
好了,主要的代码已经写完了。不过,如果你只修改这些的话,是编译不过的哦!因为像menuBar()函数返回一个QMenuBar 指针,但是你并没有include 它的头文件哦!虽然没有明着写出QMenuBar 这个类,但是实际上你已经用到了它的
addMenu 函数了,所以还是要注意的!
下面给出来全部的代码:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtGui/QMainWindow>
class QAction;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QAction *openAction;
};
#endif // MAINWINDOW_H
#include <QtGui/QAction>
#include <QtGui/QMenu>
#include <QtGui/QMenuBar>
#include <QtGui/QKeySequence>
#include <QtGui/QToolBar>
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
openAction = new QAction(tr("&Open"), this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip(tr("Open a file."));
QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
}
MainWindow::~MainWindow()
{
}
main.cpp 没有修改,这里就不给出了。下面是运行结果:
很丑,是吧?不过我们已经添加上了菜单和工具条了哦!按一下键盘上的Alt+F,因为这是我们给它定义的快捷键。虽然目前挺难看,不过以后就会变得漂亮的!
想想看,Linux 的KDE 桌面可是Qt 实现的呢!
前面一节我们已经把QAction 添加到菜单和工具条上面。现在我们要添加一些图片美化一下,然后把信号槽加上,这样,我们的action 就可以相应啦!
首先来添加图标。QAction 的图标会显示在菜单项的前面以及工具条按钮上面显示。
为了添加图标,我们首先要使用Qt 的资源文件。在QtCreator 的项目上右击,选择New File…,然后选择resource file。
然后点击next,选择好位置,Finish 即可。为了使用方便,我就把这个文件建在根目录下,建议应该在仔细规划好文件之后,建在专门的 rsources 文件夹下。
完成之后,生成的是一个.qrc 文件,qrc 其实是Qt Recource Collection 的缩
写。它只是一个普通的XML 文件,可以用记事本等打开。不过,这里我们不去深究它的结构,完全利用QtCreator 操作这个文件,
点击Add 按钮,首先选择Add prefix,然后把生成的/new/prefix 改成/。这是
prefix 就是以后使用图标时需要提供的前缀,以/开头。添加过prefix 之后,
然后在工程文件中添加一个图标,再选择Add file,选择那个图标。这样完成
之后保存qrc 文件即可。
说明一下,QToolBar 的图标大小默认是3232,菜单默认是1616。如果提供的图标小于要求的尺寸,则不做操作,Qt 不会为你放大图片;反之,如果提供的图标文件大于相应的尺寸要求,比如是64*64,Qt 会自动缩小尺寸。
图片的路径怎么看呢?可以看出,Qt 的资源文件视图使用树状结构,根是/,叶子节点就是图片位置,连接在一起就是路径。比如这张图片的路径就是
/Open.png。
注意,为了简单起见,我们没有把图标放在专门的文件夹中。正式的项目中应该
单独有一个resources 文件夹放资源文件的。
然后回到前面的mainwindow.cpp,在构造函数中修改代码:
openAction = new QAction(tr("&Open"), this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip(tr("Open a file."));
openAction->setIcon(QIcon(":/Open.png")); // Add code.
我们使用setIcon 添加图标。添加的类是QIcon,构造函数需要一个参数,是一
个字符串。由于我们要使用qrc 中定义的图片,所以字符串以 : 开始,后面跟
着prefix,因为我们先前定义的prefix 是/,所以就需要一个/,然后后面是file
的路径。这是在前面的qrc 中定义的,打开 qrc 看看那张图片的路径即可。
好了,图片添加完成,然后点击运行,看看效果吧!
瞧!我们只需要修改QAction,菜单和工具条就已经为我们做好了相应的处理,
还是很方便的!
下一步,为QAction 添加事件响应。还记得Qt 的事件响应机制是基于信号槽吗?
点击QAction 会发出triggered()信号,所以,我们要做的是声名一个slot,然
后connect 这个信号。
mainwindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void open();
private:
QAction *openAction;
};
因为我们的open()目前只要在类的内部使用,因此定义成private slots 即可。
然后修改cpp 文件:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
openAction = new QAction(tr("&Open"), this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip(tr("Open a file."));
openAction->setIcon(QIcon(":/Open.png"));
connect(openAction, SIGNAL(triggered()), this,
SLOT(open()));
QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
}
void MainWindow::open()
{
QMessageBox::information(NULL, tr("Open"), tr("Open a
file"));
}
注意,我们在open()函数中简单的弹出一个标准对话框,并没有其他的操作。
编译后运行,看看效果:
好了,关于QAction 的动作也已经添加完毕了!
至此,QAction 有关的问题先告一段落。最后说一下,如果你还不知道怎么添加子菜单的话,看一下QMenu 的API,里面会有一个addMenu 函数。也就是说,创建一个QMenu 然后添加就可以的啦!
今天的内容主要还是继续完善前面的那个程序。我们要为我们的程序加上一个状态栏。
状态栏位于主窗口的最下方,提供一个显示工具提示等信息的地方。一般地,当窗口不是最大化的时候,状态栏的右下角会有一个可以调节大小的控制点;当窗口最大化的时候,这个控制点会自动消失。Qt 提供了一个QStatusBar 类来实现
状态栏。
Qt 具有一个相当成熟的GUI 框架的实现——这一点感觉比Swing 要强一些
——Qt 似乎对GUI 的开发做了很多设计,比如 QMainWindow 类里面就有一个statusBar()函数,用于实现状态栏的调用。类似menuBar()函数,如果不存在状态栏,该函数会自动创建一个,如果已经创建则会返回这个状态栏的指针。如果你要替换掉已经存在的状态栏,需要使用QMainWindow 的setStatusBar()函数。
在Qt 里面,状态栏显示的信息有三种类型:临时信息、一般信息和永久信息。
其中,临时信息指临时显示的信息,比如QAction 的提示等,也可以设置自己的临时信息,比如程序启动之后显示Ready,一段时间后自动消失——这个功能可以使用QStatusBar 的showMessage()函数来实现;一般信息可以用来显示页码之类的;永久信息是不会消失的信息,比如可以在状态栏提示用户Caps Lock键被按下之类。
QStatusBar 继承自QWidget,因此它可以添加其他的QWidget。下面我们在
QStatusBar 上添加一个QLabel。
首先在class 的声明中添加一个私有的QLabel 属性:
private:
QAction *openAction;
QLabel *msgLabel;
然后在其构造函数中添加:
msgLabel = new QLabel;
msgLabel->setMinimumSize(msgLabel->sizeHint());
msgLabel->setAlignment(Qt::AlignHCenter);
statusBar()->addWidget(msgLabel);
这里,第一行创建一个QLabel 的对象,然后设置最小大小为其本身的建议大小
——注意,这样设置之后,这个最小大小可能是变化的——最后设置显示规则是水平居中(HCenter)。最后一行使用statusBar()函数将这个label 添加到状态栏。编译运行,将鼠标移动到工具条或者菜单的 QAction 上,状态栏就会有相应的提示:
看起来是不是很方便?只是,我们很快发现一个问题:当没有任何提示时,状态栏会有一个短短的竖线:
这是什么呢?其实,这是QLabel 的边框。当没有内容显示时,QLabel 只显示出自己的一个边框。但是,很多情况下我们并不希望有这条竖线,于是,我们对statusBar()进行如下设置:
statusBar()->setStyleSheet(QString("QStatusBar::item{bord
er: 0px}"));
这里先不去深究这句代码是什么意思,简单来说,就是把QStatusBar 的子组件的border 设置为0,也就是没有边框。现在再编译试试吧!那个短线消失了!
QStatusBar 右下角的大小控制点可以通过setSizeGripEnabled()函数来设置是否存在,详情参见API 文档。
好了,现在,我们的状态栏已经初步完成了。由于QStatusBar 可以添加多个
QWidget,因此,我们可以构建出很复杂的状态栏。
《Qt 学习之路》已经写到了第15 篇,然而现在再写下去却有点困难,原因是当初并没有想到会连续的写下去,因此并没有很好的计划这些内容究竟该怎样去写。虽然前面说过,本教程主要线路参考《C++ Gui Programming with Qt 4, 2ndEdition》,然而最近的章节由于原文是一个比较完整的项目而有所改变,因此现在不知道该从何写起。
我并不打算介绍很多组件的使用,因为Qt 有很多组件,各种组件用法众多,根本不可能介绍完,只能把API 放在手边,边用边查。所以,对于很多组件我只是简单的介绍一下,具体用法还请自行查找(确切地说,我知道的也并不多,很多时候还是要到API 里面去找)。
下面还是按照我们的进度,从Qt 的标准对话框开始说起。所谓标准对话框,其实就是Qt 内置的一些对话框,比如文件选择、颜色选择等等。今天首先介绍一下QFileDialog。
QFileDialog 是Qt 中用于文件打开和保存的对话框,相当于Swing 里面的
JFileChooser。下面打开我们前面使用的工程。我们已经很有先见之明的写好了一个打开的action,还记得前面的代码吗?当时,我们只是弹出了一个消息对话框(这也是一种标准对话框哦~)用于告知这个信号槽已经联通,现在我们要写真正的打开代码了!
修改MainWindow 的open 函数:
void MainWindow::open()
{
QString path = QFileDialog::getOpenFileName(this, tr("Open
Image"), ".", tr("Image Files(*.jpg *.png)"));
if(path.length() == 0) {
QMessageBox::information(NULL, tr("Path"), tr("You didn't
select any files."));
} else {
QMessageBox::information(NULL, tr("Path"), tr("You selected
") + path);
}
}
编译之前别忘记include QFileDialog 哦!然后运行一下吧!点击打开按钮,就会弹出打开对话框,然后选择文件或者直接点击取消,会有相应的消息提示。
QFileDialog 提供了很多静态函数,用于获取用户选择的文件。这里我们使用的是
继续来说Qt 的标准对话框,这次说说QColorDialog。这是Qt 提供的颜色选择
对话框。
使用QColorDialog 也很简单,Qt 提供了getColor()函数,类似于QFileDialog
的getOpenFileName(),可以直接获得选择的颜色。我们还是使用前面的QAction来测试下这个函数:
QColor color = QColorDialog::getColor(Qt::white, this);
QString msg = QString("r: %1, g: %2, b:
%3").arg(QString::number(color.red()),
QString::number(color.green()),
QString::number(color.blue()));
QMessageBox::information(NULL, "Selected color", msg);
不要忘记include QColorDialog 哦!这段代码虽然很少,但是内容并不少。
第一行QColorDialog::getColor()调用了QColorDialog 的static 函数
getColor()。这个函数有两个参数,第一个是QColor 类型,是对话框打开时默认选择的颜色,第二个是它的parent。
第二行比较长,涉及到QString 的用法。如果我没记错的话,这些用法还没有提到过,本着“有用就说”的原则,尽管这些和 QColorDialog 毫不相干,这里还是解释一下。QString(“r: %1, g: %2, b: %3″)创建了一个QString 对象。我
们使用了参数化字符串,也就是那些%1 之类。在Java 的properties 文件中,字符参数是用{0}, {1}之类实现的。其实这都是一些占位符,也就是,后面会用别的字符串替换掉这些值。占位符的替换需要使用QString 的arg()函数。这个函数会返回它的调用者,因此可以使用链式调用写法。它会按照顺序替换掉占位符。然后是QString::number()函数,这也是QString 的一个 static 函数,作用就是把int、double 等值换成QString 类型。这里是把QColor 的R、G、B 三个值输出了出来。关于QString 类,我们会在以后详细说明。
第三行就比较简单了,使用一个消息对话框把刚刚拼接的字符串输出。
现在就可以运行这个测试程序了。看上去很简单,不是吗?
QColorDialog 还有一些其他的函数可以使用。
QColorDialog::setCustomColor()可以设置用户自定义颜色。这个函数有两个
值,第一个是自定义颜色的索引,第二个是自定义颜色的RGB 值,类型是QRgb,大家可以查阅API 文档来看看这个类的使用,下面只给出一个简单的用发:
QColorDialog::setCustomColor(0, QRgb(0x0000FF));
getColor()还有一个重载的函数,签名如下:
QColorDialog::( const QColor & initial, QWidget * parent,
const QString & title, ColorDialogOptions options = 0 )
第一个参数initial 和前面一样,是对话框打开时的默认选中的颜色;
第二个参数parent,设置对话框的父组件;
第三个参数title,设置对话框的title;
第四个参数options,是QColorDialog::ColorDialogOptions 类型的,可以设
置对话框的一些属性,如是否显示Alpha 值等,具体属性请查阅API 文档。特别的,这些值是可以使用OR 操作的。
QColorDialog 相对简单一些,API 文档也很详细,大家遇到问题可以查阅文档的哦!
这是一篇很简单的文章,仅仅是用来说明一下一个参数的作用,因此我把它写成了tip,而不是接下来的17.
程序写的多了,你会发现几乎所有的Qt 类的构造函数都会有一个parent 参数。这个参数通常是QObject* 或者是 QWidget* 类型的。很多情况下它都会有一个初始值0,因此,即便你不去给它复制也没有丝毫的问题。于是,稍微偷懒一下,就会不自觉的忽略了这个参数。那么,这个参数到底是干什么用的呢?
其实,这个参数有很多用处。就像它的名字一样,这个参数指定了组件的父组件。
对于一个对话框来说,对话框一般是不作为顶层容器出现的,因此在任务栏上一般是没有对话框的位置的。怎么指定这个对话框不是顶层容器呢?有父组件的组件不就不是顶层容器了吗?因此,只要你指定对话框的parent 属性,任务栏就不会出现它的身影。当然,如果你不指定,这个对话框就成为顶层容器了,任务栏会给它留个位置的——利用这个特性,就可以实现特殊对话框可以在任务栏出现的效果,比如“关于”对话框的出现。
另外比较通用,也是很重要的作用是,parent 参数指明了组件的父组件,这样,当父组件delete 时,Qt 可以保证所有子组件——也就是 parent 指针指向这个组件的所有组件——都会被正确的delete 掉。这是Qt 能够帮助我们管理一部分内存的原因所在。Qt 是通过遍历parent 属性来防止了这一部分内存泄漏的。因此,必要情况下还是不要忘记设置这个parent 属性。当然,如果你不声明这个属性,当整个程序关闭时,操作系统会回收内存——因此我们所说的内存泄漏一般是指我们自己写的应用程序的内部,而不会影响到整个操作系统——当然,如果你实现太可恶,操作系统也会受不了自动关掉你的程序的:-)
这次来说一下QMessageBox 以及类似的几种对话框。其实,我们已经用过
QMessageBox 了,就在之前的几个程序中。不过,当时是大略的说了一下,现在专门来说说这几种对话框。
先来看一下最熟悉的QMessageBox::information。我们在以前的代码中这样使用过:
QMessageBox::information(NULL, "Title", "Content",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
下面是一个简单的例子:
现在我们从API 中看看它的函数签名:
static StandardButton QMessageBox::information ( QWidget *
parent, const QString & title, const QString & text,
StandardButtons buttons = Ok, StandardButton defaultButton
= NoButton );
首先,它是static 的,所以我们能够使用类名直接访问到(怎么看都像废话…);
然后看它那一堆参数,第一个参数parent,说明它的父组件;第二个参数title,也就是对话框的标题;第三个参数text,是对话框显示的内容;第四个参数buttons,声明对话框放置的按钮,默认是只放置一个OK 按钮,这个参数可以使用或运算,例如我们希望有一个Yes 和一个No 的按钮,可以使用
QMessageBox::Yes | QMessageBox::No,所有的按钮类型可以在QMessageBox声明的StandarButton 枚举中找到;第五个参数defaultButton 就是默认选中的按钮,默认值是NoButton,也就是哪个按钮都不选中。这么多参数,豆子也是记不住的啊!所以,我们在用QtCreator 写的时候,可以在输入QMessageBox::information 之后输入(,稍等一下,QtCreator 就会帮我们把函数签名显示在右上方了,还是挺方便的一个功能!
Qt 提供了五个类似的接口,用于显示类似的窗口。具体代码这里就不做介绍,只是来看一下样子吧!
QMessageBox::critical(NULL, "critical", "Content",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QMessageBox::warning(NULL, "warning", "Content",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QMessageBox::question(NULL, "question", "Content",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QMessageBox::about(NULL, “About”, “About this application”);
请注意,最后一个about()函数是没有后两个关于button 设置的按钮的!
QMessageBox 对话框的文本信息时可以支持HTML 标签的。例如:
QMessageBox::about(NULL, "About", "About this <font
color='red'>application</font>");
运行效果如下:
如果我们想自定义图片的话,也是很简单的。这时候就不能使用这几个static
的函数了,而是要我们自己定义一个QMessagebox 来使用:
QMessageBox message(QMessageBox::NoIcon, "Title", "Content
with icon.");
message.setIconPixmap(QPixmap("icon.png"));
message.show();
需要注意的是,同其他的程序类似,我们在程序中定义的相对路径都是要相对于运行时的.exe 文件的地址的。比如我们写”icon.png”,意思是是在.exe 的当前目录下寻找一个”icon.png”的文件。这个程序的运行效果如下:
还有一点要注意,我们使用的是png 格式的图片。因为Qt 内置的处理图片格式是png,所以这不会引起很大的麻烦,如果你要使用jpeg 格式的图片的话,Qt是以插件的形式支持的。在开发时没有什么问题,不过如果要部署的话,需要注意这一点。
最后再来说一下怎么处理对话框的交互。我们使用QMessageBox 类的时候有两种方式,一是使用static 函数,另外是使用构造函数。
首先来说一下static 函数的方式。注意,static 函数都是要返回一个
StandardButton,我们就可以通过判断这个返回值来对用户的操作做出相应。
QMessageBox::StandardButton rb = QMessageBox::question(NULL,
"Show Qt", "Do you want to show Qt dialog?", QMessageBox::Yes
| QMessageBox::No, QMessageBox::Yes);
if(rb == QMessageBox::Yes)
{
QMessageBox::aboutQt(NULL, "About Qt");
}
如果要使用构造函数的方式,那么我们就要自己运行判断一下啦:
QMessageBox message(QMessageBox::NoIcon, "Show Qt", "Do you
want to show Qt dialog?", QMessageBox::Yes | QMessageBox::No,
NULL);
if(message.exec() == QMessageBox::Yes)
{
QMessageBox::aboutQt(NULL, "About Qt");
}
其实道理上也是差不多的。
这是Qt 标准对话框的最后一部分。正如同其名字显示的一样,QInputDialog 用于接收用户的输入。QInputDialog 提供了一些简单的static 函数,用于快速的建立一个对话框,正像QColorDialog 提供了getColor 函数一样。
首先来看看getText 函数:
bool isOK;
QString text = QInputDialog::getText(NULL, "Input Dialog",
"Please input
your comment",
QLineEdit::Normal,
"your comment",
&isOK);
if(isOK) {
QMessageBox::information(NULL, "Information",
"Your comment is:
" + text + "",
QMessageBox::Yes |
QMessageBox::No,
QMessageBox::Yes);
}
代码比较简单,使用getText 函数就可以弹出一个可供用户输入的对话框:
下面来看一下这个函数的签名:
static QString QInputDialog::getText ( QWidget * parent,
const QString
& title,
const QString
& label,
QLineEdit::EchoMode mode = QLineEdit::Normal,
const QString
& text = QString(),
bool * ok = 0,
Qt::WindowFlags flags = 0 )
第一个参数parent,也就是那个熟悉的父组件的指针;第二个参数title 就是
对话框的标题;第三个参数label 是在输入框上面的提示语句;第四个参数mode用于指明这个QLineEdit 的输入模式,取值范围是QLineEdit::EchoMode,默认是Normal,也就是正常显示,你也可以声明为password,这样就是密码的输入显示了,具体请查阅API;第五个参数text 是QLineEdit 的默认字符串;第六个参数 ok 是可选的,如果非NLL,则当用户按下对话框的OK 按钮时,这个bool变量会被置为true,可以由这个去判断用户是按下的OK 还是Cancel,从而获知这个text 是不是有意义;第七个参数flags 用于指定对话框的样式。
虽然参数很多,但是每个参数的含义都比较明显,大家只要参照API 就可以知道了。
函数的返回值是QString,也就是用户在QLineEdit 里面输入的内容。至于这个内容有没有意义,那就要看那个ok 参数是不是true 了。
QInputDialog 不仅提供了获取字符串的函数,还有getInteger,getDouble,
getItem 三个类似的函数,这里就不一一介绍。
待续。。。学习笔记之Qt从入门到精通(三)
参考链接:https://blog.csdn.net/weixin_41486034/article/details/106379306