Qt 之路
Qt 涉及的术语和名词
Project
Project 的中文翻译是“项目”或者“工程”,这里的项目是指为实现某个相对独立功能的程序代码合,这些代码不单单是放在一块,而是有相互之间的关联性,并且有专门负责管理该项目的项目文件,比如:
- Qt 使用 .pro 文件管理项目;
- VC++ 则使用 .vcproj 作为项目文件。
集成开发环境通常都是依据项目文件(.pro/.vcproj)管理和构建项目。
Makefile
即生成脚本,虽然可以直接调用编译器如 g++ 编译程序,但是如果项目里的代码文件变多了,哪些代码文件更新了需要重新编译,哪些代码没有改不需要重新编译等等,靠程序员自己记忆去处理是比较麻烦的事,还有哪些代码需要预处理或是链接哪些库文件, 这些都是繁杂的过程。为了规范程序的编译生成过程,产生了规范化的生成脚本,就是 Makefile,生成器 make 可以依据规范的 Makefile 自动生成目标程序或库文件。
定义好 Makefile ,让程序员只需要去关注如何编写代码,而生成程序过程中的脏活累活都交给 make 程序。
现在 Makefile 通常都有工具自动生成,如 qmake 工具, 这样就大量减轻了程序员的负担。
Debug 和 Release
Debug 即调试,Release即发行。代码编写之后,生成的目标程序或库文件通常不会绝对正确,或多或少有些毛病(bug),因此需要进行纠错调试(Debug)。调试过程中需要源代码和二进制目标程序之间一一对应的关系, 这样才能定位到错误代码,所以 Debug 版本的程序是臃肿而不进行优化的。
与之相对的是 Release 发行版,在纠正了发觉到的错误后,需要发布程序用于实际用途,实际应用时强调运行效率高,减少冗余代码,因此会对二进制程序进行大量优化,提升性能。这样发布的二进制目标程序就是 Release 版。
Debug 版本和 Release 版本使用的库文件不一样: Debug 版本程序通常链接的也是 Debug 版本的库文件,比如 libQt5Guid.a/Qt5Guid.dll,库文件的简短名(不含扩展名)都是以 d 结尾的,Debug 库通常都比较大 。 Release 版本程序链接的通常就是 Release 版本的库文件,Release 版本库文件名字比 Debug 版本库文件少一个字母 d ,如 libQt5Gui.a/Qt5Gui.dll,而且 Release 版本库一般都比 Debug 版本小很多,运行效率也高很多。
C++11 标准
时代在变化,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 即动态链接,Static Link 即静态链接。
动态链接库
目标程序通常都不是独立个体,生成程序时都需要链接其他的库,要用到其他库的代码。对于多个程序同时运行而言,内存中就可能有同一个库的多个副本,占用了太多内存而干的活差不多。
为了优化内存运用效率,引入了动态链接库(Dynamic Link Library),或叫共享库(Shared Object)。使用动态链接库时,内存中只需要一份该库文件,其他程序要使用该库文件时,只要链接过来就行了。由于动态库文件外置,链接到动态库的目标程序相对比较小,因为剥离了大量库代码,而只需要一些链接指针。
使用动态库,也意味着程序需要链接到如 *.dll
或*.so
文件,得提前装好动态库文件,然后目标程序才能正常运行。
静态链接库
静态库就是将链接库的代码和自己编写的代码都编译链接到一块,链接到静态库的程序通常比较大,但好处是运行时依赖的库文件很少,因为目标程序自己内部集成了很多库代码。
库文件后缀
Linux/Unix 系统里静态库扩展名一般是 .a,动态库扩展名一般是 .so 。Windows 系统里 VC 编译器用的静态库扩展名一般是 .lib,动态库扩展名一般是 .dll 。
QT程序First
QTCreate可以创建多种项目,各类应用程序如下:
- Qt Widgets Application,支持桌面平台的有图形用户界面(Graphic User Interface,GUI) 界面的应用程序。GUI 的设计完全基于 C++ 语言,采用 Qt 提供的一套 C++ 类库。
- Qt Console Application,控制台应用程序,无 GUI 界面,一般用于学习 C/C++ 语言,只需要简单的输入输出操作时可创建此类项目。
- Qt Quick Application,创建可部署的 Qt Quick 2 应用程序。Qt Quick 是 Qt 支持的一套 GUI 开发架构,其界面设计采用 QML 语言,程序架构采用 C++ 语言。利用 Qt Quick 可以设计非常炫的用户界面,一般用于移动设备或嵌入式设备上无边框的应用程序的设计。
- Qt Quick Controls 2 Application,创建基于 Qt Quick Controls 2 组件的可部署的 Qt Quick 2 应用程序。Qt Quick Controls 2 组件只有 Qt 5.7 及以后版本才有。
- Qt Canvas 3D Application,创建 Qt Canvas 3D QML 项目,也是基于 QML 语言的界面设计,支持 3D 画布。
项目的文件组成和管理
项目名称节点下,分组管理着项目的各种源文件,几个文件及分组分别为以下几项:
- Demo.pro 是项目管理文件,包括一些对项目的设置项。
- Headers 分组,该节点下是项目内的所有头文件(.h)中所示项目有一个头文件mainwindow.h,是主窗口类的头文件。
- Sources 分组:该节点下是项目内的所有 C++源文件(.cpp),图 5 中所示项目有两个 C++ 源文件,mainwindow.cpp 是主窗口类的实现文件,与 mainwindow.h 文件对应。main.cpp 是主函数文件,也是应用程序的入口。
- Forms 分组:该节点下是项目内的所有界面文件(.ui)。图 5 中所示项目有一个界面文件mainwindow.ui,是主窗口的界面文件。界面文件是文本文件,使用 XML 语言描述界面的组成。
#include <QApplication>
#include <QWidget>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
w.show();
return a.exec();
}
- Qt头文件没有.h后缀
- Qt一个类对应一个头文件,类名就是头文件名
- QApplication应用过程序类: 管理图形用户界面应用程序的控制流和主要设置。 是Qt的整个后台管理的命脉它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理。 对于任何一个使用Qt的图形用户界面应用程序,都正好存在一个QApplication 对象,而不论这个应用程序在同一时间内是不是有0、1、2或更多个窗口。
- a.exec() 程序进入消息循环,等待对用户输入进行响应。这里main()把控制权转交给Qt,Qt完成事件处理工作,当应用程序退出的时候exec()的值就会返回。在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
信号和槽机制(listener)
信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked,&app, &QApplication::quit);
button.show();
return app.exec();
}
connect()函数最常用的一般形式:connect(sender, signal, receiver, slot); 参数:
- sender 发出信号的对象
- signal 发送对象发出的信号
- receiver 接收信号的对象
- slot 接收对象在接收到信号之后所需要调用的函数
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
自定义信号槽
======= newspaper.h ========
class Newspaper : public QObject
{
Q_OBJECT
public:
Newspaper(const QString & name) :
m_name(name)
{
}
void send()
{
emit newPaper(m_name);
}
signals:
void newPaper(const QString &name);
private:
QString m_name;
};
========reader.h ==============
#include <QObject>
#include <QDebug>
class Reader : public QObject
{
Q_OBJECT
public:
Reader() {}
void receiveNewspaper(const QString & name)
{
qDebug() << "Receives Newspaper: " << name;
}
};
============main.cpp=================
#include <QCoreApplication>
#include "newspaper.h"
#include "reader.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Newspaper newspaper("Newspaper A");
Reader reader;
QObject::connect(&newspaper, &Newspaper::newPaper, &reader, &Reader::receiveNewspaper);
newspaper.send();
return app.exec();
}
-
首先看Newspaper这个类。这个类继承了QObject类。**只有继承了QObject类的类,才具有信号槽的能力。**所以,为了使用信号槽,必须继承QObject。凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。
-
Newspaper类的 public 和 private 代码块都比较简单,只不过它新加了一个 signals。signals 块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。信号作为函数名,不需要在 cpp 函数中添加任何实现。
-
Newspaper类的send()函数比较简单,只有一个语句emit newPaper(m_name);。emit 是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。emit 的含义是发出,也就是发出newPaper()信号。感兴趣的接收者会关注这个信号,可能还需要知道是哪份报纸发出的信号?所以,我们将实际的报纸名字m_name当做参数传给这个信号。当接收者连接这个信号时,就可以通过槽函数获得实际值。这样就完成了数据从发出者到接收者的一个转移。
-
Reader类更简单。因为这个类需要接受信号,所以我们将其继承了QObject,并且添加了Q_OBJECT宏。后面则是默认构造函数和一个普通的成员函数。**Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。**与信号函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,也会受到 public、private 等访问控制符的影响。(如果信号是 private 的,这个信号就不能在类的外面连接,也就没有任何意义。)
自定义信号槽需要注意的事项:
- 发送者和接收中和都需要是的子类
- 使用signals标记信号函数,信号是一个函数声明,返回void,不需要事先函数代码
- 槽函数是普通的成员函数,作为成员函数,会受到public private protected的影响
- 使用emit在恰当的位置发送信号
- 使用QObject::connect()函数连接信号和槽
- 任何成员函数/static函数/全局函数/Lambda表达式都可以作为槽函数
信号槽的更多用法
- 一个信号可以和多个槽相连
- 多个信号连接一个个槽
- 一个信号可以连接多个信号
- 使用Lambda表达式
QObject::connect(&newspaper, static_cast<void (Newspaper:: *)
(const QString &)>(&Newspaper::newPaper),=](const QString &name)
{ /* Your code here. */ });
在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。
Lambda表达式
c++11 中国的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作.
Lambda表达式的基本构成:
1.函数对象参数
[]标识一个Lambda的开始 这部分必须存在 不能省略 函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:
- 空,没有使用任何函数对象参数
- =,函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- &,函数体内可以使用Lambda所在作用范围内搜友可见的局部变量,并且是引用传递方式
- this,函数体内可以使用Lambda所在类中的成员变量
- a,将A按值进行传递,按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的,要修改传递进来的a的拷贝,可以添加mutable修饰符
- &a,将a按值进行传递,b按引用进行传递
- a,&b,将a按值进行传递,b按引用进行传递
- =,&a,&b,除了a和b按引用进行传递外,其他参数都按值进行传递
- &,a,b 除a和b按值进行传递外,其他参数都按引用进行传递
int m=0,n=0;
[=](int a) nutable {m=++n + a;}(4);
[&] (int a) { m = ++n + a; }(4);
[=,&m] (int a) mutable { m = ++n + a; }(4);
[&,m] (int a) mutable { m = ++n + a; }(4);
[m,n] (int a) mutable { m = ++n + a; }(4);
[&m,&n] (int a) { m = ++n + a; }(4);
2.操作符重载函数参数
标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
3.可修改标识符
mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
4.错误抛出标识符
exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)
5.函数返回值
返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
6.是函数体
{},标识函数的实现,这部分不能省略,但函数体可以为空。
Qt窗口系统
Qt窗口坐标体系
坐标体系 以左上角为原点 X向右增加 Y向下增加
对于嵌套窗口,其坐标是相对于父窗口来说的.
QWidget
所有窗口及窗口控件都是从QWidget直接或间接派生出来的.
对象模型
在Qt中创建对象的时候回提供一个Parent对象指针,parent指针的作用:
- QObject是以对象树的形式组织起来的
当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。
**当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)**这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。
- QWidget是能够在屏幕上显示的一切组件的父类。
QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
当然,**我们也可以自己删除子对象,它们会自动从其父对象列表中删除。**比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
Qt引入对象数的概念,在一定程度上解决了内存问题
- 当一个QObject对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
2 任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。
如果QObject在栈上创建,Qt保持同样的行为.
QMainWindow
一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)及一个中心部件(central widget),是许多应用程序的基础,如文本编辑器,图片编辑器等。
菜单栏
一个主窗口最多只有一个菜单栏。位于主窗口顶部、主窗口标题栏下面。
- 创建菜单栏,通过QMainWindow类的menubar()函数获取主窗口菜单栏指针
QMenuBar * menuBar() const
- 创建菜单,调用QMenu的成员函数addMenu来添加菜单
`QAction* addMenu(QMenu * menu)``
`QMenu* addMenu(const QString & title)``
QMenu* addMenu(const QIcon & icon, const QString & title)
`
- 创建菜单项,调用QMenu的成员函数addAction来添加菜单项
QAction* addAction(const QString & text)
QAction* addAction(const QIcon & icon, const QString & text)
QAction* addAction(const QString & text, const QObject * receiver,
const char * member, const QKeySequence & shortcut = 0)
QAction* addAction(const QIcon & icon, const QString & text,
const QObject * receiver, const char * member,
const QKeySequence & shortcut = 0)
Qt 并没有专门的菜单项类,只是使用一个QAction类,抽象出公共的动作。当我们把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。
工具栏
主窗口的工具栏上可以有多个工具条,通常采用一个菜单对应一个工具条的的方式,也可根据需要进行工具条的划分。
直接调用QMainWindow类的addToolBar()函数获取主窗口的工具条对象,每增加一个工具条都需要调用一次该函数。
插入属于工具条的动作,即在工具条上添加操作。通过QToolBar类的addAction函数添加。
工具条是一个可移动的窗口,它的停靠区域由QToolBar的allowAreas决定,包括:
Qt::LeftToolBarArea 停靠在左侧
Qt::RightToolBarArea 停靠在右侧
Qt::TopToolBarArea 停靠在顶部
Qt::BottomToolBarArea 停靠在底部
Qt::AllToolBarAreas 以上四个位置都可停靠
使用setAllowedAreas()函数指定停靠区域:
setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea)
使用setMoveable()函数设定工具栏的可移动性:
setMoveable(false)//工具条不可移动, 只能停靠在初始化的位置上
状态栏
派生自QWidget类,使用方法与QWidget类似,QStatusBar类常用成员函数:
//添加小部件
void addWidget(QWidget * widget, int stretch = 0)
//插入小部件
int insertWidget(int index, QWidget * widget, int stretch = 0)
//删除小部件
void removeWidget(QWidget * widget)
对话框QDialog
基本概念
对话框是GUI程序中不可缺少的组成部分,很多不能或者不适合放入主窗口的功能组件都必须放在对话框中设置。对话框通常会是一个顶层窗口,出现在程序最上层,用于实现短期任务或者简洁的用户交互。
Qt 中使用QDialog类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialog。QDialog(及其子类,以及所有Qt::Dialog类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。 对话框分为模态对话框和非模态对话框。
-
模态对话框,就是会阻塞同一应用程序中其它窗口的输入。模态对话框很常见,比如“打开文件”功能。你可以尝试一下记事本的打开文件,当打开文件对话框出现时,我们是不能对除此对话框之外的窗口部分进行操作的。
-
与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。
标准对话框
所谓标准对话框,是 Qt 内置的一系列对话框,用于简化开发。事实上,有很多对话框都是通用的,比如打开文件、设置颜色、打印设置等。这些对话框在所有程序中几乎相同,因此没有必要在每一个程序中都自己实现这么一个对话框。
Qt 的内置对话框大致分为以下几类:
- QColorDialog: 选择颜色;
- QFileDialog: 选择文件或者目录;
- QFontDialog: 选择字体;
- QInputDialog: 允许用户输入一个值,并将其值返回;
- QMessageBox: 模态对话框,用于显示信息、询问问题等;
- QPageSetupDialog: 为打印机提供纸张相关的选项;
- QPrintDialog: 打印机配置;
- QPrintPreviewDialog:打印预览;
- QProgressDialog: 显示操作过程。
自定义消息框
Qt 支持模态对话框和非模态对话框。 模态与非模态的实现:
- 使用QDialog::exec()实现应用程序级别的模态对话框
- 使用QDialog::open()实现窗口级别的模态对话框
- 使用QDialog::show()实现非模态对话框。
模态对话框
Qt有两种级别的模态对话框:
- 应用程序级别的模态 当该种模态的对话框出现时,用户必须先对对话框进行交互,直到关闭对话框后,才能访问其他窗口
- 窗口级别的模态 该模态仅仅阻塞与对话框关联的窗口,但依然允许用户与程序中其他窗口交互.窗口级别的模态尤其适用于多窗口模式.
非模态对话框
void MainWindow::open()
{
QDialog *dialog = new QDialog;
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(tr("Hello, dialog!"));
dialog->show();
}
利用setAttribute()
函数设置对话框关闭时,自动销毁对话框.
消息对话框
QMessageBox用于系那是消息提示,一般会使用其提供的几个static函数:
-
显示关于对话框。
void about(QWidget * parent, const QString & title, const QString & text)
标题title,内容是text,父窗口是parent.对话框只有一个OK按钮. -
显示关于Qt对话框,该对话框用于显示有关QT的信息
void aboutQt(QWidget * parent, const QString & title = QString()):
-
显示严重错误对话框
StandardButton critical(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
这个对话框将显示一个红色的错误符号。我们可以通过 buttons 参数指明其显示的按钮。默认情况下只有一个 Ok 按钮,我们可以使用StandardButtons类型指定多种按钮。 -
与QMessageBox::critical()类似,不同之处在于这个对话框提供一个普通信息图标。
StandardButton information(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
-
与QMessageBox::critical()类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”。
StandardButton question(QWidget * parent,const QString & title, const QString & text, StandardButtons buttons = StandardButtons( Yes | No ), StandardButton defaultButton = NoButton)
-
与QMessageBox::critical()类似,不同之处在于这个对话框提供一个黄色叹号图标。
StandardButton warning(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
使用QMessageBox:
if (QMessageBox::Yes == QMessageBox::question(this,tr("Question"), tr("Are you OK?"),
QMessageBox::Yes | QMessageBox::No,QMessageBox::Yes))
{
QMessageBox::information(this, tr("Hmmm..."), tr("I'm glad to hear that!"));
}
else
{
QMessageBox::information(this, tr("Hmmm..."), tr("I'm sorry!"));
}
我们使用QMessageBox::question()来询问一个问题。
这个对话框的父窗口是this。QMessageBox是QDialog的子类,这意味着它的初始显示位置将会是在 parent 窗口的中央。
第二个参数是对话框的标题。
第三个参数是我们想要显示的内容。这里就是我们需要询问的文字。下面,我们使用或运算符(|)指定对话框应该出现的按钮。这里我们希望是一个 Yes 和一个 No。
最后一个参数指定默认选择的按钮。
这个函数有一个返回值,用于确定用户点击的是哪一个按钮。按照我们的写法,应该很容易的看出,这是一个模态对话框,因此我们可以直接获取其返回值。
QMessageBox类的 static 函数优点是方便使用,缺点也很明显:非常不灵活。我们只能使用简单的几种形式。为了能够定制QMessageBox细节,我们必须使用QMessageBox的属性设置 API。如果我们希望制作一个询问是否保存的对话框,我们可以使用如下的代码:
QMessageBox msgBox;
msgBox.setText(tr("The document has been modified."));
msgBox.setInformativeText(tr("Do you want to save your changes?"));
msgBox.setDetailedText(tr("Differences here..."));
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard| QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Save);
int ret = msgBox.exec();
switch (ret)
{
case QMessageBox::Save:
qDebug() << "Save document!";
break;
case QMessageBox::Discard:
qDebug() << "Discard changes!";
break;
case QMessageBox::Cancel:
qDebug() << "Close document!";
break;
}
msgBox 是一个建立在栈上的QMessageBox实例。我们设置其主要文本信息为“The document has been modified.”,informativeText 则是会在对话框中显示的简单说明文字。下面我们使用了一个detailedText,也就是详细信息,当我们点击了详细信息按钮时,对话框可以自动显示更多信息。我们自己定义的对话框的按钮有三个:保存、丢弃和取消。然后我们使用了exec()是其成为一个模态对话框,根据其返回值进行相应的操作。
标准文件对话框
QFileDialog,也就是文件对话框。
在openFile()函数中,我们使用QFileDialog::getOpenFileName()来获取需要打开的文件的路径。这个函数原型如下:
QString getOpenFileName(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), const QString & filter = QString(), QString * selectedFilter = 0,Options options = 0)
不过注意,它的所有参数都是可选的,因此在一定程度上说,这个函数也是简单的。这六个参数分别是:
-
parent:父窗口。Qt 的标准对话框提供静态函数,用于返回一个模态对话框;
-
caption:对话框标题;
-
dir:对话框打开时的默认目录
-
filter 过滤器 打开特定类型的文件时使用
常用控件
QLabel控件使用
QLabel是我们最常用的控件之一,用来显示文本,图片和动画等
显示文字(普通文本/html)
通过QLabel类的setText函数设置显示的内容:
void setText(const QString &)
可以显示普通文本字符串
QLabel *label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">百度一下</a></h1>");
label ->setOpenExternalLinks(true);
其中setOpenExternalLinks()函数是用来设置用户点击链接之后是否自动打开链接,如果参数指定为true则会自动打开,如果设置为false,想要打开链接只能通过捕捉linkActivated()信号,在自定义的槽函数中使用**QDesktopServices::openUrl()**打开链接,该函数参数默认值为false
QLabel *label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">百度一下</a></h1>");
// label->setOpenExternalLinks(true);
connect(label, &QLabel::linkActivated, this, &MyWidget::slotOpenUrl);
//槽函数
void MyWidget::slotOpenUrl(const QString &link)
{
QDesktopServices::openUrl(QUrl(link));
}
显示图片
可以使用QLabel的成员函数setPixel设置图片
void setPixmap(const QPixmap &)
首先定义了QMovied对象,然后加载图片,最后将图片设置到QLabel中去:
QPixmap pixmap;
pixmap.load(":/boat.jpg");
QLabel *label = new QLabel;
label.setPixmap(pixmap);
显示动画 通过使用QLabel的成员函数setMovie加载动画,可以播放gif格式的文件 定义QMovied对象,并初始化,播放加载的动画,将动画设置到QLabel中:
QMovie *movie = new QMovie("Nario.gif"):
movie->start();
QLabel *label = new QLabel;
label->setMovie(movie);
QLineEdit
Qt提供的单行文本编辑框 设置/获取内容
- 获取编辑框内容使用text(),函数声明如下:
QString text() const
- 设置编辑框内容
void setText(const QString &
设置显示模式 使用QLineEdit类的setEchoMode()函数设置文本的显示模式,函数声明:void setEchoMode(EchoMode mode)
EchoMode是一个枚举类型,一共定义了四种显示模式:
- QLineEdit::Normal 模式显示方式,按照输入的内容显示。
- QLineEdit::NoEcho 不显示任何内容,此模式下无法看到用户的输入。
- QLineEdit::Password 密码模式,输入的字符会根据平台转换为特殊字符。
- QLineEdit::PasswordEchoOnEdit 编辑时显示字符否则显示字符作为密码。
另外,我们再使用QLineEdit显示文本的时候,希望在左侧留出一段空白的区域,那么就可以使用QLineEdit给我们提供的setTextMargins函数:
void setTextMargins(int left, int top, int right, int bottom)
用此函数可以指定显示的文本与输入框上下左右边界的间隔的像素数。
设置输入提示
如果我们想实现一个与百度的搜索框类似的功能:输入一个或几个字符,下边会列出几个跟输入的字符相匹配的字符串,QLineEdit要实现这样的功能可以使用该类的成员函数setComleter()函数来实现:
void setCompleter(QCompketer * c)
创建QCompleter对象,并初始化
QStringList tipList;
tipList<< “Hello” << “how are you” << “Haha” << “oh, hello”;
// 不区分大小写
completer->setCaseSensitivity(Qt::CaseInsensitive);
QCompleter *completer = new QCompleter(tipList, this);
QCompeleter类的setCaseSensitvity()函数介意设置是否区分大小写,它的参数是一个枚举类型:
- Qt::CaseInsensitive 不区分大小写
- Qt::CaseSensitive 区分大小写
如果不设置该属性,默认匹配字符串时时区分大小写的 还可以设置字符串其中某一部分匹配,此功能可通过QCompleter类的setFilterMode函数来实现,函数声明如下:
void setFilterMode(Qt::MatchFlags filterMode)
其参数为Qt定义的宏,有多重类型,具体可参考Qt帮助稳定,要实现我们上边提到的功能,参数可以使用 Qt::MatchContains:
completer->setFilterMode(Qt::MatchContains);
属性设置完成后,将QCompleter对象设置到QLineEdit中:
QLineEdit *edit = new QLineEdit(this);
edit->setCompleter(completer);
布局管理器
Qt 提供了两种组件定位机制:绝对定位和布局定位。
绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。 这样,Qt 就知道该把组件放在哪里以及如何设置组件的大小。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。这也很自然,因为你并没有告诉 Qt,在窗口变化时,组件是否要更新自己以及如何更新。或者,还有更简单的方法:禁止用户改变窗口大小。但这总不是长远之计。
布局定位:你只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt 使用对应的布局管理器进行调整。 布局定位完美的解决了使用绝对定位的缺陷。
Qt 提供的布局中以下三种是我们最常用的:
- QHBoxLayout:按照水平方向从左到右布局;
- QVBoxLayout:按照竖直方向从上到下布局;
- QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;
水平/垂直/网格布局
水平布局的使用方法:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("Enter your age");
QSpinBox *spinBox = new QSpinBox(&window);
QSlider *slider = new QSlider(Qt::Horizontal, &window);
spinBox->setRange(0, 130);
slider->setRange(0, 130);
QObject::connect(slider, &QSlider::valueChanged, spinBox, &QSpinBox::setValue);
void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged;
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
spinBox->setValue(35);
//给控件设置布局
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(spinBox);
layout->addWidget(slider);
window.setLayout(layout);
window.show();
return app.exec();
}
上面的代码中window.setLayout(layout); 是将布局设置到窗口window中,在窗口中设置布局还有另一种写法:
//给控件设置布局
QHBoxLayout *layout = new QHBoxLayout(window);
layout->addWidget(spinBox);
layout->addWidget(slider);
在创建布局对象的时候给新对象指定父窗口,就等于给传入的窗口设置了布局。
另外布局与布局之间是可以嵌套使用的,使用addLayout()方法。QVBoxLayout的使用方法与QHBoxLayout完全相同。
关于上述代码中信号和槽链接的解释:
当数字输入框显示的内容发生改变的时候,会发出一股信息,滑块会接收这一信号,并作出改变。如果二者的信号槽连接写成下边这样:
QObject::connect(spinBox, &QSpinBox::valueChanged, slider, &QSlider::setValue);
编译器却会报错
no matching function for call to 'QObject::connect(QSpinBox*&,
这是怎么回事呢?从出错信息可以看出,编译器认为QSpinBox::valueChanged是一个 overloaded 的函数。我们看一下QSpinBox的文档发现,QSpinBox的确有两个信号:
void valueChanged(int) void valueChanged(const QString &) 当我们使用&QSpinBox::valueChanged取函数指针时,编译器不知道应该取哪一个函数(记住前面我们介绍过的,signal 也是一个普通的函数。)的地址,因此报错。解决的方法很简单,编译器不是不能确定哪一个函数吗?那么我们就显式指定一个函数。方法就是,我们创建一个函数指针,这个函数指针参数指定为 int:
void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged;
然后我们将这个函数指针作为 signal,与 QSlider 的函数连接:
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
这样便避免了编译错误。
自定义控件
从QWidget派生出一个类SamllWidget,实现自定义窗口
// smallwidget.h
class SmallWidget : public QWidget
{
Q_OBJECT
public:
explicit SmallWidget(QWidget *parent = 0);
signals:
public slots:
private:
QSpinBox* spin;
QSlider* slider;
};
// smallwidget.cpp
SmallWidget::SmallWidget(QWidget *parent) : QWidget(parent)
{
spin = new QSpinBox(this);
slider = new QSlider(Qt::Horizontal, this);
// 创建布局对象
QHBoxLayout* layout = new QHBoxLayout;
// 将控件添加到布局中
layout->addWidget(spin);
layout->addWidget(slider);
// 将布局设置到窗口中
setLayout(layout);
// 添加消息响应
connect(spin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),slider, &QSlider::setValue);
connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
}
Qt消息机制和事件
事件
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler).
在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如
- keyPressEvent()
- keyReleaseEvent()
- mouseDoubleClickEvent()
- mouseMoveEvent()
- mousePressEvent()
- mouseReleaseEvent() 等。
这些函数都是protected virtual
的,也就是说,可以在子类中重新实现这些函数.
class EventLabel : public QLabel
{
protected:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
};
void EventLabel::mouseMoveEvent(QMouseEvent *event)
{
this->setText(QString("<center><h1>Move: (%1, %2)</h1></center>").arg(QString::number(event->x()), QString::number(event->y())));
}
void EventLabel::mousePressEvent(QMouseEvent *event)
{
this->setText(QString("<center><h1>Press:(%1, %2)</h1></center>").arg(QString::number(event->x()),
QString::number(event->y())));
}
void EventLabel::mouseReleaseEvent(QMouseEvent *event)
{
QString msg;
msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>", event->x(), event->y());
this->setText(msg);
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
EventLabel *label = new EventLabel;
label->setWindowTitle("MouseEvent Demo");
label->resize(300, 200);
label->show();
return a.exec();
}
-
EventLabel继承了QLabel,覆盖了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持 HTML 代码的,因此我们直接使用了 HTML 代码来格式化文字。
-
QString的arg()函数可以自动替换掉QString中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种。QString("[%1, %2]").arg(x).arg(y);语句将会使用x替换 %1,y替换 %2,因此,生成的QString为[x, y]。
-
在mouseReleaseEvent()函数中,我们使用了另外一种QString的构造方法。我们使用类似 C 风格的格式化函数sprintf()来构造QString。
event
事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。 event()函数主要用于事件的分发。
bool CustomWidget::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << "You press tab.";
return true;
}
}
return QWidget::event(e);
}
CustomWidget是一个普通的QWidget子类.我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
-
如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
-
在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
bool CustomTextEdit::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab)
{
qDebug() << "You press tab.";
return true;
}
}
return false;
}
CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。
//!!! Qt5
bool QObject::event(QEvent *e)
{
switch (e->type()) {
timerEvent((QTimerEvent*)e);
break;
case QEvent::ChildAdded:
{
customEvent(e);
break;
}
}
}
switch (event->type()) {
case QEvent::MouseMove:
mouseMoveEvent((QMouseEvent*)event);
break;
// ...
}
event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。
事件过滤器
对象需要查看、甚至要拦截发送到另外对象的时间.
Qt创建了QEvent事件对象后,会调用QObject的event()函数处理事件的分发.可以在event()函数中实现拦截的操作,由于event()函数是protected的,需要继承已有类.但是组件过多,会使操作极为繁琐.
Qt提供了另外一种机制来达到这一目的:事件过滤器
QObject有一个eventFilter()函数,用于建立事件过滤器.
virtual bool QObject ::eventFilter QObject * watched,QEvent * event);
如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果想将参数 event 过滤出来,就返回 true,否则返回 false.
class MainWindow : public QMainWindow
{
public:
MainWindow();
protected:
bool eventFilter(QObject *obj, QEvent *event);
private:
QTextEdit *textEdit;
};
MainWindow::MainWindow()
{
textEdit = new QTextEdit;
setCentralWidget(textEdit);
textEdit->installEventFilter(this);
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == textEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
qDebug() << "Ate key press" << keyEvent->key();
return true;
} else {
return false;
}
} else {
// pass the event on to the parent class
return QMainWindow::eventFilter(obj, event);
}
}
- MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
- eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下:void QObject::installEventFilter ( QObject * filterObj )这个函数接受一个QObject *类型的参数。eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。 事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。
总结
Qt中有很多事件:鼠标事件 键盘事件 大小改变事件 位置移动事件等 实际上 选择有两种:
- 所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是 win32 API 的WndProc()函数:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是:
switch(message)
{
case WM_PAINT:
// ..
break;
case WM_DESTROY:
// ...
break;
..
}
- 每一种事件对应一个事件处理函数.Qt就是这种机制: mouseEvent() keyPressEvent() ...
event函数会有两个问题;
- 链接库的继承
- event不能阻止屏蔽事件
Qt事件的调用最终都会追溯到QCoreApplication::notify()
函数 最大的控制权是重写QCoreApplication::notify()
,函数的声明是:
virtual bool QCoreApplication::notify ( QObject * receiver, QEvent * event );
Qt事件处理里的5个层次:
- 重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
- 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
- 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
- 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
- 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。
7 文件系统
Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):
- QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
- QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
- QFlie:访问本地文件或者嵌入资源;
- QTemporaryFile:创建和访问本地文件系统的临时文件;
- QBuffer:读写QbyteArray, 内存文件;
- QProcess:运行外部程序,处理进程间通讯;
- QAbstractSocket:所有套接字类的父类;
- QTcpSocket:TCP协议网络数据传输;
- QUdpSocket:传输 UDP 报文;
- QSslSocket:使用 SSL/TLS 传输数据;
文件系统分类:
- 顺序访问设备
- 随即访问设备
基本文件操作
QFile提供了从文件中读取和写入数据的能力 通常将文件路径作为参数传递给QFile的构造函数. 我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。
有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取
使用QFile的操作:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QFile file("in.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Open file failed.";
return -1;
} else {
while (!file.atEnd()) {
qDebug() << file.readLine();
}
}
QFileInfo info(file);
qDebug() << info.isDir();
qDebug() << info.isExecutable();
qDebug() << info.baseName();
qDebug() << info.completeBaseName();
qDebug() << info.suffix();
qDebug() << info.completeSuffix();
return app.exec();
}
- isDir()检查该文件是否是目录;
- isExecutable() 检查该文件是否是可执行文件等。
- baseName() 可以直接获得文件名;
- completeBaseName() 获取完整的文件名
- suffix() 则直接获取文件后缀名。
- completeSuffix() 获取完整的文件后缀
二进制文件读写
QDataStream提供了基于QIODevice的二进制数据的序列化。 这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。
。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
file.close(); // 如果不想关闭文件,可以使用
file.flush();
QFile file("file.dat");
file.open(QIODevice::ReadWrite);
QDataStream stream(&file);
QString str = "the answer is 42";
stream << str;
文本文件读写
利用QTextStream类读写文本文件
QIODevice::NotOpen 未打开
QIODevice::ReadOnly 以只读方式打开
QIODevice::WriteOnly 以只写方式打开
QIODevice::ReadWrite 以读写方式打开
QIODevice::Append 以追加的方式打开,新增加的内容将被追加到文件末尾
QIODevice::Truncate 以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。
QIODevice::Text 在读取时,将行结束符转换成 \n;
QIODevice::Unbuffered 忽略缓存
Socket通信
Qt中提供的所有的Socket类都是非阻塞的。
Qt中常用的用于socket通信的套接字类:
QTcpServer 用于TCP/IP通信, 作为服务器端套接字使用
QTcpSocket 用于TCP/IP通信,作为客户端套接字使用。
QUdpSocket 用于UDP通信,服务器,客户端均使用此套接字。
TCP/IP
在Qt中实现TCP/IP服务器端通信的流程:
- 创建套接字
- 将套接字设置为监听模式
- 等待并接受客户端请求
- 接收或者向客户端发送数据
可以通过QTcpServer提供的void newConnection()***信号来检测是否有连接请求,如果有可以在对应的槽函数中调用nextPendingConnection函数获取到客户端的Socket信息(返回值为QTcpSocket类型指针),通过此套接字与客户端之间进行通信。
接收数据:使用read()或者readAll()函数 发送数据:用write()函数
客户端通信流程:
- 创建套接字
- 连接服务器
- 可以使用QTcpSocket类的 connectToHost 函数来连接服务器
- 向服务器发送或者接收数据
服务器端
通过Qt提供的QTcpServer类实现服务器端的socket通信
//---------- tcpserver.h ------------
class TCPServer : public QMainWindow
{
Q_OBJECT
public:
explicit TCPServer(QWidget *parent = 0);
~TCPServer();
public slots:
void slotNewConnection();
void slotReadyRead();
private:
Ui::TCPServer *ui;
// 负责监听的套接字
QTcpServer* m_server;
// 负责通信的套接字
QTcpSocket* m_client;
};
//---------- tcpserver.cpp ------------
TCPServer::TCPServer(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TCPServer),
m_server(NULL),
m_client(NULL)
{
ui->setupUi(this);
//创建套接字对象
m_server = new QTcpServer(this);
//将套接字设置为监听模式
m_server->listen(QHostAddress::Any, 9999);
//通过信号接收客户端请求
connect(m_server, &QTcpServer::newConnection, this, &TCPServer::slotNewConnection);
}
TCPServer::~TCPServer()
{
delete ui;
}
void TCPServer::slotNewConnection()
{
if(m_client == NULL)
{
//处理客户端的连接请求
m_client = m_server->nextPendingConnection();
//发送数据
m_client->write("服务器连接成功!!!");
//连接信号, 接收客户端数据
connect(m_client, &QTcpSocket::readyRead, this, &TCPServer::slotReadyRead);
}
}
void TCPServer::slotReadyRead()
{
//接收数据
QByteArray array = m_client->readAll();
QMessageBox::information(this, "Client Message", array);
}
客户端
客户端通过使用Qt提供的QTcpSocket类可以方便的实现与服务器端的通信
//------------- tcpclient.h ------------
class TCPClient : public QMainWindow
{
Q_OBJECT
public:
explicit TCPClient(QWidget *parent = 0);
~TCPClient();
public slots:
void slotReadyRead();
void slotSendMsg();
private:
Ui::TCPClient *ui;
QTcpSocket* m_client;
};
//------------- tcpclient.cpp --------------
TCPClient::TCPClient(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TCPClient)
{
ui->setupUi(this);
//创建套接字
m_client = new QTcpSocket(this);
//连接服务器
m_client->connectToHost(QHostAddress("127.0.0.1"), 9999);
//通过信号接收服务器数据
connect(m_client, &QTcpSocket::readyRead, this, &TCPClient::slotReadyRead);
//发送按钮
connect(ui->btnSend, &QPushButton::clicked, this, &TCPClient::slotSendMsg);
}
TCPClient::~TCPClient()
{
delete ui;
}
void TCPClient::slotReadyRead()
{
//接收数据
QByteArray array = m_client->readAll();
QMessageBox::information(this, "Server Message", array);
}
void TCPClient::slotSendMsg()
{
QString text = ui->textEdit->toPlainText();
//发送数据
m_client->write(text.toUtf8());
ui->textEdit->clear();
}
UDP
使用Qt提供的QUdpSocket进行UDP通信. 在UDP方式下,客户端并不与服务器建立连接,它只负责调用发送函数向服务器发送数据. 在UDP方式下,客户端并不与服务器建立连接,它只负责调用发送函数向服务器发送数据。类似的服务器也不从客户端接收连接,只负责调用接收函数,等待来自客户端的数据的到达。
两部分的工作大致相同:
- 创建套接字
- 绑定套接字
在UDP中如果需要接收数据则需要对套接字进行绑定,只发送数据则不需要对套接字进行绑定。
通过调用bind()函数将套接字绑定到指定端口上。
接收或者发送数据
接收数据:使用readDatagram()接收数据,函数声明如下:
qint64 readDatagram(char * data, qint64 maxSize, QHostAddress * address = 0, quint16 * port = 0)
参数 data: 接收数据的缓存地址 maxSize: 缓存接收的最大字节数 address: 数据发送方的地址(一般使用提供的默认值) port: 数据发送方的端口号(一般使用提供的默认值)
发送数据:使用writeDatagram()函数发送数据,函数声明如下:
qint64 writeDatagram(const QByteArray & datagram, const QHostAddress & host, quint16 port)
参数: datagram:要发送的字符串 host:数据接收方的地址 port:数据接收方的端口号
广播
在使用QUdpSocket类的writeDatagram()函数发送数据的时候,其中第二个参数host应该指定为广播地址:QHostAddress::Broadcast此设置相当于QHostAddress("255.255.255.255")使用UDP广播的的特点:
使用UDP进行广播,局域网内的其他的UDP用户全部可以收到广播的消息 UDP广播只能在局域网范围内使用
Tcp和UDP
TCP/IP UDP 是否连接 面向连接 无连接 传输方式 基于流 基于数据报 传输可靠性 可靠 不可靠 传输效率 效率低 效率高 能否广播
多线程
通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操作(例如,大批量I/O或大量矩阵变换等CPU密集操作)时,用户界面常常会冻结。而使用多线程可以解决这一问题。
多线程的几个优势:
- 提高应用程序响应速度
- 使用CUP系统更加有效
- 改善程序结构
多线程程序有以下几个特点:
多线程程序的行为无法预期,当多次执行程序时,每一次的结果都可能不同。 多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。 多线程的切换可能发生在任何时刻、任何地点。 多线程对代码的敏感度高,对代码的细微修改都可能产生意想不到的结果。