Qt(官方发音 [kju:t],音同 cute)是一个跨平台的 C++ 开发库,主要用来开发图形用户界面(Graphical User Interface,GUI)程序,当然也可以开发不带界面的命令行(Command User Interface,CUI)程序。
Qt 是纯 C++ 开发的,所以学好 C++ 非常有必要,对于不了解 C++ 的读者,我建议先阅读《C语言教程》,再阅读《C++教程》。C++ 是在C语言的基础上发展起来的,学完C语言就学了 C++ 的一半了。
Qt 还存在 Python、Ruby、Perl 等脚本语言的绑定, 也就是说可以使用脚本语言开发基于 Qt 的程序。开源社区就是这样,好东西就会被派生扩展,到处使用, 越来越壮大。
Qt 支持的操作系统有很多,例如通用操作系统 Windows、Linux、Unix,智能手机系统 Android、iOS、WinPhone, 嵌入式系统 QNX、VxWorks 等等。
Qt 虽然经常被当做一个 GUI 库,用来开发图形界面应用程序,但这并不是 Qt 的全部;Qt 除了可以绘制漂亮的界面(包括控件、布局、交互),还包含很多其它功能,比如多线程、访问数据库、图像处理、音频视频处理、网络通信、文件操作等,这些 Qt 都已经内置了。
Qt 是应用程序开发的一站式解决方案,有了 Qt,你就可以高枕无忧了!Qt 本身包含的模块也日益丰富, 一直有新模块和第三方模块加入进来。
大部分应用程序都可以使用 Qt 实现,除了与计算机底层结合特别紧密的,例如驱动开发,它直接使用硬件提供的编程接口,而不能使用操作系统自带的函数库。
1997年,Qt 被用来开发 Linux 桌面环境 KDE,大获成功,使 Qt 成为 Linux 环境下开发 C++ GUI 程序的事实标准。
下面的程序都使用 Qt 开发:WPS、YY语音、Skype、豆瓣电台、虾米音乐、淘宝助理、千牛、暴雪的战网客户端、VirtualBox、Opera、咪咕音乐、Google地图、Adobe Photoshop Album 等。
Linux 也是嵌入式的主力军,广泛应用于消费类电子、工业控制、军工电子、电信/网络/通讯、航空航天、汽车电子、医疗设备、仪器仪表等相关行业。
Qt 虽然也支持手机操作系统,但是由于 Android 本身已经有 Java 和 Kotlin,iOS 本身已经有 Objective-C 和 Swift,所以 Qt 在移动端的市场份额几乎可以忽略。
总起来说,Qt 主要用于桌面程序开发和嵌入式开发。
Qt 目前支持主流的 Android、iOS、WinPhone 等智能机操作系统。MeeGo 是基于 Qt 开发的操作系统,由于被诺基亚抛弃了, 只剩一代绝版诺基亚 N9 手机。
诺基亚手机部门出售给微软之后,大部分诺基亚手机系统开发人员都被遣散了。
目前基于 Tizen 的首款手机三星 Z1 在印度上市了。在 Tizen 阵营,国内有中兴、百度涉及了。Qt 开源项目里也有 Qt for Tizen 版本,有兴趣的可以去搜搜。
Qt 公司有专门针对移动开发的商业版本,20 欧元或 25 美元一个月。不做商业可以无视这个,用开源版本也是可以开发如 Android、iOS、WinPhone 应用的。
本教程关注的是传统桌面操作系统开发的,移动开发可以参考 Qt 官方的文档。
说到 Qt 的发展史,那真是一波三折,几经卖身。
Qt 最早是 1991 年由挪威的 Eirik Chambe-Eng 和 Haavard Nord 开发的, 他们随后于 1994 年 3 月 4 号正式成立奇趣科技公司(Trolltech)。Qt 原本是商业授权的跨平台开发库, 在 2000 年奇趣科技公司为开源社区发布了遵循 GPL(GNU General Public License)许可证的开源版本。
在 2008 年,诺基亚公司收购了奇趣科技公司,并增加了 LGPL(GNU Lesser General Public License)的授权模式。诺基亚联合英特尔利用 Qt 开发了全新的智能手机系统 MeeGo,可惜遭遇了微软木马屠城,诺基亚被迫放弃了 MeeGo, 而 Qt 商业授权业务也于 2011 年 3 月出售给了芬兰 IT 服务公司 Digia。
当然好消息是 Digia 于 2014 年 9 月宣布成立 Qt Company 全资子公司,独立运营 Qt 商业授权业务。目前 Qt 公司大力推广移动平台开发和商业应用, 总的来说 Qt 历经曲折,现在算是步入正轨了。
经过 20 多年的发展,Qt 已经成为最优秀的跨平台开发框架之一,在各行各业的项目开发中得到广泛应用。许多大型软件都是用 Qt 开发的,如 Autodesk Maya、Google Earth、Skype、WPS Office等。
永远不要忽视微软帝国的威胁,作为软件业的一代霸主,任何人都不要天真地试图和它做朋友,因为霸主不可能有朋友。微软的木马屠城是所有诺基亚人和芬兰人的痛,希望读者们都记牢这条。
之前提到 Qt 原本是商业授权软件,是怎么开源的呢?这就涉及 Qt 和 KDE 的纠葛了。
KDE 是 Linux 操作系统的桌面环境,与 GNOME 桌面是类似的,作为开源桌面它们竞争的情况更为多见,有兴趣的读者请猛击《Linux桌面环境》了解更多。
KDE 是采用 GPL 许可证发布的开源软件,而最初 Qt 是商业授权的,存在商业侵权风险,GNOME 则是基于开源 GTK 库的,没有什么商业风险,这一度是 GNOME 优越于 KDE 的特性。
由于 Qt 的商业授权,KDE 社区一度混乱纠结,与此同时 GNOME 则如火如荼发展起来了。 KDE 毕竟算是亲儿子,被另一波人欺负,奇趣科技公司当然看不下去了,最后是奇趣科技公司为了赢得开发者的支持,为 Qt 增加了 GPL 的开源授权, 对于开源社区而言,遵循 GPL 使用 Qt 就不需要付费,这为 KDE 解决了燃眉之急。
之后 KDE 桌面和 GNOME 都发展壮大起来,都做得越来越好了。
除了商业授权,目前 Qt 的开源授权有两种,一种是 GPL 授权,另一种是 LGPL 授权(诺基亚收购后新增)。
对这两种开源授权,简单来说,使用 GPL 版本的软件一定还是 GPL 的开源软件,无论是使用了 Qt 的程序代码还是修改了 Qt 库代码,都必须按照 GPL 来发布,这是 GPL 的传染性。
GPL 是什么都要开源,这对商业软件应用是不利的,所以诺基亚增加了 LGPL 授权 (第一个 L 可以叫 Lesser 宽松版或 Library 开发库版)。使用 LGPL 授权就可以利用 Qt 官方动态链接库,而不必开放商业代码。只要不修改和定制 Qt 库,仅使用 Qt 官方发布的动态链接库就可以不开源,这是商业友好的授权模式。
其实只要不是做商业,就不太需要关注用什么授权,以 GPL 授权发布程序代码就可以了。
世界上的开源协议有很多,有兴趣的读者请猛击《开源协议是什么?有哪些?如何选择?》了解更多内容。
最后请认准 Qt 官方网站(有时候访问速度很慢甚至不能访问,读者请自备梯子),可以查阅文档或者浏览资讯:https://www.qt.io/
世界上的 GUI 库多如牛毛,有的跨平台,有的专属于某个操作系统;有的只有 UI 功能,有的还融合了网络通信、多媒体处理、数据库访问等底层功能。
Windows 下的 GUI 解决方案比较多:
没有哪一种方案能够独霸 Windows,使用比较多的编程语言是 C++、C#、Java。
用 Qt 来开发 Windows 桌面程序有以下优点:
读者经常将 MFC 和 Qt 进行对比,MFC 只能应用在 Windows 平台,而 Qt 是跨平台的,一次编写,到处运行。
另外,Qt 已经封装了底层细节,学习 Qt 将会非常简单;而 MFC 只是给 Windows API 加了一层包装,不了解 Windows API 也学不好 MFC,大家普遍反映 MFC 难学。
我们不能简单地说 Qt 好还是 MFC 好,两者都有用武之地;但是初学者学习 Qt 会比较简单,不用应付那些烦人的 Windows API,很快就能开发出带有漂亮界面的应用程序。
Linux 下常用的 GUI 库有基于 C++ 的 Qt、GTK+、wxWidgets,以及基于 Java 的 AWT 和 Swing。其中最著名的就是 Qt 和 GTK+:KDE 桌面系统已经将 Qt 作为默认的 GUI 库,Gnome 桌面系统也将 GTK+ 作为默认的 GUI 库。
有兴趣的读者请猛击《Linux桌面系统》了解更多关于 KDE 和 Gnome 的内容。
相比 GTK+,Qt 的功能更加强大,更新也很快,比较受人们追捧。
Qt4 时代的主流就是传统部件(或叫控件)编程,所用的语言一般是 C++。 Qt5 诞生之时,正是手机移动设备蓬勃发展的时候,而传统的 C++ 部件编写的界面对手机应用程序非常方便,比如手机屏幕显示随意翻转, 这在传统桌面程序里基本遇不到,谁会将 22 寸显示器翻过来转过去呢。
为了适应手机移动应用开发, Qt5 将 QML 脚本编程提到与传统 C++ 部件编程相同的高度,力推 QML 界面编程,当然 QML 主要用于手机移动应用程序。 QML 包含大量使用手机移动设备的功能模块,比如基本部件(QtQuick 模块)、GPS 定位、渲染特效、蓝牙、NFC、WebkKit 等等。
QML 类似于网页设计的 HTML,是一种标记语言,我们可以借助 CSS 对它进行美化,也可以借助 JavaScript 进行交互。有 Web 开发经验的读者学习 QML 将非常轻松。
使用 QML 开发界面主要有以下几个优点:
既然 QML 有这么多优点,我们是不是可以不学 C++,直接学习 QML 呢?
非也!QML 只能用来进行界面设计和人机交互,也就是只能胜任 UI 部分,在底层仍然需要调用 C++ 编写的组件来完善功能,比如访问数据库、网络通信、多线程多进程、文件读写、图像处理、音频视频处理等都离不开 C++。
另外,现阶段新生的 QML 还不如传统的 C++ 部件编程那样拥有丰富的开发组件,尤其缺乏复杂的企业级应用程序所必须的树等控件。这就决定了至少现阶段,真正大型的桌面程序仍然只能选择以 C++ 为主、QML 为辅的开发模式。
相信大部分读者都没有 Web 开发经验,学习 QML 成本还是比较高的,不但要习惯 QML 这种标记性语言,还要学习 CSS 和 JavaScript。
总的来说,C++ 对于 Qt 是不可或缺的,而 QML 只是一个加分项。
C++依旧是 Qt 的主要编程语言,Qt 5 也并没有忽略它,Qt 5 添加了很多新的 C++ API,而且会持续更新。引入 QML 只是 Qt 5 提供的另外一种选择,并不是让它成为唯一的选择。
C++ 是 Qt 的基础,无论如何都要掌握,本教程也只讲解传统的 C++ 部件编程,不讲解 QML。
Qt 体积很大,有 1GB~3GB,官方下载通道非常慢,相信很多读者会崩溃,所以建议大家使用国内的镜像网站(较快),或者使用迅雷下载(很快)。
作为 Qt 下载教程,本文会同时讲解以上三种下载方式。
Qt 官网有一个专门的资源下载网站,所有的开发环境和相关工具都可以从这里下载,具体地址是:http://download.qt.io/
目录 | 说明 |
---|---|
archive | 各种 Qt 开发工具安装包,新旧都有(可以下载 Qt 开发环境和源代码)。 |
community_releases | 社区定制的 Qt 库,Tizen 版 Qt 以及 Qt 附加源码包。 |
development_releases | 开发版,有新的和旧的不稳定版本,在 Qt 开发过程中的非正式版本。 |
learning | 有学习 Qt 的文档教程和示范视频。 |
ministro | 迷你版,目前是针对 Android 的版本。 |
official_releases | 正式发布版,是与开发版相对的稳定版 Qt 库和开发工具(可以下载Qt开发环境和源代码)。 |
online | Qt 在线安装源。 |
snapshots | 预览版,最新的开发测试中的 Qt 库和开发工具。 |
archive 和 official_releases 两个目录都有最新的 Qt 开发环境安装包,我们以 archive 目录里的内容为例来说明。点击进入 archive 目录,会看到四个子目录:
目录 | 说明 |
---|---|
vsaddin | 这是 Qt 针对 Visual Studio 集成的插件,本教程基本不使用 Visual Studio ,所以不需要插件。 |
qtcreator | 这是 Qt 官方的集成开发工具,但是 qtcreator 本身是个空壳,它没有编译套件和 Qt 开发库。 除了老版本的 Qt 4 需要手动下载 qtcreator、编译套件、Qt 开发库进行搭配之外,一般用不到。对于我们教程压根不需要下载它,因为 Qt 5 有专门的大安装包,里面包含开发需要的东西,并且能自动配置好。 |
qt | 这是 Qt 开发环境的下载目录,我们刚说的 Qt 5 的大安装包就在这里面。 |
online_installers | 在线安装器,国内用户不建议使用,在线安装是龟速,还经常断线。我们教程采用的全部是离线的大安装包。 |
我们再进入 qt 子目录 ,看到如下列表:
上图没有列完整,这个 qt 目录包含了所有的 Qt 版本,从 1.0 到目前的 5.12 。
由于 Qt 5.9 是一个长期技术支持版本(Long Term Support,LTS),在未来几年里都将有更新支持,因此,本教程以 Qt 5.9 LTS 版本为例进行讲解,并且所有实例程序均使用 Qt 5.9 编译测试通过。
Qt 的上一个 LTS 版本是 5.6,它其实已经超出支持期了。
进入 5.9 目录,会看到各种子版本:
这里解释一下 Qt 的版本号,比如 5.9.8 是完整的 Qt 版本号,第一个数字 5 是大版本号(major),第二个数字 9 是小版本号(minor),第三个数字 8 是补丁号(patch)。 只要前面两个数字相同,Qt 的特性就是一致的,最后的数字是对该版本的补丁更新。也就是说本教程对 5.9.* 系列的 Qt 都是通用的,下载 5.9.* 任意一个版本都可以,这里我们以下载 5.9.0。
点击 5.9.0,进入子目录:
根据不同的操作系统,选择不同的安装包即可,不用管源码包,除非你想自己编译或者阅读源码。
我们以 Windows 安装包(qt-opensource-windows-x86-5.9.0.exe)讲解一下 Qt 安装包命名规则,其中:
请读者注意图5中最后一栏的 Details 链接(红色方框圈起来的地方)。点击 Details 链接可以进入详情页,在该页面可以看到文件的大小、校验和以及世界各地镜像下载链接(这才是重点)。
可以清楚地看到,Qt 在国内的有三个镜像网站可以下载,点击这些地址中的一个就可以下载,从国内镜像网站下载速度快一些。
这里给大家推荐几个国内著名的 Qt 镜像网站,主要是各个高校的:
国内镜像网站的结构和官方是类似的,我们在第一部分已经分析过了,这里不再赘述。
将 Qt 软件的下载地址复制到迅雷的下载框,如果迅雷官方有资源,就会自动识别,下载速度就很快了。
如何找到 Qt 软件的下载地址呢?以清华大学开源软件镜像站为例,进入 Qt 5.9.0 的下载目录(https://mirrors.tuna.tsinghua.edu.cn/qt/archive/qt/5.9/5.9.0/),在某个链接处单击鼠标右键,会弹出一个菜单,选择“复制链接地址”,如下图所示:
这样就把 Qt 5.9.0 的下载地址(具体为 https://mirrors.tuna.tsinghua.edu.cn/qt/archive/qt/5.9/5.9.0/qt-opensource-windows-x86-5.9.0.exe)复制到了剪切板,然后再粘贴到迅雷的下载框:
点击“立即下载”按钮,稍等片刻,迅雷会自动匹配到资源,速度飞快。
注意,常用的 Qt 版本一般都能匹配到资源,但是不保证每个版本都能匹配到资源,上面的例子仅对清华大学镜像站的 Qt 5.9.0 Windows 版(https://mirrors.tuna.tsinghua.edu.cn/qt/archive/qt/5.9/5.9.0/qt-opensource-windows-x86-5.9.0.exe)有效。
对 Qt 版本更新感兴趣的读者请访问 Qt wiki 网站,地址为:https://wiki.qt.io/Main
Qt wiki 网站会显示最新的正式版、LTS 版、正在开发中的版本等等,比主站(https://www.qt.io/)靠谱多了。Qt 主站因为商业推广的原因,安装包的下载步骤非常繁琐。
本节介绍 Qt 5.9.0 在 Windows 平台下的安装,请提前下载好 Qt 5.9.0。不知道如何下载 Qt 的读者请转到:Qt下载(多种下载通道+所有版本)
目前较高版本的 Qt 仅支持 Win7 及其以后的操作系统,不支持 Win XP;使用 Win XP 的读者请安装 Qt 5.5.1 之前的版本。
Qt 占用的存储空间很大,安装之前建议先准备好 8GB 以上的磁盘空间。对于目前 Qt 最新版开发环境,如果不安装源代码包,实际占用大约 5.5GB;如果选择安装源码包,大约占用 7.5GB。
双击下载得到的 qt-opensource-windows-x86-5.9.0.exe 即可开始安装。Qt 的安装过程和普通的 Windows 软件一样,按照向导进行操作即可。
关于 Qt 的安装需要说明以下几点。
Qt 在安装过程中会提示用户进行注册和登录,不用理会,跳过(Skip)即可,实际开发时不需要登录。
Qt 允许用户自定义安装路径,但是请注意,安装路径不能带空格、中文字符或者其它任何特殊字符。
另外,该界面还会询问是否关联特定的文件类型。如果关联(默认是关联的),特定后缀的文件(包括 .cpp 文件)默认使用 Qt 打开。我喜欢使用纯文本编辑器(例如 Sublime Text)来打开 C++ 源文件,所以我取消了该选项,读者根据自己的实际情况定夺。
Qt 安装过程中最关键的一步是组件的选择,请看下图:
Qt 的安装组件分为两部分:一部分是“Qt 5.9”分类下的,该分类包含的是真正的 Qt 开发库组件;另一部分是“Tools”分类下的,该分类包含的是集成开发环境和编译工具。
“Qt 5.9”分类下的开发组件 | |
---|---|
组件 | 说明 |
MinGW 5.3.0 32 bit | 编译器模块。MinGW 是 Minimalist GNU for Windows 的缩写,MinGW 是 Windows 平台上使用的 GNU 工具集导入库的集合。是本教程使用 MinGW 编译,所以必须安装。 |
UWP *** | UWP 是 Windows 10 中 Universal Windows Platform 的简称,有不同编译器类型的 UWP,属于 MSVC 编译器生成的 Qt 库。如果不是开发 UWP 应用程序,就不需要,直接忽略。 |
MSVC *** | 针对 Windows 平台上的 MSVC 编译器的 Qt 组件,如 msvc2015 32-bit 和 msvc2015 64-bit 等。安装该组件需要计算机上已经安装相应版本的 Visual Studio。如果你不使用 MSVC 编译器进行开发,就不用安装。本教程使用 MinGW 编译组件,所以不用安装 MSVC *** 组件。 |
Android *** | 这是针对安卓应用开发的 Qt 库,如果读者有安卓开发这方面需求可以自己选择安装,一般情况下用不到。 |
Sources | Qt 的源代码包,除非你想阅读 Qt 的源码,否则不用安装。 |
Qt *** | Qt 的附加模块,大部分建议安装,这些附加模块括号里的 TP 是指 Technology Preview ,技术预览模块的意思,还处在功能测试阶段,不是正式版模块;附加模块括号里的 Deprecated 是指抛弃的旧模块,兼容旧代码使用的,一般用不到。这些附加模块读者可以选择部分或都勾选了安装,占用空间不大。 部分组件说明:Qt Charts 是二维图表模块,用于绘制柱状图、饼图、曲线图等常用二维图表。Qt Data Visualization 是三维数据图表模块,用于数据的三维显示,如散点的三维空间分布、三维曲面等。Qt Scritp(Deprecated)是脚本模块,已被抛弃,不建议安装。 |
“Tools”分类下的开发组件 | |
组件 | 说明 |
Qt Creator 4.3.0 | 这是集成开发环境,强制安装的,以后所有的项目和代码都在 Qt Creator 里面新建和编辑。 |
Qt Creator 4.3.0 CDB Debugger surpport | 用于和 CDB 调试工具对接,默认安装,一般用于调试 VC 编译的 Qt 程序。 |
MinGW 5.3.0 | 这是开源的编译器套件,这本教程必须用到的,需要读者勾选安装。 |
Strawberry Perl 5.22.1.3 | 用于编译 Qt 源代码的 Perl 开发环境,不需要安装。如果读者以后用到,也可以另外手动安装,在搜索引擎搜索 Strawberry Perl 关键词,去 Strawberry Perl 官网下载最新的安装包是一样用的。 |
选择完了组件,根据向导一步一步操作就可以了。安装完成后,在 Windows“开始”菜单中会看到 Qt 5.9.0 程序组。
程序 | 说明 |
---|---|
Qt Creator 4.6.2 (Enterprise) | Qt 的集成开发环境,本教程就使用它来创建和管理 Qt 项目。 |
Assistant(Qt 助手) | 用来查看帮助文档,已被集成在 Qt Creator 中。 |
Designer(Qt 设计师) | 图形界面可视化编辑工具,已被集成在 Qt Creator 中,在 Qt Creator 中编辑或创建界面文件时,就可以自动打开。 |
Linguist(Qt 语言家) | 多国语言翻译支持工具,可以用来编辑语言资源文件,在开发多语言界面的应用程序时会用到。 |
Qt 5.11.1 for Desktop (MinGW 5.3.0 32bit) | Qt 命令行工具,用来配置 Qt 开发环境(主要是设置 PATH 变量)。 |
了解 Qt 安装目录的结构虽然不是编程必须的,但是它能练就我们的内功,让我们对 Qt 的编程环境了如指掌。Windows 和 Linux 下 Qt 安装目录的结构非常相似,我们以 Windows 为例进行讲解,Linux 不再赘述。
不同版本 Qt 的安装目录结构大同小异,本节我们以 Qt 5.9.0 为例来说明,如下图所示。
为了方便描述,下文我们使用~
表示 Qt 的安装目录。
注意,~\5.9\ 和 ~\Tools\ 目录下都有 mingw53_32 目录(图中我用红色标出来了),但是两者是有区别的:
QtCreator 是个例外,QtCreator 使用 MSVC2015 编译生成的,所以安装目录里有一个 vcredist 文件夹存储 VC 运行库安装文件。
最后的 MaintenanceTool.exe ,对于离线安装包,它只能用于删除软件包,如果 Qt 开发环境是用在线安装方式装的,这个工具还可以管理开发环境组件和升级组件。
Qt 类库的帮助文件位于 Docs 文件夹里,需要用 Qt Assistant 工具才能查看。
Examples 里是示例代码,可以用 QtCreator 集成开发环境打开各个示例。
下面我们再探究一下 Qt 类库目录(~\5.9\mingw53_32\)的结构,如下图所示。
图上列的比较有限,不一定全面,主要是教大家熟悉一下 Qt 的开发环境。
Qt 不是凭空产生的,它是基于现有工具链打造而成的,它所使用的编译器、链接器、调试器等都不是自己的,Qt 官方只是开发了上层工具。下面我们分几个部分讲解 Qt 使用到的工具链。
在上个世纪八十年代,计算机都是奢侈品,操作系统里最著名的是 Unix 家族, 当时还没有 Windows、Linux 之类的,Unix 系统都是商业软件,里面的应用软件也是商业软件, 全是封闭的环境。
系统程序员 Richard M. Stallman (RMS) 在此环境下创立了与众不同的 GNU 项目 (GNU’s Not Unix) , 以及推进自由软件发展的 Free Software Foundation (FSF) 自由软件基金会。
GNU 项目是为了创建自由的类 Unix 系统,也因此开发出来很多开源的系统工具,其中非常著名的就是 GCC (GNU Compiler Collection,GNU编译器套件)。
现在我们知道,GUN 开发类 Unix 系统的项目失败了,但是它开发的一系列工具集却用到了后来的 Linux 内核上,两者结合形成了今天的各种 Linux 发行版,有兴趣的读者请转到《Linux和UNIX的关系及区别》了解更多。
在 GNU 工具集里面,开发时常见到的几个罗列如下(这些工具通常位于 Linux 或 Unix 系统里的 /usr/bin/ 目录):
工具 | 说明 |
---|---|
gcc | GNU C 语言编译器。 |
g++ | GNU C++ 语言编译器。 |
ld | GNU 链接器,将目标文件和库文件链接起来,创建可执行程序和动态链接库。 |
ar | 生成静态库 .a ,可以编辑和管理静态链接库。 |
make | 生成器,可以根据 makefile 文件自动编译链接生成可执行程序或库文件。 |
gdb | 调试器,用于调试可执行程序。 |
ldd | 查看可执行文件依赖的共享库(扩展名 .so,也叫动态链接库)。 |
原本 GNU 工具只在 Linux/Unix 系统里才有,随着 Windows 系统的广泛使用, 为了在 Windows 系统里可以使用 GNU 工具,诞生了 MinGW(Minimalist GNU for Windows) 项目,利用 MinGW 就可以生成 Windows 里面的 exe 程序和 dll 链接库。
需要注意的是,MinGW 与 Linux/Unix 系统里 GNU 工具集的有些区别:
另外 MinGW 里也没有 ldd 工具,因为 Windows 不使用 .so 共享库文件。如果要查看 Windows 里可执行文件的依赖库,需要使用微软自家的 Dependency Walker 工具。Windows 里面动态库扩展名为 .dll,MinGW 可以通过 dlltool 来生成用于创建和使用动态链接库需要的文件,如 .def 和 .lib。
MinGW 原本是用于生成 32 位程序的,随着 64 位系统流行起来, 从 MinGW 分离出来了 MinGW-w64 项目,该项目同时支持生成 64 位和 32 位程序。Qt 的 MinGW 版本库就是使用 MinGW-w64 项目里面的工具集生成的。
另外提一下,由于 MinGW 本身主要就是编译链接等工具和头文件、库文件,并不包含系统管理、文件操作之类的 Shell 环境, 这对希望用类 Unix 命令的开发者来说还是不够用的。 所以 MinGW 官方又推出了 MSYS(Minimal SYStem),相当于是一个部署在 Windows 系统里面的小型 Unix 系统环境, 移植了很多 Unix/Linux 命令行工具和配置文件等等,是对 MinGW 的扩展。
MSYS 对于熟悉 Unix/Linux 系统环境或者要尝试学习 Unix/Linux 系统的人都是一种便利。MSYS 和 MinGW 的安装升级都是通过其官方的 mingw-get 工具实现,二者是统一下载安装管理的。
对于 MinGW-w64 项目,它对应的小型系统环境叫 MSYS2(Minimal SYStem 2),MSYS2 是 MSYS 的衍生版,不仅支持 64 位系统和 32 位系统,还有自己的独特的软件包管理工具,它从 Arch Linux 系统里移植了 pacman 软件管理工具,所以装了 MSYS2 之后,可以直接通过 pacman 来下载安装软件,而且可以自动解决依赖关系、方便系统升级等。装了 MSYS2 之后,不需要自己去下载 MinGW-w64,可以直接用 pacman 命令安装编译链接工具和 git 工具等。
MinGW 项目主页(含 MSYS): http://www.mingw.org/
MinGW-w64 项目主页: https://sourceforge.net/projects/mingw-w64/
MSYS2 项目主页: https://sourceforge.net/projects/msys2/
CMake(Cross platform Make)是一个开源的跨平台自动化构建工具, 可以跨平台地生成各式各样的 makefile 或者 project 文件, 支持利用各种编译工具生成可执行程序或链接库。
CMake 自己不编译程序, 它相当于用自己的构建脚本 CMakeLists.txt,叫各种编译工具集去生成可执行程序或链接库。
一般用于编译程序的 makefile 文件比较复杂,自己去编写比较麻烦, 而利用 CMake ,就可以编写相对简单的 CMakeLists.txt ,由 CMake 根据 CMakeLists.txt 自动生成 makefile,然后就可以用 make 生成可执行程序或链接库。
本教程里面是使用 Qt 官方的 qmake 工具生成 makefile 文件,没有用 CMake。这里之所以提 CMake,是因为整个 KDE 桌面环境的茫茫多程序都是用 CMake 脚本构建的,另外跨平台的程序/库如 Boost C++ Libraries、OpenCV、LLVM、Clang 等也都是用 CMake 脚本构建的。以后如果接触到这些东西,是需要了解 CMake 的。
CMake 项目主页:https://cmake.org/
KDE 项目主页:https://www.kde.org/
Qt 官方的开发环境安装包里有自己专门的开发工具,之前用过 qmake 命令。qmake 是 Qt 开发最核心的工具,既可以生成 Qt 项目文件 .pro ,也可以自动生成项目的 Makefile 文件。
这里将常用的 Qt 开发工具列表如下:
工具 | 说明 |
---|---|
qmake | 核心的项目构建工具,可以生成跨平台的 .pro 项目文件,并能依据不同操作系统和编译工具生成相应的 Makefile,用于构建可执行程序或链接库。 |
uic | User Interface Compiler,用户界面编译器,Qt 使用 XML 语法格式的 .ui 文件定义用户界面,uic 根据 .ui 文件生成用于创建用户界面的 C++ 代码头文件,比如 ui_*****.h 。 |
moc | Meta-Object Compiler,元对象编译器,moc 处理 C++ 头文件的类定义里面的 Q_OBJECT 宏,它会生成源代码文件,比如 moc_*****.cpp ,其中包含相应类的元对象代码,元对象代码主要用于实现 Qt 信号/槽机制、运行时类型定义、动态属性系统。 |
rcc | Resource Compiler,资源文件编译器,负责在项目构建过程中编译 .qrc 资源文件,将资源嵌入到最终的 Qt 程序里。 |
qtcreator | 集成开发环境,包含项目生成管理、代码编辑、图形界面可视化编辑、 编译生成、程序调试、上下文帮助、版本控制系统集成等众多功能, 还支持手机和嵌入式设备的程序生成部署。 |
assistant | Qt 助手,帮助文档浏览查询工具,Qt 库所有模块和开发工具的帮助文档、示例代码等都可以检索到,是 Qt 开发必备神器,也可用于自学 Qt。 |
designer | Qt 设计师,专门用于可视化编辑图形用户界面(所见即所得),生成 .ui 文件用于 Qt 项目。 |
linguist | Qt 语言家,代码里用 tr() 宏包裹的就是可翻译的字符串,开发人员可用 lupdate 命令生成项目的待翻译字符串文件 .ts,用 linguist 翻译多国语言 .ts ,翻译完成后用 lrelease 命令生成 .qm 文件,然后就可用于多国语言界面显示。 |
qmlscene | 在 Qt 4.x 里是用 qmlviewer 进行 QML 程序的原型设计和测试,Qt 5 用 qmlscene 取代了旧的 qmlviewer。新的 qmlscene 另外还支持 Qt 5 中的新特性 scenegraph 。 |
本节我们来介绍一下使用 Qt 编程过程中常用的术语和名字,它们不一定专属于 Qt,在其它的 C/C++ 开发过程中也会使用到。
Project 的中文翻译是“项目”或者“工程”,这里的项目是指为实现某个相对独立功能的程序代码合集,这些代码不单单是放在一块,而是有相互之间的关联性,并且有专门负责管理该项目的项目文件,比如:
集成开发环境通常都是依据项目文件(.pro/.vcproj)管理和构建项目。
即生成脚本,虽然可以直接调用编译器如 g++ 编译程序,但是如果项目里的代码文件变多了,哪些代码文件更新了需要重新编译,哪些代码没有改不需要重新编译等等,靠程序员自己记忆去处理是比较麻烦的事,还有哪些代码需要预处理或是链接哪些库文件, 这些都是繁杂的过程。为了规范程序的编译生成过程,产生了规范化的生成脚本,就是 Makefile,生成器 make 可以依据规范的 Makefile 自动生成目标程序或库文件。
简单的说,就是定义好 Makefile ,让程序员只需要去关注如何编写代码,而生成程序过程中的脏活累活都交给 make 程序。
现在 Makefile 通常都有工具自动生成,如 qmake 工具, 这样就大量减轻了程序员的负担。
Debug 即调试,Release 即发行。代码编写之后,生成的目标程序或库文件通常不会绝对正确,或多或少有些毛病(bug), 因此需要进行纠错调试(Debug)。调试过程中需要源代码和二进制目标程序之间一一对应的关系, 这样才能定位到错误代码,所以 Debug 版本的程序是臃肿而不进行优化的。
与之相对的是 Release 发行版,在纠正了发觉到的错误后,需要发布程序用于实际用途,实际应用时强调运行效率高,减少冗余代码,因此会对二进制程序进行大量优化,提升性能。这样发布的二进制目标程序就是 Release 版。
Debug 版本和 Release 版本使用的库文件不一样:
时代在变化,C++ 标准也在前进。C++ 正式公布标准有 C++98、C++03、C++11。最新的 C++11 标准是2011年8月12日公布的,在公布之前该标准原名为 C++0x 。这是一次较大的修订和扩充,建议读者专门学一下。
Qt 从 4.8 版本就开始用 C++11 新特性了。编译器里面开始支持 C++11 的版本是 MSVC 2010、GCC 4.5、Clang 3.1,这之后版本的编译器都在逐步完善对 C++11 的支持,现在新版本编译器对新标准的支持都比较全面了。
Qt 官方在编译 Qt5 库的时候都是开启 C++11 特性的,如果我们要在自己项目代码启用新标准,需要在 .pro 文件里面添加一行:
CONFIG += c++11
如果是 Qt4 版本则是添加:
gcc:CXXFLAGS += -std=c++0x
MSVC 编译器默认开启 C++11 特性,GCC(g++命令)则需要自己添加选项 -std=c++0x ,上面 CXXFLAGS 就是为 GCC 编译器(g++命令)添加 -std=c++0x 选项。
Dynamic Link 即动态链接,Static Link 即静态链接。
目标程序通常都不是独立个体,生成程序时都需要链接其他的库,要用到其他库的代码。对于多个程序同时运行而言,内存中就可能有同一个库的多个副本,占用了太多内存而干的活差不多。
为了优化内存运用效率,引入了动态链接库(Dynamic Link Library),或叫共享库(Shared Object)。使用动态链接库时,内存中只需要一份该库文件,其他程序要使用该库文件时,只要链接过来就行了。由于动态库文件外置,链接到动态库的目标程序相对比较小,因为剥离了大量库代码,而只需要一些链接指针。
使用动态库,也意味着程序需要链接到如 *.dll 或 *.so 文件,得提前装好动态库文件,然后目标程序才能正常运行。
静态库就是将链接库的代码和自己编写的代码都编译链接到一块,链接到静态库的程序通常比较大,但好处是运行时依赖的库文件很少,因为目标程序自己内部集成了很多库代码。
Linux/Unix 系统里静态库扩展名一般是 .a,动态库扩展名一般是 .so 。Windows 系统里 VC 编译器用的静态库扩展名一般是 .lib,动态库扩展名一般是 .dll 。
MinGW 比较特殊,是将 GNU 工具集和链接库从 Linux/Unix 系统移植到 Windows 里, 有意思的情况就出现了,MinGW 使用的静态库扩展名为 .a ,而其动态库扩展名则为 .dll, .a 仅在生成目标程序过程中使用,.dll 则是在目标程序运行时使用。
Explicit Linking 即显式链接,Implicit Linking 即隐式链接,这两种都是动态链接库的使用方式。
动态链接库通常都有其导出函数列表, 告知其他可执行程序可以使用它的哪些函数。可执行程序使用这些导出函数有两种方式:一是在运行时使用主动加载动态库的函数,Linux 里比如用 dlopen 函数打开并加载动态库,Windows 里一般用 LoadLibrary 打开并加载动态库,只有当程序代码执行到这些函数时,其参数里的动态库才会被加载,这就是显式链接。显式链接方式是在运行时加载动态库,其程序启动时并不检查这些动态库是否存在。
隐式链接是最为常见的,所有的编译环境默认都是采用隐式链接的方式使用动态库。隐式链接会在链接生成可执行程序时就确立依赖关系,在该程序启动时,操作系统自动会检查它依赖的动态库,并一一加载到该程序的内存空间,程序员就不需要操心什么时候加载动态库了。比如 VC 编译环境,链接时使用动态库对应的 .lib 文件(包含动态库的导出函数声明,但没有实际功能代码),在 .exe 程序运行前系统会检查依赖的 .dll,如果找不到某个动态库就会出现类似下图对话框:
MinGW 是将动态库的导出函数声明放在了 .a 文件里,程序运行依赖的动态库也是 .dll 。
请注意,VC 链接器使用的 .lib 文件分两类,一种是完整的静态库,体积比较大,另一种是动态库的导出声明,体积比较小。MinGW 链接器使用的 .a 文件也是类似的,Qt 官方库都是按照动态库发布的,静态库只有自己编译才会有。
启动 Qt Creator,出现如图 1 所示的主窗口:
Qt Creator 的界面很简洁。上方是主菜单栏,左侧是主工具栏,窗口的中间部分是工作区。根据设计内容不同,工作区会显示不同的内容。
图 1 是在左侧主工具栏单击“Welcome(欢迎)”按钮后显示实例的界面。这时工作区的左侧有 “Projects”、“Examples(示例)”、“Tutorials(教程)”、“Get Started Now”几个按钮,单击后会在主工作区显示相应的内容:
主窗口左侧是主工具栏,主工具栏提供了项目文件编辑、窗体设计、程序调试、项目设置等各种功能按钮。
对 Qt Creator 可以进行一些设置,如刚安装好的 Qt Creator 界面语言可能是中文,也可以选择将 Qt Creator 的界面语言设置为英文。
单击 Qt Creator 菜单栏的 Tools→Options
菜单项会打开选项设置对话框(如图 2 所示)。对话框的左侧是可设置的内容分组,单击后右侧出现具体的设置界面。常用的设置包括以下几点:
注意,如果只是在计算机上安装了 Visual Studio 2015
,图 2 Kits 显示的界面上 MSVC2015 的两个编译器的图标会变为带有感叹号的一个黄色图标。Debuggers 页面没有 Windows 的 CDB 调试器,可以用 MSVC 编译器对 Qt Creator 编写的程序进行编译,但是不能调试,这是因为缺少了 Windows Software Development Kit (SDK)
。这个 SDK 不会随 Visual Studio 一同安装,需要从 Microsoft 网站上下载。可以下载 Windows Software Development Kit (SDK) for Windows 8.1
,安装后重启计算机即可。
学习一种编程语言或编程环境,通常会先编写一个“Hello World”程序。我们也用 Qt Creator 编写一个“Hello World”程序,以初步了解 Qt Creator 设计应用程序的基本过程,对使用 Qt Creator 编写 Qt C++ 应用程序建立初步的了解。
单击 Qt Creator 的菜单项文件->新建文件或项目
,出现如图 1 所示的对话框。在这个对话框里选择需要创建的项目或文件的模板。
Qt Creator 可以创建多种项目,在最左侧的列表框中单击“Application”,中间的列表框中列出了可以创建的应用程序的模板,各类应用程序如下:
在图 1 显示的对话框中选择项目类型为 Qt Widgets Application
后,单击“Choose…”按钮,出现如图 2 所示的新建项目向导:
在图 2 中,选择一个目录,如“E:\QtDemo”,再设置项目名称为 Demo, 这样新建项目后,会在“E:\QtDemo”目录下新建一个目录,项目所有文件保 存在目录“E:\QtDemo\Demo\”下。
在图 2 中设置好项目名称和保存路径后,单击“Next”按钮,出现如图 3 所示的选择编译工具的界面:
可以将这几个编译工具都选中,在编译项目时再选择一个作为当前使用的编译工具,这样可以编译生成不同版本的可执行程序。
在图 3 显示的界面中单击“Next”按钮,出现如图 4 所示的界面。在此界面中选择需要创建界面的基类(base class)。有 3 种基类可以选择:
在此选择 QMainWindow
作为基类,自动更改的各个文件名不用手动去修改。勾选“创建界面”复选框。这个选项如果勾选,就会由 Qt Creator 创建用户界面文件,否则,需要自己编程手工创建界面。初始学习,为了了解 Qt Creator 的设计功能,勾选此选项。
然后单击“Next”按钮,出现一个页面,总结了需要创建的文件和文件保存目录,单击“完成”按钮就可以完成项目的创建。
完成了以上新建项目的步骤后,在 Qt Creator 的左侧工具栏中单击“编辑”按钮,可显示如图 5 所示的窗口。
窗口左侧有上下两个子窗口,上方的目录树显示了项目内文件的组织结构,显示当 前项目为 Demo。项目的名称构成目录树的一个根节点,Qt Creator 可以打开多个项目,但是只有一个活动项目,活动项目的项目名称节点用粗体字体表示。
在项目名称节点下面,分组管理着项目内的各种源文件,几个文件及分组分别为以下几项:
左侧上下两个子窗口的显示内容可以通过其上方的一个下拉列表框进行选择,可以选择的显示内容包括项目、打开文档、书签、文件系统、类视图、大纲等。在图 5 中,上方的子窗口显示了项目的文件目录树,下方显示打开的文件列表。可以在下方选择显示类视图,这样下方则显示项目内所有的类的结构,便于程序浏览和快速切换到需要的代码位置。
双击文件目录树中的文件mainwindow.ui,出现如图 6 所示的窗体设计界面:
这个界面实际上是 Qt Creator 中集成的 Qt Designer。窗口左侧是分组的组件面板,中间是设计的窗体。在组件面板的 Display Widgets 分组里,将一个Label组件拖放到设计的窗体上面。双击刚刚放置的 Label 组件,可以编辑其文字内容,将文字内容更改为“Hello, World!”。还可以在窗口右下方的属性编辑器里编辑标签的 Font 属性,Point Size(点大小)更改为 12,勾选粗体。
单击主窗口左侧工具栏上的“项目”按钮,出现如图 7 所示的项目编译设置界面。
界面左侧一栏的“Build & Run”下面显示了本项目中可用的编译器工具,要使用哪一个编译器用于项目编译,单击其名称即可,选择的编译器名称会用粗体字表示。这里选择使用 MinGW 32bit 编译器。
每个编译器又有 Build 和 Run 两个设置界面。在 Build 设置界面上,有一个“Shadow build” 复选框。如果勾选此项,编译后将在项目的同级目录下建立一个编译后的文件目录,目录名称包含编译器信息,这种方式一般用于使用不同编译器创建不同版本的可执行文件。如果不勾选此项,编译后将在项目的目录下建立“Debug”和“Release”子目录用于存放编译后的文件。
在设计完 mainwindow.ui 文件,并设置好编译工具之后,就可以对项目进行编译、调试或运行。主窗口左侧工具栏下方有 4 个按钮,其功能见表 1。
图标 | 作用 | 快捷键 |
---|---|---|
弹出菜单选择编译工具和编译模式,如 Debug或 Release模式 | ||
直接运行程序,如果修改后未编译,会先进行编译。即使在程序中设置了断点,此方式运行的程序也无法调试。 | Ctrl+R | |
项目需要以Debug模式编译,点此按钮开始调试运行,可以在程序中设置断点。若是以 Release模式编译,点此按钮也无法进行调试。 | F5 | |
编译当前项目 | Ctrl+B |
首先对项目进行编译,没有错误后,再运行程序。程序运行的界面如图 8 所示。这就是一个标准的桌面应用程序,我们采用可视化的方式设计了一个窗口,并在上面显示了字符串“Hello, World!”。
在 Qt Creator 中也可以对程序设置断点进行调试,但是必须以 Debug 模式编译,并以“Start Debugging”(快捷键 F5)方式运行程序。
程序调试的方法与一般 IDE 工具类似,不再详述。注意,要在 Qt Creator 里调试 MSVC2015 编译的程序,必须安装 Windows 软件开发工具包 SDK。
前面章节中,通过在 xxx.ui 文件中拖拽 Label 组件,设计出了一个显示 “Hello,World!” 的窗口,如下图所示:
本节我们完全舍弃 xxx.ui 文件,亲手编写代码实现图 1 所示的界面。
首先,打开 Qt Creator 并创建一个 Qt Widgets Application 项目,创建过程可以参考 《编写第一个Qt程序》一节。需要注意的是,我们要创建一个不带 xxx.ui 文件的项目,如下图所示:
最终创建的项目结构如下图所示:
Demo.pro 是项目文件,文件中的内容可以手动修改,我们会在《Qt pro文件详解》一节中详细讲解,本节不需要修改此文件。接下来,我们逐一介绍 main.cpp、mainwindow.h 和 mainwindow.cpp 这 3 个文件。
main.cpp 是主函数文件,内部主要包含应用程序的入口函数,也就是 main() 函数。
我们知道,C/C++ 程序中 main() 函数的语法格式是固定的:
int main(int argc, char *argv[]){
//填充代码
return 0;
}
Qt 界面程序中的 main() 函数也有固定的格式:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//填充代码
return a.exec();
}
对于刚刚学习 Qt 的读者,暂时不用了解第 3 行和第 5 行代码的含义,只要记住:使用 Qt 框架编写带界面的应用程序,main() 函数中必须包含第 3 行和第 5 行代码,否则程序无法正常运行。
双击图 3 所示的 main.cpp 文件,可以看到该文件包含的所有代码:
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
除了第 6、8 行代码外,其它代码的含义分别是:
" "
双引号括起来;QApplication 是 Qt 提供给我们的,引入时用<>
括起来。创建项目时,我们在图 2 所示的对话框中定义了一个继承自 QMainWindow 的主窗口类,并起名为 MianWindow,该类的定义部分位于 mainwindow.h 头文件中,实现部分位于 mainwindow.cpp 源文件中。
双击图 3 所示的 mainwindow.h 和 mainwindow.cpp 文件,可以看到它们各自包含的代码:
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
};
//mainwindow.cpp
#endif // MAINWINDOW_H
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
}
MainWindow::~MainWindow()
{
}
初始状态下,MainWindow 类由 Q_OBJECT、构造函数和析构函数组成,这里重点介绍一下 Q_OBJECT 和构造函数:
直接运行程序,会输出下图所示的界面:
图 4 看到的就是 main() 函数中创建的 w 主窗口。由于没有往 w 窗口中放置任何组件,所以 w 是一个空白窗口。
我们尝试向 w 主窗口添加一个文本框,需要对 MainWindow 类进行修改。修改后的 MainWindow 类如下:
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include // 引入 QLable 文件框组件的头文件
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QLabel *lab; // 定义一个私有的 QLabel 指针对象
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
// 创建一个 QLable 对象
this->lab = new QLabel("Hello,World!",this);
}
MainWindow::~MainWindow()
{
}
和先前空的 MainWindow 类相比,做了如下修改:
头文件;有关 QLabel 组件的用法,我们会在《Qt QLabel文本框的使用》一节详细讲解,这里不用深究具体的语法。
再次运行程序,显示的窗口如下图所示:
图 1 和图 5 是类似的,区别在于图 5 中的 “Hello, World!” 没有加粗,也没有调整它在主窗口中的位置,这些都可以通过编码实现,后续讲 QLabel 时会做详细介绍。
图 5 中,“Hello,World!” 文本框的父窗口是主窗口,所以文本框位于主窗口中(位置默认在窗口的左上角),主窗口关闭时文本框也会随之关闭。
由此,我们就成功设计了一个包含文本框的窗口,这也是我们编写的第一个 Qt 程序。
Qt 是一个著名的 GUI 框架,用来开发和用户交互的图形界面。作为 GUI 框架,丰富的控件和灵活的事件机制是不可或缺的,Qt 在这一方面做得非常优秀。
Qt 控件又称组件或者部件,指用户看到的所有可视化界面以及界面中的各个元素,比如按钮、文本框、输入框等。
为了方便程序员开发,Qt 提供了很多现成的控件。打开某个带 ui 文件的 Qt Widgets Application 项目,ui 文件的 Widget Box 一栏展示了 Qt 提供的几乎所有控件:
Qt 中的每个控件都由特定的类表示,每个控件类都包含一些常用的属性和方法,所有的控件类都直接或者间接继承自 QWidget 类。实际开发中,我们可以使用 Qt 提供的这些控件,也可以通过继承某个控件类的方式自定义一个新的控件。
前面说过,Qt 中所有可视化的元素都称为控件,我们习惯将带有标题栏、关闭按钮的控件称为窗口。例如,下图展示了两种常用的窗口,实现它们的类分别是 QMainWindow 和 QDialog。
除了 QMainWindow 和 QDialog 之外,还可以使用 QWidget 类,它的用法非常灵活,既可以用来制作窗口,也可以作为某个窗口上的控件。
窗口很少单独使用,它的内部往往会包含很多控件。例如图 2 中,我们分别往 MainWindow 和 Dialog 窗口中放置了一个按钮控件,根据需要还可以放置更多的控件。当窗口弹出时,窗口包含的所有控件会一同出现;当窗口关闭时,窗口上的所有控件也会随之消失。
实际开发中,制作应用程序的主窗口可以用 QMainWindow 或者 QWdiget;制作一个提示信息的对话框就用 QDialog 或 QWidget;如果暂时无法决定,后续可能作为窗口,也可能作为控件,就选择 QWidget。
简单地理解,Qt 事件指的是应用程序和用户之间的交互过程,例如用户按下某个按钮,点击某个输入框等等。实际上除了用户会与应用程序进行交互外,操作系统也会与应用程序进行交互,例如当某个定时任务触发时,操作系统会关闭应用程序,这也是一个事件。
《分析第一个Qt程序》一节中提到,Qt 界面程序的 main() 主函数中首先要创建一个 QApplication 类的对象,函数执行结束前还要调用 QApplication 对象的 exec() 函数。一个 Qt 界面程序要想接收事件,main() 函数中就必须调用 exec() 函数,它的功能就是使程序能够持续不断地接收各种事件。
Qt 程序可以接收的事件种类有很多,例如鼠标点击事件、鼠标滚轮事件、键盘输入事件、定时事件等。每接收一个事件,Qt 会分派给相应的事件处理函数来处理。所谓事件处理函数,本质就是一个普通的类成员函数,以用户按下某个 QPushButton 按钮为例,Qt 会分派给 QPushButton 类中的 mousePressEvent() 函数处理。
事件处理函数通常会完成两项任务,分别是:
信号和槽是 Qt 中处理事件最常用的方法,我们会在《Qt信号和槽》一节中做详细地讲解。
创建一个不带 ui 文件的 Qt Widgets Application 项目,项目中只保留一个 main.cpp 源文件,删除其它文件(mainwindows.h 和 mainwindow.cpp)。将下述代码直接拷贝到 main.cpp 文件:
//main.cpp
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//添加窗口
QWidget widget;
//定义一个按钮,它位于 widget 窗口中
QPushButton But("按钮控件",&widget);
//设置按钮的位置和尺寸
But.setGeometry(10,10,100,50);
//信号与槽,实现当用户点击按钮时,widget 窗口关闭
QObject::connect(&But,&QPushButton::clicked,&widget,&QWidget::close);
//让 widget 窗口显示
widget.show();
return a.exec();
}
运行结果如下图所示:
整个程序的运行过程如下:
实际开发中,我们用各种 Qt 控件设计出功能丰富的界面,用 Qt 事件完成与用户的交互。学习 Qt 界面编程,本质上就是学习 Qt 各个控件的用法以及对 Qt 事件的处理。本节我们只是对 Qt 控件和事件做了简单的了解,接下来将为您系统地讲解它们的用法。
信号和槽是 Qt 特有的消息传输机制,它能将相互独立的控件关联起来。
举个简单的例子,按钮和窗口本是两个独立的控件,点击按钮并不会对窗口造成任何影响。通过信号和槽机制,我们可以将按钮和窗口关联起来,实现“点击按钮会使窗口关闭”的效果。
在 Qt 中,用户和控件的每次交互过程称为一个事件,比如“用户点击按钮”是一个事件,“用户关闭窗口”也是一个事件。每个事件都会发出一个信号,例如用户点击按钮会发出“按钮被点击”的信号,用户关闭窗口会发出“窗口被关闭”的信号。
Qt 中的所有控件都具有接收信号的能力,一个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。例如,按钮所在的窗口接收到“按钮被点击”的信号后,会做出“关闭自己”的响应动作;再比如输入框自己接收到“输入框被点击”的信号后,会做出“显示闪烁的光标,等待用户输入数据”的响应动作。在 Qt 中,对信号做出的响应动作就称为槽。
信号和槽机制底层是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为信号函数;每个槽也可以用函数表示,称为槽函数。例如,“按钮被按下”这个信号可以用 clicked() 函数表示,“窗口关闭”这个槽可以用 close() 函数表示,信号和槽机制实现“点击按钮会关闭窗口”的功能,其实就是 clicked() 函数调用 close() 函数的效果。
信号函数和槽函数通常位于某个类中,和普通的成员函数相比,它们的特别之处在于:
为了提高程序员的开发效率,Qt 的各个控件类都提供了一些常用的信号函数和槽函数。例如 QPushButton 类提供了 4 个信号函数和 5 个 public slots 属性的槽函数,可以满足大部分场景的需要。
实际开发中,可以使用 Qt 提供的信号函数和槽函数,也可以根据需要自定义信号函数和槽函数,我们会在《Qt自定义信号和槽函数》一节做详细介绍。
Qt Creator 提供了很强大的 Qt GUI 开发手册,很容易就能查到某个控件类中包含哪些信号函数和槽函数。举个例子,查看 QPushButton 类中信号函数和槽函数的过程是:
头文件,双击选中“QPushButton”并按 “Fn+F1” 快捷键,就会弹出 QPushButton 类的使用手册,如下图所示。Public Slots
属性的槽函数,没有提供信号函数。对于 QPushButton 类按钮,除了可以使用自己类提供的槽函数,还可以使用从父类继承过来的信号函数和槽函数。由图 2 可知,QPushButton 的父类是 QAbstractButton,点击 QAbstractButton 就可以直接跳转到此类的使用手册,如下图所示:QAbstractButton 类中既有 Signals 信号函数,也有 Public Slots 槽函数,这里不再一一列举,感兴趣的读者可以自行查看。
注意,并非所有的控件之间都能通过信号和槽关联起来,信号和槽机制只适用于满足以下条件的控件:
将某个信号函数和某个槽函数关联起来,需要借助 QObject 类提供的 connect() 函数。
connect() 是 QObject 类中的一个静态成员函数,专门用来关联指定的信号函数和槽函数。
关联某个信号函数和槽函数,需要搞清楚以下 4 个问题:
仍以实现“按下按钮后窗口关闭”为例,先创建一个窗口和一个按钮,如下所示:
QWidget widget;
//定义一个按钮,它位于 widget 窗口中
QPushButton But("按钮控件",&widget);
信号发送者是 But 按钮对象,要发送的信号是“按钮被点击”,可以用 QPushButton 类提供的 clicked() 信号函数表示;信号的接收者是 widget 主窗口对象,“窗口关闭”作为信号对应的槽,可以用 QWidget 类提供的 close() 函数表示。
在 Qt5 版本之前,connect() 函数最常用的语法格式是:
QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
各个参数的含义分别是:
用 connect() 函数将 But 按钮的 clicked() 信号函数和 widget 窗口的 close() 槽函数关联起来,实现代码如下:
connect(&But, SIGNAL(clicked()), &widget, SLOT(close()));
如此就实现了“按下按钮会关闭窗口”的功能。
Qt5 版本中,connect() 函数引入了新的用法,常用的语法格式是:
QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
和旧版本相比,新版的 connect() 函数改进了指定信号函数和槽函数的方式,不再使用 SIGNAL() 和 SLOT() 宏。
例如,用新版 connect() 函数关联 But 按钮的 clicked() 信号函数和 widget 窗口的 close() 槽函数,实现代码为:
connect(&But, &QPushButton::clicked, &widget, &QWidget::close);
可以看到,新版 connect() 函数指定信号函数和槽函数的语法格式是&+函数所在类+函数名
。
一个 connect() 函数只能关联一个信号函数和一个槽函数,程序中可以包含多个 connect() 函数,能实现以下几种效果:
此外,connect() 函数的 method 参数还可以指定一个信号函数,也就是说,信号之间也可以相互关联,这样当信号发出时,会随之发出另一个信号。
创建一个不含 ui 文件的 Qt Widgets Application 项目,只保留 main.cpp 源文件,删除 mainwindow.h 和 mainwindow.cpp 文件。在 main.cpp 文件中编写如下代码:
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//添加窗口
QWidget widget;
//定义一个按钮,它位于 widget 窗口中
QPushButton But("按钮控件",&widget);
//设置按钮的位置和尺寸
But.setGeometry(10,10,100,50);
//信号与槽,实现当用户点击按钮时,widget 窗口关闭
QObject::connect(&But,&QPushButton::clicked,&widget,&QWidget::close);
//让 widget 窗口显示
widget.show();
return a.exec();
}
运行结果为:
如上图所示,由于使用了 conect() 函数将 But 的 clicked() 信号函数和 widget 的 close() 槽函数关联起来,所以生成了“点击按钮后主窗口关闭”的效果。
QLabel 是 Qt 帮我们写好的一个控件类,间接继承自 QWidget 类,它的继承关系如下:
QLabel -> QFrame -> QWidget
从字面上理解,QLabel 可以解释为“Qt 的 Label”,即 Qt 提供给我们的一种文本控件,它的基础功能是显示一串文本。例如,下图就是一个普通的文本框:
除了显示一串文本外,QLabel 控件上还可以放置图片、超链接、动画等内容。例如:
本质上,每个文本框都是 QLabel 类的一个实例对象。QLabel 类提供了两个构造函数,分别是:
QLabel(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags())
QLabel(const QString &text, QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags())
各个参数的含义分别是:
需要注意的是,第一个构造函数中的 parent 和 f 参数都有默认值,因此 QLabel 类还隐含了一个默认构造函数。也就是说,创建 QLable 对象时可以不传递任何参数,或者只给 txt 参数传递一个字符串,就可以成功创建一个文本框。通常情况下,我们会给 text 和 parent 参数传递相应的值,即在创建文本框的同时指定文本内容和父窗口。
QLabel 类本身提供有很多属性和方法,它还从父类继承了很多属性和方法。下表给大家罗列了 QLabel 类常用的一些属性和方法:
属 性 | 含 义 |
---|---|
alignment | 保存 QLabel 控件中内容的对齐方式,默认情况下,QLabel 控件中的内容保持左对齐和垂直居中。 该属性的值可以通过调用 alignment() 方法获得,可以借助 setAlignment() 方法修改。 |
text | 保存 QLabel 控件中的文本,如果 QLabel 控件中没有文本,则 text 的值为空字符串, 该属性的值可以通过 text() 方法获得,可以借助 setText() 方法修改。 |
pixmap | 保存 QLabel 控件内显示的图片,如果控件内没有设置图片,pixmap 的值为 0。 该属性的值可以通过调用 pixmap() 方法获得,可以借助 setPixmap() 方法修改。 |
selectedText | 保存 QLabel 控件中被选择了的文本,当没有文本被选择时,selectedText 的值为空字符串。 该属性的值可以通过调用 selectedText() 方法获得。 |
hasSelectedText | 判断用户是否选择了 QLabel 控件内的部分文本,如果是则返回 true,反之则返回 false。默认情况下,该属性的值为 false。 |
indent | 保存 QLabel 控件内文本的缩进量,文本的缩进方向和 alignment 属性的值有关。 该属性的值可以通过调用 indent() 方法获得,可以借助 setIndent() 方法修改。 |
margin | 保存 QLabel 控件中内容与边框之间的距离(边距),margin 的默认值为 0。 该属性的值可以通过调用 margin() 方法获得,可以借助 setMargin() 方法修改。 |
wordWrap | 保存 QLabel 控件内文本的换行策略。当该属性的值为 true 时,控件内的文本会在必要时自动换行。默认情况下,控件内的文本是禁止自动换行的。 该属性的值可以通过 wordWrap() 方法获得,可以借助 setWordWrap() 方法修改。 |
除了上表中提到了获取和修改属性值得成员方法外,下表给大家罗列了一些常用的操作 QLabel 控件的成员方法,它们有些定义在 QLabel 类内,有些是通过继承父类得到的:
成员方法 | 功 能 |
---|---|
hide() | 隐藏文本框。 |
clear() | 清空 QLabel 控件内所有显示的内容。 |
setToolTip(QString) | 设置信息提示,当用户的鼠标放在QLabel 文本框上时会自动跳出文字。 |
setToolTipDuration(int) | 设置提示信息出现的时间,单位是毫秒。 |
setStyleSheet(QString) | 设置 QLabel 文本框的样式。 |
setGeometry(int x, int y, int w, int h) | 设置 QLabel 文本框的位置 (x, y) 以及尺寸 (w, h)。 |
QLabel 控件只用来显示文本、图像等内容,很好与用户交互。但是,当 QLabel 控件内包含超链接内容时,可以使用 QLabel 类提供的两个信号函数:
信号函数 | 功 能 |
---|---|
linkActivated(const QString &link) | 用户点击超链接时触发,link 参数用于向槽函数传输超链接的 URL。 |
linkHovered(const QString &link) | 用户的鼠标悬停到超链接位置时触发,link 参数用于向槽函数传输超链接的 URL。 |
QLabel 控件提供了很多槽函数,如下表所示:
槽函数 | 功 能 |
---|---|
clear() | 清空 QLabel 控件内所有的内容。 |
setMovie(QMovie *movie) | 清空 QLabel 控件内所有的内容,改为显示指定的 movie 动画。 |
setNum(int num) | 清空 QLabel 控件内所有的内容,改为显示 num 整数的值。 |
setNum(double num) | 清空 QLabel 控件内所有的内容,改为显示 num 小数的值。 |
setPicture(const QPicture &picture) | 清空 QLabel 控件内所有的内容,改为显示经 QPicture 类处理的图像。 |
setPixmap(const QPixmap &) | 清空 QLabel 控件内所有的内容,改为显示经 QPixmap 类处理的图像。 |
setText(const QString &) | 清空 QLabel 控件内所有的内容,改为显示指定的文本。 |
除了表 2、3 罗列的这些信号和槽函数外,QLabel 类还从父类处继承了一些信号和槽函数,这里不再一一罗列。
接下来通过一个实例,给大家演示 QLabel 控件中一些属性和方法的用法。
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建一个文本框
QLabel lab;
//设置文本框内容居中显示
lab.setAlignment(Qt::AlignCenter);
//设置文本框的坐标和尺寸
lab.setGeometry(100,100,400,400);
//设置文本框的外观,包括字体的大小和颜色、按钮的背景色
lab.setStyleSheet("QLabel{font:30px;color:red;background-color:rgb(f9,f9,f9);}");
//设置文本框要显示超链接内容
lab.setText("C语言中文网");
//当用户鼠标位于文本框上时,显示提示内容
lab.setToolTip("点击超链接显示URL");
//提示内容显示 1 秒
lab.setToolTipDuration(1000);
//为文本框设置信号和槽,当用户点击超链接时,将文本框内容改为超链接的 URL
QObject::connect(&lab,&QLabel::linkActivated,&lab,&QLabel::setText);
//程序运行后,文本框显示
lab.show();
return a.exec();
}
执行结果如下图所示,用户最先看到的是图 3a),当用户鼠标移动到文本框区域内时,会提示“点击超链接显示URL”,提示时间为 1 秒。当用户点击“C语言中文网”时会触发 linkActivated() 信号函数,该函数会调用 setText() 函数,将文本框中显示的“C语言中文网”改为“http://c.biancheng.net”,字体颜色为红色,如图 3b) 所示。
图 3 程序运行结果
有关 QLabel 类提供的更多属性和方法,后续章节用到时会做详细地讲解,您也可以借助 Qt Creator 提供的 Qt 帮助手册自行查看 QLabel 类提供的成员。
按钮是 GUI 开发中最常用到的一种控件,作为一款著名的 GUI 开发框架,Qt 提供了很多种按钮,比如 QPushButton(普通按钮)、QRadioButton(单选按钮)、QToolButton(工具栏按钮)等。
QPushButton 是实际开发中最常使用的一种按钮,本节就给大家详细讲解它的用法。
QPushButton 类间接继承自 QWidget 类,它的继承关系如下:
QPushButton -> QAbstractButton -> QWidget
QAbstractButton 类是所有按钮控件类的基类,包含很多通用的按钮功能。
QPushButton 类专门用来创建可按压的按钮,如图 1 所示。
QPushButton 按钮上除了可以放置一串文本,文本左侧还可以放置图标,必要时还可以在按钮上放置图片。QPushButton 按钮可以作为一个独立的窗口,但实际开发中很少这样用,通常的用法是像图 1 这样将按钮内嵌到某个窗口中,作为一个子控件和其它控件搭配使用。
QPushButton 类提供了 3 个构造函数,分别是:
QPushButton(QWidget *parent = Q_NULLPTR)
QPushButton(const QString &text, QWidget *parent = Q_NULLPTR)
QPushButton(const QIcon &icon, const QString &text, QWidget *parent = Q_NULLPTR)
parent 参数用于指定父窗口;text 参数用于设置按钮上要显示的文字;icon 参数用于设置按钮上要显示的图标。
注意,第一个构造函数的 parent 参数附有默认值,所以 QPushButton 类还隐含着一个默认构造函数。也就是说,实例化 QPushButton 类对象时可以不传递任何参数。
QPushButton 类提供了很多实用的属性和方法,它还从父类继承了很多属性和方法。下表给大家罗列了一些比较常用的属性和方法:
属 性 | 含 义 |
---|---|
text | 保存按钮上要显示的文字。 该属性的值可以通过 text() 方法获取,也可以通过 setText(const QString &text) 方法修改。 |
icon | 保存按钮左侧要显示的图标。 该属性的值可以通过 icon() 方法获取,也可以通过 setIcon(const QIcon &icon) 方法修改。 |
iconsize | 保存按钮左侧图标的尺寸。 该属性的值可以通过 iconSize() 方法获取,也可以通过 setIconSize(const QSize &size) 方法修改。 |
size | 保存按钮的尺寸。 该属性的值可以通过 size() 方法获取,也可以通过 resize(int w, int h) 或者 resize(const QSize &) 方法修改。 |
font | 保存按钮上文字的字体和大小。 该属性的值可以通过 font() 方法获取,也可以通过 setFont(const QFont &) 方法修改。 |
flat | 初始状态下,按钮是否显示边框。flat 属性的默认值为 flase,表示按钮带有边框。 该属性的值可以通过 isFlat() 方法获取,也可以通过 setFlat(bool) 方法修改。 |
enabled | 指定按钮是否可以被按下。 该属性的默认值为 true,表示按钮可以被按下,即按钮处于启用状态。当该属性的值为 false 时,按钮将不能被点击,按钮处于禁用状态。 该属性的值可以通过 isEnabled() 方法获取,也可以通过 setEnabled(bool) 方法进行修改。 |
autoDefault | 当用户按下 Enter 回车键时,是否触发点击按钮的事件。 当按钮的父窗口为 QDialog 窗口时,该属性的值为 true;其它情况下,该属性的默认值为 false。 该属性的值可以通过 autoFault() 方法获取,也可以通过 setAutoFault(bool) 方法修改。 |
除了表 1 中罗列的获取、修改属性值的方法外,QPushButton 类常用的成员方法还有:
方 法 | 功 能 |
---|---|
move(int x, int y) | 手动指定按钮位于父窗口中的位置。 |
setStyleSheet(const QString &styleSheet) | 自定义按钮的样式,包括按钮上文字或图片的显示效果,按钮的形状等等。 |
setGeometry(int x, int y, int w, int h) | 同时指定按钮的尺寸和位置。 |
adjustSize() | 根据按钮上要显示的内容,自动调整按钮的大小。 |
setDisabled(bool disable) | 指定按钮是否可以被按下。当 disable 值为 true 时,表示按钮不能被按下,即禁用按钮的功能。 |
GUI 程序中,按钮的主要任务是完成和用户之间的交互,下表罗列了 QPushButton 类常用的信号函数和槽函数:
信号函数 | 功 能 |
---|---|
clicked() clicked(bool checked = false) | 用户点击按钮并释放(或者按下按钮对应的快捷键)后,触发此信号。 |
pressed() | 用户按下按钮时会触发此信号。 |
released() | 用户松开按钮时会触发此信号。 |
槽函数 | 功 能 |
click() | 单击指定的按钮。 |
setIconSize() | 重新设置按钮上图片的尺寸。 |
hide() | 隐藏按钮控件。 |
setMenu(QMenu *menu) | 弹出与按钮关联的菜单。 |
接下来通过一个实例,给大家演示 QPushButton 按钮的用法:
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget widget;
//设置 widget 窗口的标题
widget.setWindowTitle("QWidget窗口");
//创建一个按钮,并内嵌到 widget 窗口中
QPushButton but("QPushButton按钮",&widget);
//按钮的位置位于距 widget 窗口左上角 (100,100) 的位置
but.move(100,100);
//设置按钮上文字的大小。
but.setStyleSheet("QPushButton{font:20px;}");
//调整按钮的尺寸
but.resize(200,200);
//建立信息和槽,当用户点击并释放按钮后,该按钮隐藏。
QObject::connect(&but,&QPushButton::clicked,&but,&QPushButton::hide);
widget.show();
return a.exec();
}
将程序复制到 main.cpp 文件中,运行结果为:
图 2 运行结果
QLineEdit 是 Qt 提供的一个控件类,它直接继承自 QWdiget 类,专门用来创建单行输入框,如下图所示:
实际开发中,我们经常用到 QLineEdit 输入框,比如接收用户输入的个人信息、账户信息、角色名称等,就可以用 QLineEdit 实现。
每个单行输入框都是 QLineEdit 类的一个实例对象,QLineEdit 类提供有两个构造函数,分别是:
QLineEdit(QWidget *parent = Q_NULLPTR)
QLineEdit(const QString &contents, QWidget *parent = Q_NULLPTR)
contents 参数用于指定输入框中的文本内容;parent 参数用于指定新建输入框控件的父窗口,新建输入框将会内嵌到父窗口上,作为父窗口的一个子控件。当然,我们也可以不指定父窗口,那么新建的输入框就会作为独立的窗口。
在 QLineEdit 输入框中,用户可以直接输入一行文本,也可以粘贴一行文本,还可以修改输入框内的文本。某些实际场景中,QLineEdit 输入框还可以对用户输入的内容加以限定,比如:
QLineEdit 类的内部提供了很多实用的属性和方法,同时还从 QWidget 父类处继承了一些属性和方法。
下表列出了 QLineEdit 类对象经常调用的一些属性以及它们各自的含义:
属 性 | 含 义 |
---|---|
text | 保存输入框中的文本。 该属性的值可以通过 text() 方法获取,也可以通过 setText(const QString &) 方法修改。 |
maxLength | 设置输入框中最多可以放置的文本长度。当文本长度超出最大限度后,超出部分将被丢弃。 默认情况下,maxLength 的值为 32767。该属性的值可以通过 maxLength() 函数获得,也可以通过 setMaxLength(int) 方法修改。 |
placeholderText | 设置提示信息,例如当用户未选中输入框时,输入框中显示“请输入…”,而用户选中输入框时,“请输入…” 随之消失。 该属性的值可以通过 placeholderText() 方法获取,也可以通过 setPlaceholderText(const QString &) 方法修改。 |
clearButtonEnabled | 当输入框中有文本时,输入框的右侧可以显示一个“一键清除”按钮。该属性的默认值为 false,即输入框中不会自动显示清除按钮。 该属性的值可以通过 isClearButtonEnabled() 方法获取,也可以通过 setClearButtonEnabled(bool enable) 方法修改。 |
echoMode | 设定输入框中文本的显示样式,该属性的可选值有以下几个:QLineEdit::Normal:正常显示所输入的字符,此为默认选项。QLineEdit::NoEcho:不显示任何输入的字符,常用于密码类型的输入,且长度保密QLineEdit::Password:显示与平台相关的密码掩饰字符,而不是实际输入的字符。当用户重新点击输入框时,可以紧接着之前的文本继续输入。QLineEdit::PasswordEchoOnEdit:编辑时正常显示输入的字符,编辑完成后改为用密码掩饰字符显示。当用户重新点击输入框时,不能紧接着之前的文本继续输入。 该属性的是可以通过 echoMode() 方法获取,也可以通过 setEchoMode(EchoMode) 方法修改。 |
frame | 控制输入框的边框。默认情况下,输入框是带有边框的。 该属性的值可以通过 hasFrame() 方法获取,也可以通过 setFrame(bool) 方法修改。 |
除了上表提到的获取和修改属性值的方法外,QLineEdit 类还提供了一些功能实用的方法,例如:
成员方法 | 功 能 |
---|---|
move(int x, int y) | 指定输入框位于父窗口中的位置。 |
setValidator(const QValidator *v) | 限制输入框中的文本内容,比如输入框只包含整数。 |
setReadOnly(bool) | 设置输入框是否进入只读状态。在只读状态下,用户仍可以采用粘贴、拖拽的方式向输入框中放置文本,但无法进行编辑。 |
setAlignent(Qt::Alignment flag) | 设置输入框中输入文本的位置。 |
QLineEdit 类提供了几个信号函数,分别对应用户的几种输入状态。
信号函数 | 功 能 |
---|---|
textEdited(const QString &text) | 当用户编辑输入框中的文本时,此信号就会触发,text 参数即为用户新编辑的文本。 注意,当程序中试图通过 setText() 方法修改输入框中的文本时,不会触发此信号函数。 |
textChanged(const QString &text) | 只要输入框中的文本内容发生变化,就会触发此信息。 |
returnPressed() | 用户按下回车键时,会触发此信号。 |
editingFinished() | 用户按下回车键,或者鼠标点击输入框外的其它位置时,会触发此信号。 |
QLineEdit 类常用的槽函数有以下几个:
槽函数 | 功 能 |
---|---|
clear() | 清空文本框中的内容。 |
setText(const QString &) | 重新指定文本框中的内容。 |
下面的实例给大家演示了 QLineEdit 单行输入框控件的基本用法,同时还演示了几个成员方法的用法。
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建一个窗口,作为输入框的父窗口
QWidget widget;
//设置窗口的标题
widget.setWindowTitle("QWidget窗口");
//接下来,分别创建两个输入框,用于让用户分别输入账号和密码
//创建账号输入框
QLineEdit lineEdit(&widget);
//指定输入框位于父窗口中的位置
lineEdit.move(100,100);
//设置提示信息
lineEdit.setPlaceholderText("请输入账号...");
//让输入框显示“一键清除”按钮
lineEdit.setClearButtonEnabled(true);
//创建密码输入框
QLineEdit lineEditPass(&widget);
lineEditPass.setPlaceholderText("请输入密码...");
lineEditPass.move(100,150);
//指定文本显示方式,保护用户账号安全
lineEditPass.setEchoMode(QLineEdit::Password);
//指定窗口的尺寸和显示文字的大小
widget.resize(500,300);
widget.setFont(QFont("宋体",16));
widget.show();
return a.exec();
}
运行结果为:
很多应用程序中需要以列表的形式向用户展示数据(资源),比如 Windows 操作系统会以列表的方式展示很多张桌面背景图(如图 1a) 所示),再比如很多音乐播放器中以列表的形式展示音乐资源,用户可以选择自己喜欢的音乐(如图 1b) 所示)。
使用 Qt 框架开发 GUI 程序,如果需要以列表的方法展示数据,可以优先考虑用 QListWidget 类实现。
QListWidget 是 Qt 提供的控件类,专门用来创建列表。QListWidget 类的继承关系如下:
QListWidget -> QListView -> QAbstractItemView -> QAbstractScrollArea -> QFrame -> QWidget
这里着重介绍一下 QListView 类,它也可以用来创建列表。对于初学者来说,我强烈建议先学习 QListWidget,它是“简易版”的 QListView,创建和使用列表的方式更简单、门槛更低,对初学者更友好。当然,QListWidget 只能创建结构简单的列表,如果要制作复杂的列表,应优先考虑 QListView,因为它的功能更强大,很多 QListWidget 难以实现的功能,QListView 都能实现。
通过实例化 QListWidget 类,可以很轻松地创建一个列表。QListWidget 类只提供了 1 个构造函数:
QListWidget(QWidget *parent = Q_NULLPTR)
parent 参数用来指定新建列表的父窗口,该参数的默认值是 Q_NULLPTR,表示新建控件没有父窗口。
语法层面上分析,可以不为 QListWidget 列表指定父窗口,那么它将作为一个独立的窗口。但实际开发中,通常会为 QListWidget 列表指定一个父窗口(例如 QWidget 窗口),它将作为父窗口中的一个子控件,和窗口中的其它控件一起搭配使用。
QListWidget 列表控件可以显示多份数据,每份数据习惯称为列表项(简称项),每个列表项都是 QListWidgetItem 类的实例对象。也就是说,QListWidget 中有多少个列表项,就有多少个 QListWidgetItem 类对象。
默认情况下,QListWidget 中每个列表项独自占用一行,每个列表项中可以包含文字、图标等内容。实际开发中,我们还可以将指定的窗口或者控件放置到列表项中显示,例如 QWidget 窗口、QLabel 文本框、QPushButton 按钮、QLineEdit 输入框等。
借助 QListWidgetItem 类,可以轻松管理 QListWidget 中的每个列表项,包括:
当然,QListWidgetItem 类还提供有很多其它的成员方法,这里不再一一罗列。
对于刚刚创建好的 QListWidget 类对象,不包含任何 QListWidgetItem 类对象,就是一个空列表。借助 QListWidget 类以及父类提供的属性和方法,我们可以对新建列表执行多种操作。
下表给大家罗列了一些 QListWidget 类常用的属性和方法:
属性名或方法名 | 功 能(含 义) |
---|---|
count 属性 | 保存当前列表中含有的列表项的总数。 该属性的值可以通过 count() 方法获取。 |
currentRow 属性 | 保存当前选择的列表项所在的行数。 该属性的值可以通过 currentRow() 方法获取,也可以通过 setCurrentRow(int row) 方法修改当前选择的列表项。 |
sortingEnabled 属性 | 决定当前 QlistWidget 列表是否开发排序功能,默认值为 false,即不开启排序功能。 该属性的是可以通过 isSortingEnabled() 方法获取,可以通过 setSortingEnabled(bool enable) 方法进行修改。 |
SelectionMode 属性 | 指明当前列表中是否可以同时选择多个列表项,或者是否只能连续选择多个列表项。 该属性是枚举类型,可选值有 5 个:QAbstractItemView::SingleSelection:用户最多只能选择一个列表项。QAbstractItemView::ContiguousSelection:用户按住 Shift 键,可以连续选择多个列表项。QAbstractItemView::ExtendedSelection:用户按住 Ctrl 键,可以选中多个列表项。当用户按住 Shift 键,也可以连续选择多个列表项。QAbstractItemView::MultiSelection:用户可以选择多个列表项。QAbstractItemView::NoSelection:用户无法选择任何列表项。 该属性的值可以通过 selectionMode() 方法获取,也可以通过 setSelectionMode(QAbstractItemView::SelectionMode mode) 方法进行修改。 |
ViewMode 属性 | 指定 QListWidget 是按行显示数据(如图 1b) ),还是分列显示数据(如图 1a) )。 该属性是枚举类型,可选值有 2 个:QListView::ListMode:一行一行地显示列表项,默认情况下,各个列表项不能拖动。QListView::IconMode:分列显示列表项,默认情况下,各个列表项可以拖动。 该属性的值可以通过 viewMode() 方法获取,也可以通过 setViewMode(ViewMode mode) 方法进行修改。 |
void addItem ( const QString & label ) void addItem ( QListWidgetItem * item ) void addItems ( const QStringList & labels ) | 向 QListWidget 列表的尾部添加指定项,可以是一个文本(label)、一个列表项(item),还可以一次性添加多个文本(labels)。 |
void QListWidget::setItemWidget(QListWidgetItem *item, QWidget *widget) | 将指定的 widget 窗口添加到 item 列表项中。 |
currentItem() | 返回当前选中的列表项。 |
removeItemWidget(QListWidgetItem *item) | 删除指定的 item 列表项。 |
sortItems(Qt::SortOrder order = Qt::AscendingOrder) | 默认将所有列表项按照升序排序,通过指定参数为 Qt::DescendingOrder,可以进行降序排序。 |
takeItem(int row) | 返回位于 row 行的列表项。 |
selectedItems() | 返回当前被选择的所有列表项。 |
对于给定的 QlistWidget 列表,用户可以选中其中的一个或者某些列表项,甚至还可以修改列表项中的内容。QListWidget 类具有很多信号和槽信息,可以捕捉用户的很多动作,还可以针对用户的动作做出适当地响应。
下表给大家罗列了一些常用的信号和槽函数:
信号函数 | 功 能 |
---|---|
itemClicked(QListWidgetItem *item) | 用户点击某个列表项时会触发此信号函数,item 参数指的就是被用户点击的列表项。 |
itemDoubleClicked(QListWidgetItem *item) | 用户双击某个列表项时会触发此信号函数,item 参数指的就是被用户双击的列表项。 |
itemPressed(QListWidgetItem *item) | 鼠标按下某个列表项时会触发此信号函数,item 参数指的就是被鼠标按下的列表项, |
itemSelectionChanged() | 当选择的列表项发生变化时会触发此信号函数。 |
currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) | 当前列表项发生变化时会触发此信号函数,current 参数指的是新选择的列表项;previous 参数指的是先前选择的列表项。 |
槽函数 | 功 能 |
clear() | 删除列表中的所有列表项。 |
scrollToItem(const QListWidgetItem *item, QAbstractItemView::ScrollHint hint = EnsureVisible) | 用 hint 参数指定的滑动方式,让用户看到指定的 item 列表项。 |
selectAll() | 选择所有的列表项。 |
scrollToBottom() scrollToTop() | 分别将列表滑动到底部和顶部。 |
接下来,我们亲自制作一个 QListWidget 列表:
//main.cpp#include #include #include #include #include using //main.cpp
#include
#include
#include
#include
#include
using namespace std;
class QMyLabel:public QLabel{
Q_OBJECT
public slots:
void rsetText(QListWidgetItem *item);
};
void QMyLabel::rsetText(QListWidgetItem *item){
this->setText(item->text());
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建一个窗口,作为输入框的父窗口
QWidget widget;
//设置窗口的标题
widget.setWindowTitle("QWidget窗口");
widget.resize(500,500);
QListWidget listWidget(&widget);
listWidget.resize(500,400);
listWidget.setFont(QFont("宋体",14));
listWidget.addItem("C语言中文网");
listWidget.addItem("http://c.biancheng.net");
listWidget.addItem(new QListWidgetItem("Qt教程"));
QMyLabel print;
print.setText("选中内容");
print.setParent(&widget);
print.resize(500,100);
print.move(0,400);
print.setAlignment(Qt::AlignCenter);
QObject::connect(&listWidget,&QListWidget::itemClicked,&print,&QMyLabel::rsetText);
widget.show();
return a.exec();
}
//QMyLabel类的定义应该放到 .h 文件中,本例中将其写到 main.cpp 中,程序最后需要添加 #include "当前源文件名.moc" 语句,否则无法通过编译。
#include "main.moc"
程序中,我们自定义了一个 QMyLabel 类,它继承自 QLabel 文本框类,因此 QMyLabel 也是一个文本框类。在 QMyLabel 类中,我们自定义了一个 rsetText() 槽函数。
程序的运行结果为:
通过运行动画可以看到,我们将 QListMidget 和自定义的 QMyLabel 控件合理地分布在 QWidget 窗口上,通过为它们的信号和槽建立连接,使得当点击列表中的某个列表项时,文本框可以显示列表项中的文本内容。
实际开发中,一个界面上可能包含十几个控件,手动调整它们的位置既费时又费力。作为一款成熟的 GUI 框架,Qt 提供了很多摆放控件的辅助工具(又称布局管理器或者布局控件),它们可以完成两件事:
总之借助布局管理器,我们无需再逐个调整控件的位置和大小,可以将更多的精力放在软件功能的实现上。
Qt 共提供了 5 种布局管理器,每种布局管理器对应一个类,分别是 QVBoxLayout(垂直布局)、QHBoxLayout(水平布局)、QGridLayout(网格布局)、QFormLayout(表单布局)和 QStackedLayout(分组布局),它们的继承关系如下图所示:
垂直布局指的是将所有控件从上到下(或者从下到上)依次摆放,例如:
图 2 展示了 4 个 QPushButton 按钮利用 QVBoxLayout 垂直布局的效果。实际场景中,QVBoxLayout 中还可以放置其它控件,比如 QLabel 文本框、QLineEdit 单行输入框等。
程序中使用 QVBoxLayout 布局控件,需提前引入
头文件。每个 QVBoxLayout 控件本质都是 QVBoxLayout 类的实例对象,该类提供了两个构造函数,分别是:
QVBoxLayout()
QVBoxLayout(QWidget *parent)
创建 QVBoxLayout 控件的同时可以指定父窗口,那么它将作为父窗口中管理其它控件的工具;也可以暂时不指定父窗口,待全部设置完毕后再将其添加到某个窗口中。
QVBoxLayout 类没有新增任何成员方法,它只能使用从父类继承的成员方法,下表给大家罗列了常用的一些:
成员方法 | 功 能 |
---|---|
void QBoxLayout::addWidget(QWidget *widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment()) | 向布局管理器中添加指定的 widget 控件。 默认情况下,stretch 拉伸系数为 0,表示 widget 控件的尺寸为默认值;alignment 是一个枚举类型参数,默认值也是 0,表示该控件会填满占用的整个空间。 |
void QBoxLayout::addStretch(int stretch = 0) | 添加一个空白行,整个窗口中除了控件占用的区域外,其它区域可以由多个(≥0)空白行分摊,分摊比例取余于各个空白行设置的 stretch 参数的值。 strech 参数的默认值为 0,表示当窗口很小时,空白行可以不占据窗口空间。当窗口中包含多个 strech 值为 0 的空白行时,它们会平分窗口中的空白区域。 |
void QBoxLayout::addSpacing(int size) | 添加一个 size 大小的固定间距。 |
void QLayout::setMargin(int margin) | 设置布局管理器中所有控件的外边距,上、下、左、右外边距的大小都为 margin。默认情况下,所有方向的外边距为 11 px。 |
void QLayout::setContentsMargins(int left, int top, int right, int bottom) | 设置布局管理器中所有控件的外边距,和 setMargin() 的区别是,此方法可以自定义上、下、左、右外边距的值。 |
void QBoxLayout::setDirection(Direction direction) | 设置布局管理器中控件的布局方向,Direction 是一个枚举类型,对于 QVBoxLayout 布局管理器,direction 参数的值通常选择 QBoxLayout::TopToBottom(从上到下依次摆放)或者 QBoxLayout::BottomToTop(从下到上依次摆放)。 |
bool QBoxLayout::setStretchFactor(QWidget *widget, int stretch) | 设置布局管理器中某个控件的拉伸系数。 |
bool QBoxLayout::setStretchFactor(QLayout *layout, int stretch) | 布局管理器内部可以再放置一个布局管理器,该方法用来设置内部某个布局管理器的拉伸系数。 |
举个简单的例子:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
QWidget widget;
widget.setWindowTitle("QVBoxLayout垂直布局");
//创建垂直布局管理器
QVBoxLayout *layout=new QVBoxLayout;
//设置布局管理器中所有控件从下往上依次排列
layout->setDirection(QBoxLayout::BottomToTop);
//连续创建 3 个文本框,并设置它们的背景和字体大小
QLabel lab1("Label1");
lab1.setStyleSheet("QLabel{background:#dddddd;font:20px;}");
lab1.setAlignment(Qt::AlignCenter);
QLabel lab2("Label2");
lab2.setStyleSheet("QLabel{background:#cccccc;font:20px;}");
lab2.setAlignment(Qt::AlignCenter);
QLabel lab3("Label3");
lab3.setStyleSheet("QLabel{background:#ffffff;font:20px;}");
lab3.setAlignment(Qt::AlignCenter);
//将 3 个文本框和 2 个空白行添加到管理器中,它们的伸缩系数比是 2:1:2:3:3
layout->addStretch(2);
layout->addWidget(&lab1,1);
layout->addWidget(&lab2,2);
layout->addWidget(&lab3,3);
layout->addStretch(3);
//将布局管理器添加到 widget 窗口中
widget.setLayout(layout);
widget.show();
return a.exec();
}
程序中做了以下几个操作:
执行结果为:
水平布局指的是将所有控件从左到右(或者从右到左)依次摆放,例如:
使用 QHBoxLayout 水平布局控件,程序中要提前引入
头文件。QHBoxLayout 和 QVBoxLayout 继承自同一个类,它们的用法非常相似,比如 QHBoxLayout 类也提供了两个构造函数:
QHBoxLayout()
QHBoxLayout(QWidget *parent)
QHBoxLayout 类也没有新添任何成员方法,它只能使用从父类继承的成员方法。因此,表 1 中罗列的所有成员方法也同样适用于 QHBoxLayout 对象。
注意,当 QHBoxLayout 对象调用表 1 中的 addStretch() 方法时,表示添加一个空白列。
举个简单的例子:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
QWidget widget;
widget.setWindowTitle("QHBoxLayout水平布局");
//创建水平布局管理器
QHBoxLayout *layout=new QHBoxLayout;
//设置布局管理器中所有控件的布局方向为从右往左依次排列
layout->setDirection(QBoxLayout::RightToLeft);
//连续创建 3 个文本框,并设置它们的背景和字体大小
QLabel lab1("Label1");
lab1.setStyleSheet("QLabel{background:#dddddd;font:20px;}");
lab1.setAlignment(Qt::AlignCenter);
QLabel lab2("Label2");
lab2.setStyleSheet("QLabel{background:#cccccc;font:20px;}");
lab2.setAlignment(Qt::AlignCenter);
QLabel lab3("Label3");
lab3.setStyleSheet("QLabel{background:#ffffff;font:20px;}");
lab3.setAlignment(Qt::AlignCenter);
//将 3 个文本框和 2 个空白列添加到管理器中,它们的拉伸系数比是 2:1:2:3:3
layout->addStretch(2);
layout->addWidget(&lab1,1);
layout->addWidget(&lab2,2);
layout->addWidget(&lab3,3);
layout->addStretch(3);
//将布局管理器添加到 widget 窗口中
widget.setLayout(layout);
widget.show();
return a.exec();
}
程序执行结果为:
图 5 中,最左侧和最右侧各添加了一个空白列,它们的伸缩比例为 3:2,即它们的宽度比为 3:2。
网格布局又称格栅布局或者表格布局,指的是将一些控件按照行和列排列在窗口上,例如:
QGridLayout 的行标和列标都从 0 开始,例如图 6 中 one 按钮的位置为 (0, 0),Four 按钮的位置为 (2, 0)。我们可以随意指定 QGridLayout 的行数和列数,各个控件可以随意摆放,必要时某些位置可以空着不用。
使用 QGridLayout 网格控件,程序中需引入
头文件。每个 QGridLayout 控件都是 QGridLayout 类的一个实例对象,该类提供了两个构造函数,分别是:
QGridLayout(QWidget *parent)
QGridLayout()
QGridLayout 类提供了很多实用的成员方法,常用的如下表所示:
成员方法 | 功 能 |
---|---|
int QGridLayout::rowCount() const | 获取网格的行数。 |
int QGridLayout::columnCount() const | 获取网格的列数。 |
void QGridLayout::addWidget(QWidget *widget, int row, int column, Qt::Alignment alignment = Qt::Alignment()) | 将 widget 控件添加到网格中的 (row,column) 位置处,并且可以自定义该控件的对齐方式。 |
void QGridLayout::addWidget(QWidget *widget, int fromRow, int fromColumn, int rowSpan, int columnSpan, Qt::Alignment alignment = Qt::Alignment()) | 将 widget 控件从 (fromRow, fromColumn) 位置开始,跨 rowSpan 行和 ColumnSpan 列添加到网格中,并且可以自定义该控件的对齐方式。 |
void QGridLayout::addLayout(QLayout *layout, int row, int column, Qt::Alignment alignment = Qt::Alignment()) | 向网格中的 (row, column) 位置处添加 layout 布局管理器。 |
void QGridLayout::addLayout(QLayout *layout, int row, int column, int rowSpan, int columnSpan, Qt::Alignment alignment = Qt::Alignment()) | 将 layout 布局管理器从 (row, column) 位置开始,跨 rowSpan 行和 ColumnSpan 列添加到网格中,并且可以自定义该布局控件的对齐方式。 |
void QGridLayout::setColumnStretch(int column, int stretch) | 给指定的第 column 列设置伸缩系数。 |
void QGridLayout::setRowStretch(int row, int stretch) | 给指定的第 row 行设置伸缩系数。 |
void QGridLayout::setColumnMinimumWidth(int column, int minSize) | 设置第 column 列的最小宽度。 |
void QGridLayout::setRowMinimumHeight(int row, int minSize) | 设置第 row 行的最小宽度。 |
举个简单的例子:
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
QWidget widget;
widget.setWindowTitle("QGridLayout网格布局");
//创建 4 个按钮和 1 个文本框
QPushButton *but1 = new QPushButton("but1");
QPushButton *but2 = new QPushButton("but2");
QLabel *lab3 = new QLabel("lab");
lab3->setStyleSheet("QLabel{background:#dddddd;font:20px;}");
lab3->setAlignment(Qt::AlignCenter);
QPushButton *but3 = new QPushButton("but3");
QPushButton *but4 = new QPushButton("but4");
//创建网格布局控件
QGridLayout *layout = new QGridLayout;
//向 layout 中添加控件,并指定各个控件的位置
layout->addWidget(but1, 0, 0);
layout->addWidget(but2, 0, 2);
layout->addWidget(lab3, 1, 0, 3, 3);
layout->addWidget(but3, 4, 0);
layout->addWidget(but4, 4, 2);
//将 layout 添加到 widget 窗口中
widget.setLayout(layout);
widget.show();
return a.exec();
}
程序运行结果为:
图 7 中,文本框控件从 (1,0) 位置开始,占据了 3 行 3 列的表格空间。
Qt 提供了很多种输入框控件,包括 QLineEdit 单行输入框、QTextEdit 多行输入框等。通常情况下,每个输入框的旁边都会附带一些文字(又称标签),用来提示用户需要输入的信息。例如,图 8 中第一个输入框的标签为 “Name”,提示用户填写自己的姓名。
生成图 8 这样的界面,实现的方法有很多,例如:
第 1 种方法最大的弊端在于,各个控件的尺寸都是固定的,不会随着父窗口尺寸的改变而改变。第 2、3、4 种方法都是借助布局控件实现的,各个控件的尺寸可以自动调整,但前两种方法需要手动设置每一列的 strech 拉伸系数,而第 4 种方式不需要。总之对于生成类似图 8 这样的表单窗口,建议大家使用 QFormLayout 控件,因为使用 QFormLayout 编写的代码量最少,开发效率最高。
QFormLayout 可以容纳很多个输入框以及对应的标签,并将它们从上到下依次排列在界面上(如图 8 所示)。大多数情况下,QFormLayout 底层是用 QGridLayout 网格布局管理器实现的,和后者不同的是,QFormLayout 只包含 2 列(不限制行数),且第一列放置标签,第二列放置输入框。
使用 QFormLayout 布局控件之前,程序中应引入
头文件。每一个表单布局控件都是 QFormLayout 类的一个实例对象,该类仅提供了一个构造函数:
QFormLayout(QWidget *parent = Q_NULLPTR)
下表给大家罗列了操作 QFormLayout 对象常用的一些成员方法:
成员方法 | 功 能 |
---|---|
void QFormLayout::addRow(QWidget *label, QWidget *field) | 将指定的 field 控件和存储标签的 label 控件添加到表单控件中的末尾。 |
void QFormLayout::addRow(const QString &labelText, QWidget *field) | 将指定的 field 控件和 labelText 标签添加到表单控件的末尾。 |
void QFormLayout::insertRow(int row, const QString &labelText, QWidget *field) | 将指定的 field 控件和 labelText 标签插入到表单控件中指定行的位置。 |
void QFormLayout::removeRow(int row) | 删除表单控件中的指定行。 |
void QFormLayout::removeRow(QWidget *widget) | 删除表单控件中 widget 控件所在的行。 |
void setRowWrapPolicy(RowWrapPolicy policy) | 设置标签的显示格式,默认标签位于控件的左侧。 RowWrapPolicy 是 QFormLayout 中定义的枚举类型,该类型包含 3 个值:QFormLayout::DontWrapRows:标签始终在输入框的左侧;QFormLayout::WrapLongRows:根据输入框的尺寸,标签可能位于输入框的左侧,也可能位于上方;QFormLayout::WrapAllRows:标签始终在输入框的上方; |
void QFormLayout::setSpacing(int spacing) | 将行间距和列间距设置为 spacing。 |
举个简单的例子:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
QWidget widget;
widget.setWindowTitle("QFormLayout表单布局");
//创建 4 个按钮和 1 个文本框
QFormLayout* layout = new QFormLayout();
//设置表单中的标签都位于控件的上方
layout->setRowWrapPolicy(QFormLayout::WrapAllRows);
//添加 3 行输入框和标签
layout->addRow("Name:",new QLineEdit());
layout->addRow("Email:",new QLineEdit());
layout->addRow("Adress:",new QLineEdit());
//设置行间距和列间距为 10
layout->setSpacing(10);
//将 layout 表单添加到 widget 窗口中
widget.setLayout(layout);
widget.show();
return a.exec();
}
程序运行结果为:
QStackedLayout 布局管理器可以容纳多个控件或者窗口,但每次只显示其中的一个。
举个简单的例子,下图中的界面就使用了 QStackedLayout 布局管理器:
整个窗口被一分为二,左侧是 QListWidget 列表控件,右侧是 QStackedLayout 布局管理器。QStackedLayout 中包含 QPushButonn、QLabel 和 QLineEdit 这 3 个控件,但每次只能 3 个控件中的一个。
QStackedLayout 自身无法切换当前显示的控件或窗口,实际应用时通常和 QListWidget 或者 QComboBox 搭配使用。
使用 QStackedLayout 布局控件,程序中必须先引入
头文件。 每个 QStackedLayout 控件都是 QStackedLayout 类的一个实例对象,该类提供有 3 个构造函数,分别是:
QStackedLayout()
QStackedLayout(QWidget *parent)
QStackedLayout(QLayout *parentLayout)
借助第二个构造函数,我们可以将 QStackedLayout 添加到指定的 parent 窗口中;借助第三个构造函数,我们可以将 QStackedLayout 嵌入到指定的 parentLayout 布局控件中
本节学习的 5 种布局控件都可以嵌套使用,例如将 QVBoxLayout 放到 QHBoxLayout 内部、将 QGridLayout 放到 QStackedLayout 内部等。
下表罗列了操作 QStackedLayout 对象常用的一些成员方法:
成员方法 | 功 能 |
---|---|
int QStackedLayout::addWidget(QWidget *widget) | 将 widget 控件添加到 QStackedLayout 控件中。 |
int QStackedLayout::insertWidget(int index, QWidget *widget) | 将 widget 控件插入到 QStackedLayout 控件指定的位置处。 |
信号函数 | 功 能 |
void QStackedLayout::currentChanged(int index) | 切换当前显示的控件时,会触发此信号,index 为显示的新控件的索引。 |
void QStackedLayout::widgetRemoved(int index) | 移除某个控件时,会触发此信号,index 为被移除控件的索引。 |
槽函数 | 功 能 |
void setCurrentIndex(int index) | 将第 index 个控件作为要显示的控件。 |
void QStackedLayout::setCurrentWidget(QWidget *widget) | 设置 widget 作为当前要实现的控件。注意,必须保证 widget 存储在 QStackedLayout 控件中。 |
这里我们以图 10 所示的窗口为例,实现代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
QWidget widget;
widget.setWindowTitle("QStackedLayout分组布局");
widget.resize(600,400);
//向主窗口中添加一个水平布局控件
QHBoxLayout *layout=new QHBoxLayout;
//创建一个列表
QListWidget listWidget(&widget);
listWidget.setMinimumWidth(150);
listWidget.setFont(QFont("宋体",14));
listWidget.addItem("QPushButton");
listWidget.addItem("QLabel");
listWidget.addItem("QLineEdit");
//新建 3 个窗口,分别放置文本框、按钮和单行输入框
QWidget widget1;
widget1.setMinimumSize(400,400);
QPushButton but1("这是一个按钮",&widget1);
QWidget widget2;
widget2.setMinimumSize(400,400);
QLabel lab1("这是一个文本框",&widget2);
QWidget widget3;
widget3.setMinimumSize(400,400);
QLineEdit edit("这是一个单行输入框",&widget3);
//创建一个分组布局,将 3 个窗口添加到分组控件中
QStackedLayout *stackedLayout = new QStackedLayout;
stackedLayout->addWidget(&widget1);
stackedLayout->addWidget(&widget2);
stackedLayout->addWidget(&widget3);
//layout 第一列添加 QListWidget 控件,第二列添加分组布局控件,设置它们的伸缩系数比为 1:4
layout->addWidget(&listWidget,1);
layout->addLayout(stackedLayout,4);
//将 layout 水平布局控件添加到 widget 窗口中
widget.setLayout(layout);
widget.show();
//连接信号和槽,实现当点击列表中的某一项,切换分组布局管理器显示的控件
QObject::connect(&listWidget,&QListWidget::currentRowChanged,stackedLayout,&QStackedLayout::setCurrentIndex);
return a.exec();
}
此程序中,我们在 QHBoxLayout 水平布局控件内又放置了一个 QStackedLayout 分组布局控件。感兴趣的读者可以编写程序,测试其它布局控件之间嵌套的效果。
默认情况下,每个 Qt 项目都包含一个后缀名为.pro
、名称和项目名相同的文件,我们通常称它为项目管理文件或者工程管理文件(简称 pro 文件)。
例如,新建一个 Qt 项目,如下图所示:
该项目的项目名为 Demo,项目中共包含 4 个文件,其中 Demo.pro 就是项目管理文件。
任何一个 Qt 项目都至少包含一个 pro 文件,此文件负责存储与当前项目有关的配置信息,比如:
所谓模块,可以简单地理解为文件夹或者压缩包,内部包含多个功能相近的类。作为一款成熟的 GUI 框架,Qt 提供了大量的类,根据这些类的功能,Qt 将它们分成了几个组,每个组称为一个模块。打开 Qt Creator 的帮助界面并搜索“All modules”,可以看到 Qt 的所有模块。
一个项目中可能包含上百个源文件,Qt 编译这些源文件的方法是:先由 qmake 工具根据 pro 文件记录的配置信息生成相应的 makefile 文件,然后执行 make 命令完成对整个项目的编译。也就是说,pro 文件存储的配置信息是用来告知编译器如何编译当前项目的,所以一个 Qt 项目要想完美运行,既要保证各个源文件中程序的正确性,还要保证 pro 文件中配置信息的合理性。
对于一个刚刚创建好的 Qt 项目,pro 文件并不是空的,而是包含一些基本的配置信息。实际开发中,Qt 会自动修改 pro 文件的内容,但有时也需要我们手动修改,例如程序中用到某个第三方库时,就需要我们手动修改 pro 文件。
接下来,我们就为大家详细地讲解 pro 文件中各个配置信息的含义,以及如何手动修改 pro 文件。
在图 1 所示的 Demo 项目中,双击打开 Demo.pro 项目管理文件,会看到如下内容:
#-------------------------------------------------
#
# Project created by QtCreator 2021-08-31T16:05:04
#
#-------------------------------------------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = Demo
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES +=
main.cpp
mainwindow.cpp
HEADERS +=
mainwindow.h
以上是 Demo.pro 配置文件中默认包含的内容。其中,#
号是注释符号,除了以#
号开头的注释内容外,其它内容都是当前项目的配置信息,比如QT += core gui
、TARGET = Demo
等。
pro 文件可以存储上百条配置信息,每条配置信息由三部分构成:
例如在 QT += core gui
中,Qt
是配置项,core
和gui
是该配置项的值,中间用+=
符号连接。下表给大家罗列了一些常用的配置项以及它们各自的含义。
配置项 | 含 义 |
---|---|
QT | 指定项目中用到的所有模块,默认值为 core 和 gui,中间用 += 符号连接。 |
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets | 如果 QT 版本大于 4(Qt5 或更高版本),则需要添加 widgets 模块,该模块包含所有控件类。 |
TARGET | 指定程序成功运行后生成的可执行文件的名称,中间用 = 符号连接。 |
TEMPLATE | 指定如何运行当前程序,默认值为 app,表示当前程序是一个应用程序,可以直接编译、运行。常用的值还有 lib,表示将当前程序编译成库文件。 |
DEFINES | 在程序中新定义一个指定的宏,比如 DEFINES += xxx,如同在程序中添加了 #define xxx 语句。 |
SOURCES | 指定项目中包含的所有 .cpp 源文件。 |
HEADERS | 指定项目中包含的所有 .h 头文件。 |
FORMS | 指定项目中包含的 ui 文件。 |
INCLUDEPATH | 指定头文件的存储路径,例如:INCLUDEPATH += /opt/ros/include |
CONFIG | 经常对应的值有:release:以 release 模式编译程序;debug:以 debug 模式编译程序;warn_on:编译器输出尽可能多的警告;c++11:启动 C++11 标准支持。例如 CONFIG += c++11。 |
根据上表中对各个配置项的讲解,您可以很轻松地搞清楚 Demo.pro 文件中各个配置项的含义,这里不再过多赘述。
上表中,大部分配置项不需要我们手动修改,比如 SOURCES、HEADERS、FORMS 等,当我们添加或者删除项目中的源文件时,Qt 会自动修改这些配置项。有些配置项需要手动修改,比如 QT 配置项,接下来重点给大家讲解 QT 的用法。
前面提到,Qt 根据各个类的功能将它们分到不同的模块,因此程序中要想使用某个类,必须完成两项准备工作:
头文件中;QT
用来指明当前项目中用到的所有模块,它的默认值是 core 和 gui,分别表示引入 Core 模块和 GUI 模块:
每个新创建的 Qt GUI 项目中,都默认包含 Core 模块和 GUI 模块,如果项目中用不到它们,可以使用QT -=
删除。例如,删除项目中包含的 GUI 模块,只需在 pro 文件中添加一条配置信息:
QT -= gui
除了 Core 和 GUI 模块外,Qt 还有 SQL(包含操作数据库相关的类)、Widgets(包含构建界面的所有控件类)、Multimedia(包含提供音频、视频等功能的类)等模块,Qt 项目不会自动包含这些模块。例如,项目中用到 SQL 模块中的一些类时,需要在 pro 文件中添加如下配置信息:
QT += sql
那么,当程序中用到某个类时,如何知道它属于哪个模块呢?很简单,先将该类所在的头文件中引入到程序中,然后鼠标选中头文件并按Fn+F1
组合键,打开该头文件的使用手册后就可以看到它所属的模块。以程序中使用 QWidget 窗口类为例,先在程序中添加如下语句:
#include
紧接着,鼠标选中“QWidget”并按Fn+F1
组合键,打开下图所示的 QWdiget 类使用手册,可以看到该类所属的模块为 widgets。
实际开发中,如果仅使用 Qt 提供的信号函数和槽函数,会经常遇到信号函数的参数类型和个数无法满足实际需求、信号函数和槽函数的参数类型不匹配等问题。解决此类问题,最简单有效的方式就是:自定义场景需要的信号函数和槽函数。
信号函数指的是符合以下条件的函数:
举个简单的例子:
class MyWidget:public QWidget{
//Q_OBJECT 是一个宏,添加它才能正常使用 Qt 的信号和槽机制
Q_OBJECT
//修饰信号函数的关键字
signals:
//自定义的信号函数
void MySignal(QString message);
};
我们自定义了一个继承自 QWidget 的 MyWidget 类,QWidget 是 QObject 的子类,所以 MyWidget 间接继承自 QObject 类。MyWidget 类中自定义了名为 MySignal 的信号函数(可以简称 MySignal 信号),它用 signals 关键字修饰,没有返回值,也没有定义(实现),仅有 1 个参数。
对于 MySignal() 信号函数,程序中不会直接调用它,而是借助 connect() 连接某个槽函数,实现的语法格式是:
MyWidget myWidget;
QObject::connect(&myWidget,&MyWidget::MySignal,信号接收者,槽函数);
一旦确定了信号接收者和槽函数,当 MySignal 信号发出后,与之相连的槽函数就会执行。那么,程序中如何发出 MySignal 信号呢?
对于 Qt 提供给我们的信号函数,其底层已经设置好了信号发出的时机,例如按下鼠标时、点击 Enter 回车键时等等。对于自定义的信号,我们需要自己指定信号发出的时机,这就需要用到 emit 关键字。emit 中文意思为“发出、射出”,是 Qt 在 C++ 基础上扩展的一个关键字,专门用来发射信号。
以定义好的 MySignal 信号为例,修改 MyWidget 类为:
class MyWidget:public QWidget{
//Q_OBJECT 是一个宏,添加它才能正常使用 Qt 的信号和槽机制
Q_OBJECT
//自定义信号函数
signals:
void MySignal(QString mess);
public:
void emitSignal(){
emit MySignal(message);
}
private:
QString message;
};
我们为 MyWidget 类新增了一个 emitSignal() 方法和一个 message 属性,emitSignal() 方法中的emit MySignal(message);
语句就表示发射 MySignal 信号。当程序中执行 emitSingal() 函数时,就会发出 MySignal 信号,message 属性的值也会随信号一同发出,对应的槽函数可以接收到 message 的值。
对于每一个自定义的信号函数,程序中都应该提供发射该信号的方法(函数),而且这样的方法(函数)可以有多个。
Qt5 中,槽函数既可以是普通的全局函数、也可以是类的成员函数、静态成员函数、友元函数、虚函数,还可以用 lambda 表达式表示。
和信号函数不同,槽函数必须手动定义(实现)。槽函数可以在程序中直接调用,但主要用来响应某个信号。自定义一个槽函数时,需要注意以下几点:
举个例子,自定义响应 MySignal 信号的槽函数:
class MyWidget:public QWidget{
//Q_OBJECT 是一个宏,添加它才能正常使用 Qt 的信号和槽机制
Q_OBJECT
signals:
void MySignal(QString mess1,QString mess2);
public:
void emitSignal(){
emit MySignal(message1,message2);
}
//类的成员函数
void recSlot1(QString mess){
qDebug() << "执行 recSlot1() 成员函数,输出" << mess;
}
//指明定义的是槽函数
public slots:
void recSlot2(QString mess1,QString mess2){
qDebug() << "执行 recSlot2() 槽函数,输出"<< mess1 << " " << mess2;
}
public:
QString message1;
QString message2;
};
//全局函数
void recSlot3(){
qDebug() << "执行 recSlot3() 全局函数";
}
程序中,重点关注 recSlot1()、recSlot2()、recSlot3() 这 3 个函数:
public slots
。slots 和 emit 一样,是 Qt 扩展的一个关键字,专门用来修饰槽函数。也就是说,recSlot2() 是 MyWidget 类中的槽函数。slots 关键字可以和 public、protected、private 搭配使用,它们的区别是:
通常情况下,槽函数使用 public slots 修饰。
很多读者会问,既然 public 修饰的成员函数可以当做槽函数,为什么还要提供 slots 关键字呢?笔者认为,“兼容旧的 Qt 版本”是其中的一个原因。Qt4 中的槽函数只能是 slots 修饰的类成员函数,Qt5 中取消了这一限制,但考虑到要兼容旧的 Qt 版本,Qt5 保留了旧版本中 connect() 函数的语法格式,也保留了 slots 关键字。
调用 connect() 函数,将 MySignal() 信号函数分别连接 recSlot1()、recSlot2()、recSlot3() 三个槽函数,实现代码为:
//类的成员函数作为槽函数QObject::connect(&mywidget,&MyWidget::MySignal,&mywidget,&MyWidget::recSlot1);//信号函数和槽函数相连接QObject::connect(&mywidget,&MyWidget::MySignal,&mywidget,&MyWidget::recSlot2);//全局函数作为槽函数QObject::connect(&mywidget,&MyWidget::MySignal,&recSlot3);
//main.cpp
#include
#include
#include
class MyWidget:public QWidget{
//Q_OBJECT 是一个宏,添加它才能正常使用 Qt 的信号和槽机制
Q_OBJECT
//信号函数
signals:
void MySignal(QString mess1,QString mess2);
public:
//发射信号的函数
void emitSignal(){
emit MySignal(message1,message2);
}
//普通类成员函数
void recSlot1(QString mess){
qDebug() << "执行 recSlot1() 成员函数,输出" << mess;
}
//槽函数
public slots:
void recSlot2(QString mess1,QString mess2){
qDebug() << "执行 recSlot2() 槽函数,输出"<< mess1 << " " << mess2;
}
public:
QString message1;
QString message2;
};
//全局函数
void recSlot3(){
qDebug() << "执行 recSlot3() 全局函数";
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//创建主窗口
MyWidget mywidget;
mywidget.message1 = "C语言中文网";
mywidget.message2 = "http://c.biancheng.net";
//类的成员函数作为槽函数
QObject::connect(&mywidget,&MyWidget::MySignal,&mywidget,&MyWidget::recSlot1);
//信号函数和槽函数相连接
QObject::connect(&mywidget,&MyWidget::MySignal,&mywidget,&MyWidget::recSlot2);
//全局函数作为槽函数
QObject::connect(&mywidget,&MyWidget::MySignal,&recSlot3);
mywidget.show();
//发射 Signal 信号
mywidget.emitSignal();
return a.exec();
}
//MyWidget类的定义应该放到 .h 文件中,本例中将其写到 main.cpp 中,程序最后需要添加 #include "当前源文件名.moc" 语句,否则无法通过编译。
#include "main.moc"
执行程序,会弹出一个 myWidget 空白窗口,同时输出以下信息:
执行 recSlot1() 成员函数,输出 “C语言中文网”
执行 recSlot2() 槽函数,输出 “C语言中文网” “http://c.biancheng.net”
执行 recSlot3() 全局函数
很多应用程序都需要具备操作文件的能力,包括对文件内容进行读/写、创建和删除文件等,甚至某些应用程序的诞生纯粹是为了操作文件,比如 WPS Office、PDFedit 等。为此,Qt 框架提供了 QFile 类专门用来操作文件。
QFile 类支持对文件进行读取、写入、删除、重命名、拷贝等操作,它既可以操作文件文件,也可以操作二进制文件。
使用 QFile 类操作文件之前,程序中需引入
头文件。创建 QFile 类的对象,常用的构造函数有:
QFile::QFile()
QFile::QFile(const QString &name)
参数 name 用来指定要操作的目标文件,包含文件的存储路径和文件名,存储路径可以使用绝对路径(比如 “D:/Demo/test.txt”)或者相对路径(比如"./Demo/test.txt"),路径中的分隔符要用 “/” 表示。
通常情况下,我们会调用第二个构造函数,直接指明要操作的文件。对于第一个构造函数创建的 QFile 对象,需要再调用 setFileName() 方法指明要操作的文件。
与 C++ 读写文件的规则一样,使用 QFile 读写文件之前必须先打开文件,调用 open() 成员方法即可,常用的语法格式为:
bool QFile::open(OpenMode mode)
mode 参数用来指定文件的打开方式,下表罗列了此参数的可选值以及各自的含义:
打开方式 | 含 义 |
---|---|
QIODevice::ReadOnly | 只能对文件进行读操作 |
QIODevice::WriteOnly | 只能对文件进行写操作,如果目标文件不存在,会自行创建一个新文件。 |
QIODevice::ReadWrite | 等价于 ReadOnly | WriteOnly,能对文件进行读和写操作。 |
QIODevice::Append | 以追加模式打开文件,写入的数据会追加到文件的末尾(文件原有的内容保留)。 |
QIODevice::Truncate | 以重写模式打开,写入的数据会将原有数据全部清除。注意,此打开方式不能单独使用,通常会和 ReadOnly 或 WriteOnly 搭配。 |
QIODevice::Text | 读取文件时,会将行尾结束符(Unix 系统中是 “\n”,Windows 系统中是 “\r\n”)转换成‘\n’;将数据写入文件时,会将行尾结束符转换成本地格式,例如 Win32 平台上是‘\r\n’。 |
根据需要,可以为 mode 参数一次性指定多个值,值和值之间用|
分割。比如:
注意,传递给 mode 参数的多个值之间不能相互冲突,比如 Append 和 Truncate 不能同时使用。
如果文件成功打开,open() 函数返回 true,否则返回 false。
QFile 类提供了很多功能实用的方法,可以快速完成对文件的操作,下表列举了常用的一些:
普通成员方法 | 功 能 |
---|---|
qint64 QFile::size() const | 获取当前文件的大小。对于打开的文件,该方法返回文件中可以读取的字节数。 |
bool QIODevice::getChar(char *c) | 从文件中读取一个字符,并存储到 c 中。读取成功时,方法返回 true,否则返回 false。 |
bool QIODevice::putChar(char c) | 向文件中写入字符 c,成功时返回 true,否则返回 false。 |
QByteArray QIODevice::read(qint64 maxSize) | 从文件中一次性最多读取 maxSize 个字节,然后返回读取到的字节。 |
qint64 QIODevice::read(char *data, qint64 maxSize) | 从文件中一次性对多读取 maxSize 个字节,读取到的字节存储到 data 指针指定的内存控件中。该方法返回成功读取到的字节数。 |
QByteArray QIODevice::readAll() | 读取文件中所有的数据。 |
qint64 QIODevice::readLine(char *data, qint64 maxSize) | 每次从文件中读取一行数据或者读取最多 maxSize-1 个字节,存储到 data 中。该方法返回实际读取到的字节数。 |
qint64 QIODevice::write(const char *data, qint64 maxSize) | 向 data 数据一次性最多写入 maxSize 个字节,该方法返回实际写入的字节数。 |
qint64 QIODevice::write(const char *data) | 将 data 数据写入文件,该方法返回实际写入的字节数。 |
qint64 QIODevice::write(const QByteArray &byteArray) | 将 byteArray 数组中存储的字节写入文件,返回实际写入的字节数。 |
bool QFile::copy(const QString &newName) | 将当前文件的内容拷贝到名为 newName 的文件中,如果成功,方法返回 true,否则返回 false。 copy 方法在执行复制操作之前,会关闭源文件。 |
bool QFile::rename(const QString &newName) | 对当前文件进行重命名,新名称为 newName,成功返回 true,失败返回 false。 |
bool QFile::remove() | 删除当前文件,成功返回 true,失败返回 false。 |
【实例一】演示了 QFile 类读写文本文件的过程。
#include #include int main(int argc, char *argv[]){ //创建 QFile 对象,同时指定要操作的文件 QFile file("D:/demo.txt"); //对文件进行写操作 if(!file.open(QIODevice::WriteOnly|QIODevice::Text)){ qDebug()<<"文件打开失败"; } //向文件中写入两行字符串 file.write("C语言中文网\n"); file.write("http://c.biancheng.net"); //关闭文件 file.close(); //重新打开文件,对文件进行读操作 if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){ qDebug()<<"文件打开失败"; } //每次都去文件中的一行,然后输出读取到的字符串 char * str = new char[100]; qint64 readNum = file.readLine(str,100); //当读取出现错误(返回 -1)或者读取到的字符数为 0 时,结束读取 while((readNum !=0) && (readNum != -1)){ qDebug() << str; readNum = file.readLine(str,100); } file.close(); return 0;}#include
#include
int main(int argc, char *argv[])
{
//创建 QFile 对象,同时指定要操作的文件
QFile file("D:/demo.txt");
//对文件进行写操作
if(!file.open(QIODevice::WriteOnly|QIODevice::Text)){
qDebug()<<"文件打开失败";
}
//向文件中写入两行字符串
file.write("C语言中文网\n");
file.write("http://c.biancheng.net");
//关闭文件
file.close();
//重新打开文件,对文件进行读操作
if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
qDebug()<<"文件打开失败";
}
//每次都去文件中的一行,然后输出读取到的字符串
char * str = new char[100];
qint64 readNum = file.readLine(str,100);
//当读取出现错误(返回 -1)或者读取到的字符数为 0 时,结束读取
while((readNum !=0) && (readNum != -1)){
qDebug() << str;
readNum = file.readLine(str,100);
}
file.close();
return 0;
}
执行程序,“C语言中文网” 和 “http://c.biancheng.net” 先写入 D 盘的 demo.txt 文件,然后再从文件中将它们读取出来。
【实例二】演示 QFile 读写二进制文件的过程。
#include
#include
int main(int argc, char *argv[])
{
//指定要写入文件的数据
qint32 nums[5]={1,2,3,4,5};
//写入文件之前,要将数据以二进制方式存储到字节数组中
QByteArray byteArr;
byteArr.resize(sizeof(nums));
for(int i=0;i<5;i++){
//借助指针,将每个整数拷贝到字节数组中
memcpy(byteArr.data()+i*sizeof(qint32),&(nums[i]),sizeof(qint32));
}
//将 byteArr 字节数组存储到文件中
QFile file("D:/demo.dat");
file.open(QIODevice::WriteOnly);
file.write(byteArr);
file.close();
//再次打开文件,读取文件中存储的二进制数据
file.open(QIODevice::ReadOnly);
QByteArray resArr = file.readAll();
//输出读取到的二进制数据
qDebug()<<"resArr: "<<resArr;
//将二进制数据转化为整数
char* data = resArr.data();
while(*data){
qDebug() << *(qint32*)data;
data += sizeof(qint32);
}
return 0;
}
执行程序,demo.dat 文件中会存储 {1,2,3,4,5} 这 5 个整数的二进制形式,同时输出以下内容:
resArr: “\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00”
1
2
3
4
5
单独使用 QFile 类读写文件的过程既繁琐又复杂,Qt 提供了两个辅助类 QTextStream 和 QDataStream,前者用来读写文件文件,后者用来读写二进制文件,QFile 可以和它们搭配使用,从整体上提高读写文件的开发效率。
和单独使用 QFile 类读写文本文件相比,QTextStream 类提供了很多读写文件相关的方法,还可以设定写入到文件中的数据格式,比如对齐方式、写入数字是否带前缀等等。
使用 QTextStream 类之前,程序中要先引入
头文件。QTextStream 类提供了很多种构造函数,常用的是:
QTextStream(QIODevice *device)
QIODevice 是 QFile 的父类,因此在构造 QTextStream 类的对象时,需要传递一个 QFile 类的对象。
下表罗列了 QTextStream 类常用的一些方法:
成员方法 | 功 能 |
---|---|
bool QTextStream::atEnd() const | 判断是否读到文件末尾,如果已经达到末尾,返回 true,否则返回 false。 |
QString QTextStream::read(qint64 maxlen) | 从文件中读最多 maxlen 个字符,返回这些字符组成的 QString 字符串。 |
QString QTextStream::readAll() | 从文件中读取所有内容,返回由读取内容组成的 QString 字符串。 |
QString QTextStream::readLine(qint64 maxlen = 0) | 默认读取一行文本,如果手动指定 maxlen 的值,则最多读取 maxlen 个字符,并返回读取内容组成的 QString 字符串。 |
void QTextStream::setFieldAlignment(FieldAlignment mode) | 设置对齐方式,通常与 setFieldWidth() 一起使用。 |
void QTextStream::setFieldWidth(int width) | 设置每份数据占用的位置宽度为 width。 |
QTextStream 类重载了>>
输入运算符和>>
输出运算符,使读写文本文件变得更简单。例如,用 QTextStream 实现【实例一】的程序如下:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
//创建 QFile 对象,同时指定要操作的文件
QFile file("D:/demo.txt");
//对文件进行写操作
if(!file.open(QIODevice::WriteOnly|QIODevice::Text)){
qDebug()<<"文件打开失败";
}
QTextStream out(&file);
//向文件中写入两行字符串
out << (QString)"C语言中文网\n" << (QString)"http://c.biancheng.net";
//关闭文件
file.close();
//重新打开文件,对文件进行读操作
if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
qDebug()<<"文件打开失败";
}
QTextStream in(&file);
//一直读,直至读取失败
while(!in.atEnd()){
QString str;
//从文件中读取一个字符串
in >> str;
qDebug() << str;
}
file.close();
return 0;
}
和
类似,QTextStream 类提供了两种格式化输出的方法,一种是调用该类的成员方法,例如表 3 中的 setFieldAlignment()、setFieldWidth 等,另一种是调用 QTextStream 类提供的格式描述符,下表罗列了常用的一些:
描述符 | 功能相同的方法 | 功 能 |
---|---|---|
Qt::hex | QTextStream::setIntegerBase(16) | 将指定整数对应的 16 进制数写入到文件中。 |
Qt::showbase | QTextStream::setNumberFlags(numberFlags() | ShowBase) | 对于非十进制数,写入到文件中时带上相应的前缀。二进制数前缀是 0b,八进制数前缀是 0,十六进制数前缀是 0x。 |
Qt::forcesign | QTextStream::setNumberFlags(numberFlags() | ForceSign) | 将数字写入文件时,带上正负号。 |
Qt::fixed | QTextStream::setRealNumberNotation(FixedNotation) | 将浮点数以普通小数的形式写入文件。 |
Qt::scientific | QTextStream::setRealNumberNotation(ScientificNotation) | 将浮点数以科学计数法的形式写入文件。 |
Qt::left | QTextStream::setFieldAlignment(AlignLeft) | 左对齐 |
Qt::right | QTextStream::setFieldAlignment(AlignRight) | 右对齐 |
Qt::center | QTextStream::setFieldAlignment(AlignCenter) | 居中对齐 |
举个简单的例子:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QFile file("D:/demo.txt");
if(!file.open(QIODevice::WriteOnly|QIODevice::Text)){
qDebug()<<"文件打开失败";
}
QTextStream out(&file);
//将 10 的十六进制数写入文件
out << hex << 10;
//设置每份数据占用 10 个字符的位置
out.setFieldWidth(10);
//以右对齐的方式写入 3.14
out << left << 3.14;
//后续数据以左对齐的方式写入文件
out.setFieldAlignment(QTextStream::AlignRight);
out << 2.7;
//关闭文件
file.close();
return 0;
}
程序运行后,demo.txt 存储的文本内容为:
a3.14 2.7
QDataStream 类的用法和 QTextStream 非常类似,最主要的区别在于,QDataStream 用于读写二进制文件。
使用 QDataStream 类之前,程序中要引入
头文件。创建 QDataStream 对象常用的构造函数为:
QDataStream::QDataStream(QIODevice *d)
下表罗列了 QDataStream 类常用的成员方法:
成员方法 | 功 能 |
---|---|
bool QDataStream::atEnd() const | 判断是否读到文件末尾,如果已经达到末尾,返回 true,否则返回 false。 |
QDataStream &QDataStream::readBytes(char *&s, uint &l) | 对于用 writeBytes() 方法写入文件的 l 和 s,只能使用 readBytes() 方法读取出来。 |
int QDataStream::readRawData(char *s, int len) | 从文件中读取最多 len 字节的数据到 s 中,返回值表示实际读取的字节数。注意,调用该方法之前,需要先给 s 参数分配好内存空间。 |
void QDataStream::setVersion(int v) | 不同版本的 Qt 中,同名称的数据类型也可能存在差异,通过调用此方法手动指定版本号,可以确保读取数据的一致性。 |
int QDataStream::skipRawData(int len) | 跳过文件中的 len 个字节,返回实际跳过的字节数。 |
QDataStream &QDataStream::writeBytes(const char *s, uint len) | 将长度 len 和 s 一起写入到文件中,对于 writeBytes() 写入的数据,只能用 readBytes() 方法读取。 |
int QDataStream::writeRawData(const char *s, int len) | 将 s 中前 len 字节的数据写入文件,返回值表示成功写入的字节数。 |
QDataStream 类也对<<
和>>
进行了重载,举个简单的例子,用 QDataStream 重新实现实例二:
#include
#include
#include
int main(int argc, char *argv[])
{
//指定要写入文件的数据
qint32 nums[5]={1,2,3,4,5};
QFile file("D:/demo.dat");
file.open(QIODevice::WriteOnly);
//创建 QDataStream 对象
QDataStream out(&file);
//将 nums 数组中的整数逐个写入到二进制文件中
for(int i=0;i<5;i++){
out << nums[i];
}
file.close();
//再次打开文件,读取文件中存储的二进制数据
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
//读取二进制文件中的数据
while(!in.atEnd()){
//每次读取一个整数
qint32 num;
in >> num;
qDebug() << num;
}
return 0;
}
输出结果为:
1
2
3
4
5
本节我们教大家用 Qt 实现一个带界面的学生信息管理系统,完成后的系统主界面如下图所示:
该学生信息管理系统将学生信息保存到文件中,用户借助界面上的表格、列表、按钮、输入框等控件,可以对学生信息进行查看、添加、删除、查找、更改、保存等操作。
整个学生信息管理系统,需要设计两个界面,一个是图 1 所示的主界面,另一个是添加学生信息的界面,如下图所示:
主界面的设计实现思路是:将 QHBoxLayout 作为主界面的布局工具,内部添加两个 QGroupBox 分组框,从而将整个界面一分为二:
由此,主窗口就设计完成了。
设计图 2 所示的添加学生信息界面非常简单,只需要自定义一个继承自 QDialog 的窗口类,用 QVBoxLayout 作为该窗口的布局工具,并依次将 QFormLayout 和 QHBoxLayout 添加到 QVBoxLayout 中:
由此,添加学生信息的窗口就设计完成了。
整个学生信息管理系统,由以下几个文件构成:
各个文件的作用分别是:
整个项目的实现过程,需要重点说明的有以下几点:
对于关联在一起的信号函数和槽函数,有些场景中,需要调用 disconnect() 暂时取消它们之间的连接,后续再重新关联它们。
例如在本项目中,MainWidget.cpp 文件 flushTable() 函数的功能是更新 QTableWidget 表格控件中显示的学生信息。更新学生信息之前,需要调用 disconnect() 函数切断 cellChanged() 信号函数与其它所有槽函数的关联,然后才能正常更新数据,更新完成后再恢复 cellChanged() 与其它槽函数的关联。
之所以更新数据前必须切断 cellChanged() 与其它槽函数的关联,是因为更新表格数据会不断地触发 cellChanged() 信号,最终会导致程序崩溃。
实现对学生信息的“增删查改”操作中,删除和修改学生信息的实现过程更复杂一些,本项目中采取的实现方法是:不断地从 student.txt 文件中读取学生信息,判断读取到的学生信息是否需要删除或修改,如果不需要,则直接写入 student_temp.txt 文件;反之如果需要删除,则直接将读取到的信息丢弃,如果需要修改,则将修改后的学生信息写入到 student_temp.txt 文件中。
最终,student_temp.txt 文件中存储的就是最新的学生信息,我们可以将 student.txt 文件中的内容删除,然后将 student_temp.txt 文件中的内容拷贝到 student.txt 文件中,最后删除 student_temp.txt 文件;也可以直接删除 student.txt 文件,然后将 student_temp.txt 文件的名称改为 student.txt,本项目中采用的是第二种方法。
图 4 中,image.qrc 文件是用来为项目添加图标的,本项目选用的是 C 语言中文网的 icon 图标,就存储在当前项目的文件夹内,如下图所示:
首先,我们要在项目中新建一个后缀名为 qrc 的文件,鼠标移动到项目名上右击选择“添加新文件”,Qt Creator 会弹出如下对话框:
选择 “Qt -> Qt Rescource File”,可以创建一个 qrc 文件。在此基础上,在 image.qrc 上右击选择“添加现有文件”,选中项目中的 logo.ico 图标,就被成功地将图标添加到项目中。
接下来,哪个界面需要添加图标,直接调用 setWindowIcon() 方法即可,例如:
setWindowIcon(QIcon(":/logo.ico"));
icon 图标的存储路径可以通过右击项目中的 logo.ico 图标,选择 "Copy Path “:logo.ico” 即可获得。
点击学生信息管理系统,即可下载整个项目的实现源码。下载完成后,依次完成以下操作即可运行项目:
源码中有非常详尽的注释,再结合本文对整个项目的介绍,很容易可以搞清楚每段代码的含义和作用,这里不再一一解释。
分享 Qt 程序(项目)的方式无非两种,要么直接分享程序的源代码,要么分享程序生成的可执行文件。
和直接分享源码相比,大多数人会选择后者。但遗憾地是,Qt Creator 默认以动态链接的方式生成可执行文件,该文件无法独立运行,必须为其提供所需的动态链接库。也就是说,只分享 Qt Creator 生成的可执行文件是不行的,必须将运行所需的动态链接库一起分享,可执行文件才能在他人的电脑上正常运行。
对 Qt 程序进行打包,指的就是找到可执行文件运行需要的所有动态库文件,并将它们统一存放到指定的空文件夹里。本节以编写完成的学生信息管理系统为例,给大家讲解“怎样在 Windows 平台上打包一个 Qt 程序”。
《Qt实现学生信息管理系统》一节讲了如何给程序中的窗口添加 icon 图标。打包 Qt 程序之前,我们也可以给可执行文件设置 icon 图标。
以学生信息管理系统为例,默认生成的可执行文件如图 1a) 所示,添加 icon 图标后的可执行文件如图 1b) 所示。
为可执行文件添加 icon 图标的方法很简单,将事先准备好的 icon 图标拷贝到程序对应的文件夹中,然后在 pro 工程文件内添加如下指令:
RC_ICONS += logo.ico
其中,logo.ico 是图标文件的名称。重新对程序进行编译、链接,最终生成的就是带 icon 图标的可执行文件。
打包 Qt 程序,通常选用以 release 模式生成的可执行文件。和 debug 模式相比,release 模式生成的可执行文件体积更小,运行效率更快。
Qt Creator 默认以 debug 模式生成可执行文件,如下图所示,可以手动修改 Qt Creator 以 release 模式生成可执行文件:
选择“Release”之后,再次运行程序,生成的可执行文件可以在下图所示的路径中找到:
找到可执行文件之后,将其拷贝到一个空的文件夹,比如笔者将其拷贝到了新建的 D:\StuInfoFile 文件夹中,如下图所示:
此时的 StuInfoFile.exe 是无法运行的,双击它系统会提示类似“找不到 xxx.dll”的错误信息。
在“开始”中找到 Qt 命令行程序并打开,如下图所示:
在命令行中,先执行 “cd D:\StuInfoFile” 命令进入 StuInfoFile 文件夹,然后再执行 “windeployqt StuInfoFile.exe” 命令。windeployqt 是 Qt 提供的 Windows 平台打包工具,它能找到 StuInfoFile.exe 可执行文件需要的所有动态链接库,并将它们拷贝到当前文件夹中。
成功执行命令之后,StuInfoFile 文件夹内会增加很多文件夹和文件(如下图所示),这些都是 StuinfoFile.exe 执行所需要的。
再次双击 StuInfoFile.exe,如果它可以成功执行,表明打包操作是成功的。我们可以直接将 StuInfoFile 文件夹分享给他人,只要是 Windows 平台,都可以直接点击运行 StuinfoFile.exe 文件。