【Qt应用程序】
Qt应用程序
- 摘要
- 概述
- 快速开始
- Qt在线下载与安装
- Visual Studio开发Qt项目
- VS配置Qt扩展
- VS创建Qt项目
- 配置qDebug调试信息
- 配置源程序的字符集
- 项目结构
- 对象树与内存回收
- 基础数据类型
- 信号槽
- 定时器
- 窗口
- QWidget
- QMainWindow
- QDialog
- 窗口布局
- 窗口中添加右键菜单
- 控件
- 按钮类
- 容器类
- 自定义控件
- 事件
- 事件处理机制
- 事件过滤器
- 事件分发器
- 事件处理器
- 多线程
- QThread线程类
- 线程池
- 网络编程
- 基于TCP套接字的网络通信
- 基于WebSocket的双向网络通信
- 数据持久化
- 数据库插件与操作
- JSON
- 程序发布与打包
- 发布
- 打包
- C++语法补充
- Lambda表达式
练习项目
摘要
本篇博客对Qt的核心基础知识进行了总结,以便可以快速上手Qt应用程序,加深理解和记忆
概述
- 介绍:Qt是一个跨平台的C++应用程序开发框架
- 特点
- Qt是标准C++的扩展
- 有自己的事件处理机制,提供了signals/slots的安全类型来代替回调函数,使得各个组件之间的协同工作十分简单
- 支持2D/3D图形渲染,支持OpenGL
- 框架底层模块化,用户使用时可进行裁剪
- 可移植性强:不同平台使用相同的上层接口,底层封装了不同平台的API
- 模块架构(All Modules)
- 基本模块Qt Essentials:提供了Qt在所有平台上的基本功能
- Qt Core:Qt类库的核心,其他所有模块都依赖于此模块
- Qt GUI:设计GUI的基础类,包括OpenGL
- Qt Widgets:用于构建GUI界面的C++图形组件类
- Qt Multimedia:音频、视频、摄像头和广播功能的类
- Qt Multimedia Widgets:实现多媒体功能的界面组件类
- Qt Network:网络编程
- Qt QML:用于QML和js语言的类
- Qt Quick:用于构建具有定制用户界面的动态应用程序的声明框架
- Qt Quick Controls:创建桌面样式用户界面,基于Qt Quick的用户界面控件
- Qt Quick Dialogs:用于Qt Quick的系统对话框类型
- Qt Quick Layouts:用于Qt Quick 2界面元素的布局项
- Qt SQL:数据库操作
- Qt Test:单元测试
- 附加模块Qt Add-Ons:实现了一些特定功能的附加价值的模块
- 增值模块Value-AddModules:单独发布的提供额外价值的模块或工具
- 技术预览模块Technology Preview Modules:一些处于开发阶段,可作为技术预览使用的模块
- Qt工具
- 基本模块Qt Essentials:提供了Qt在所有平台上的基本功能
快速开始
Qt在线下载与安装
- Qt及相关工具的在线下载
Qt官网:Qt | Tools for Each Stage of Software Development Lifecycle
- 安装
在自定义安装中,选择编译工具的类型
- 【为Qt配置环境变量】:如
~\Qt\6.9.2\msvc2022_64\bin
Visual Studio开发Qt项目
VS配置Qt扩展
- 安装扩展:扩展 → 管理扩展 → Qt Visual Studio Tools
- 配置Qt版本:重启Visual Studio → 扩展 → Qt VS Tools → Qt Versions
- 【配置Qt Designer与窗体分离】(避免双击
*.ui
文件报错)
VS创建Qt项目
配置qDebug调试信息
从默认的窗口 → 控制台
配置源程序的字符集
Visual Studio源程序的字符集默认为国标,Qt默认的字符集是uft8。因此可以在VS中进行适配:
1)增加文件的高级配置选项
2)配置源文件字符集
项目结构
项目结构顺序:
由项目文件
配置项目信息,组织项目文件 → 由可视化工具设计界面UI(生成UI的XML布局文件)→ 由框架读取XML布局文件生成UI头文件UI_窗体类名.h
,其包含了UI类,用于代码级别的控件组织和布局 → 由窗体类名.h
窗体头文件的窗体类声明,引入UI头文件,实现窗体与UI的绑定 → 由窗体类名.cpp
窗体类源代码对窗体类头文件中的窗体类声明进行实现,并进行逻辑处理。
- 项目文件
.pro
,该文件记录了项目的属性信息
# 项目编译的时候需要加载哪些底层模块
QT = core gui widgets # 关闭自动“fixpath”行为
# fixpath行为:
# 1.Qt会检测你的项目所依赖的 Qt 模块(如 core, gui, widgets)
# 2.Qt会找到这些模块对应的 DLL 所在的目录(即 Qt 安装目录下的 bin 文件夹)
# 3.Qt会在启动你的 .exe 文件之前,临时地将 Qt 的 bin 目录路径前置(prepend)到当前的环境变量 PATH 中
CONFIG += no_fixpath# 如果在项目中调用了废弃的函数, 项目编译的时候会有警告的提示
# DEFINES += QT_DEPRECATED_WARNINGS# 项目中的源文件
# SOURCES += \
# QtStudy.cpp \
# QtStudy.cpp# 项目中的头文件
# HEADERS += \
# QtStudy.h# 项目中的窗口界面文件
# FORMS += \
# QtStudy.ui
- 程序入口文件
// 窗口类头文件
#include "QtStudy.h"
// 应用程序类头文件
#include <QtWidgets/QApplication>int main(int argc, char *argv[])
{// 创建应用程序对象:在一个Qt项目中仅有一个实例,作用进行事件循环并处理QApplication app(argc, argv);// 创建窗口类对象QtStudy window;// 显示窗口window.show();// 应用程序对象开始事件循环return app.exec();
}
-
窗口程序文件
- UI文件:Qt窗口的UI布局是以XML文件形式组织的
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"><class>QtStudyClass</class><widget class="QMainWindow" name="QtStudyClass"><property name="geometry"><rect><x>0</x><y>0</y><width>580</width><height>400</height></rect></property><property name="windowTitle"><string>QtStudy</string></property><widget class="QMenuBar" name="menuBar"><property name="geometry"><rect><x>0</x><y>0</y><width>580</width><height>22</height></rect></property></widget><widget class="QToolBar" name="mainToolBar"><attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute><attribute name="toolBarBreak"><bool>false</bool></attribute></widget><widget class="QStatusBar" name="statusBar"/></widget><layoutdefault spacing="6" margin="11"/><resources><include location="QtStudy.qrc"/></resources><connections/> </ui>
-
头文件
-
UI头文件:该文件通过读取UI文件自动生成,当UI文件发送变动时,重新编译项目,该文件会重新生成
/********************************************************************************** Form generated from reading UI file 'QtStudy.ui'**** Created by: Qt User Interface Compiler version 6.9.2**** WARNING! All changes made in this file will be lost when recompiling UI file!********************************************************************************/#ifndef UI_QTSTUDY_H#define UI_QTSTUDY_H#include <QtCore/QVariant>#include <QtWidgets/QApplication>#include <QtWidgets/QMainWindow>#include <QtWidgets/QMenuBar>#include <QtWidgets/QPushButton>#include <QtWidgets/QStatusBar>#include <QtWidgets/QToolBar>#include <QtWidgets/QWidget>QT_BEGIN_NAMESPACEclass Ui_QtStudyClass{public:QWidget *centralWidget;QMenuBar *menuBar;QToolBar *mainToolBar;QStatusBar *statusBar;void setupUi(QMainWindow *QtStudyClass){if (QtStudyClass->objectName().isEmpty())QtStudyClass->setObjectName("QtStudyClass");QtStudyClass->resize(580, 400);centralWidget = new QWidget(QtStudyClass);centralWidget->setObjectName("centralWidget");QtStudyClass->setCentralWidget(centralWidget);menuBar = new QMenuBar(QtStudyClass);menuBar->setObjectName("menuBar");menuBar->setGeometry(QRect(0, 0, 580, 22));QtStudyClass->setMenuBar(menuBar);mainToolBar = new QToolBar(QtStudyClass);mainToolBar->setObjectName("mainToolBar");QtStudyClass->addToolBar(Qt::ToolBarArea::TopToolBarArea, mainToolBar);statusBar = new QStatusBar(QtStudyClass);statusBar->setObjectName("statusBar");QtStudyClass->setStatusBar(statusBar);retranslateUi(QtStudyClass);QMetaObject::connectSlotsByName(QtStudyClass);} // setupUivoid retranslateUi(QMainWindow *QtStudyClass){QtStudyClass->setWindowTitle(QCoreApplication::translate("QtStudyClass", "QtStudy", nullptr));}; // retranslateUinamespace Ui {class QtStudyClass: public Ui_QtStudyClass {};} // namespace UiQT_END_NAMESPACE#endif // UI_QTSTUDY_H
-
窗体头文件
#pragma once// Qt标准窗口类头文件#include <QtWidgets/QMainWindow>// 自动生成的UI头文件#include "ui_QtStudy.h"class QtStudy : public QMainWindow{// 该宏是为了能够使用Qt中的信号槽机制Q_OBJECTpublic:QtStudy(QWidget *parent = nullptr);~QtStudy();private:// 定义指向窗口的UI对象Ui::QtStudyClass ui;};
-
-
源程序
#include "QtStudy.h"QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent) {// 双向绑定,为UI对象设置对应的窗体ui.setupUi(this); }QtStudy::~QtStudy() {}
对象树与内存回收
QObject
是Qt所有类的基类,它是以对象树的形式组织起来的。**当创建一个QObject对象时,其构造函数会接收一个QObject指针作为参数,该参数即为父对象指针parent(通常是组件的父窗体/容器),然后创建的QObject对象会被加入到其父对象的children()列表中。**当父对象被析构时,其children列表中的所有对象也会被析构。(如,当用户关闭一个对话框时,应用程序会将该对话框对象的内存进行释放回收,其包含的按钮、图标等组件对象也会被一并删除)
- 当一个QObject对象在堆上创建的时候,Qt会为其创建一个对象树。不过,该对象树中对象的顺序是未定的,这意味着这些对象销毁时的顺序也是不定的
- 对象树中的任何对象被删除时,若该对象有parent,则自动从parent的children()列表中删除;如果该对象有孩子,则自动删除列表中的每个孩子
为QObject对象指定parent的方式:
// 1.在创建对象时,在构造函数中传入parent指针
QWidget::QWidget(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
QTimer::QTimer(QObject *parent = nullptr);// 2.通过setParent()方法,假设这个控件没有在构造的时候指定符对象, 可以调用QObject的api指定父窗口对象
void QObject::setParent(QObject *parent);
基础数据类型
- 基础类型
头文件#include <QtGlobal>
(虽然在Qt中有属于自己的整形或者浮点型, 但是在开发过程中一般不用, 常用的类型关键字还是 C/C++中的 int, float, double 等)
类型名称 | 对应关系 | 备注 |
---|---|---|
qint8 | signed char | 有符号8位数据 |
qint16 | signed short | 16位数据类型 |
qint32 | signed short | 32位有符号数据类型 |
qint64 | long long int 或(__int64) | 64位有符号数据类型,Windows中定义为__int64 |
qintptr | qint32 或 qint64 | 指针类型 根据系统类型不同而不同,32位系统为qint32、64位系统为qint64 |
qlonglong | long long int 或(__int64) | Windows中定义为__int64 |
qptrdiff | qint32 或 qint64 | 根据系统类型不同而不同,32位系统为qint32、64位系统为qint64 |
qreal | double 或 float | 除非配置了-qreal float选项,否则默认为double |
quint8 | unsigned char | 无符号8位数据类型 |
quint16 | unsigned short | 无符号16位数据类型 |
quint32 | unsigned int | 无符号32位数据类型 |
quint64 | unsigned long long int 或 (unsigned __int64) | 无符号64比特数据类型,Windows中定义为unsigned __int64 |
quintptr | quint32 或 quint64 | 根据系统类型不同而不同,32位系统为quint32、64位系统为quint64 |
qulonglong | unsigned long long int 或 (unsigned __int64) | Windows中定义为__int64 |
uchar | unsigned char | 无符号字符类型 |
uint | unsigned int | 无符号整型 |
ulong | unsigned long | 无符号长整型 |
ushort | unsigned short | 无符号短整型 |
- 控制台输出
在Qt中进行log输出, 一般不使用c中的printf, 也不是使用C++中的cout, Qt框架提供了专门用于日志输出的类qDebug,其头文件为:#include <QDebug>
直接执行可执行程序(非IDE调试)下没有日志输出窗口,默认情况下日志信息是不会打印到终端窗口的, 如果想要实现这样的效果, 必须在项目文件中添加相关的属性信息(Visual Studio IDE的Debug调试配置如上文)
# 项目文件(*.pro)# 找到配置项 config, 添加 console 控制台属性
CONFIG += c++11 console
#include <QtGlobal>
#include <QDebug>int basic_data_type()
{qreal double_type = 9.9;uint unsigned_int_type = 9;qDebug() << double_type << "\t" << unsigned_int_type << "\n";return 0;
}
若在qDebug打印日志时使用中文,但是编译报错,此时属于字符集的错误。有如下解决方法:
1)修改IDE编辑源码时的字符集(VS参考如上)
2)在项目文件.pro中添加编码配置:QMAKE_CXXFLAGS += /utf-8
3)打印时使用宽字符编码:qDebug() << "中文";
4)依靠QString进行转码:qDebug() << QString::fromUtf8("中文");
5)使用tr函数:qDebug() << tr("中文");
- 字符串类型
语言类型 | 字符串类型 |
---|---|
C | char* |
C++ | std::string, char* |
Qt | QByteArray, QString 等 |
构造、操作、查找、类型转换
- 位置和尺寸
点QPoint、直线QLine、尺寸(长宽)QSize、矩形QRect
- 日期和时间
日期QDate、时间QTime、日期时间QDateTime
- QVariant
QVariant可以作为不同Qt和C++数据类型及其自定义数据的容器
#include <QVariant>/*
* QVariant
*/
int test_qvariant()
{// 1.构造、初始化QVariant vInt(42);QVariant vDouble = 3.14159;QVariant vBool = false;QVariant q;q.setValue('a');QVariant qv = QVariant::fromValue(QStringList({ "hello lyf1" ,"hello lyf2" ,"hello lyf3" }));// 2.检测存储数据的类型qDebug() << vInt.typeName();qDebug() << vDouble.typeName();qDebug() << vInt.typeName();qDebug() << q.typeName();qDebug() << qv.typeName();// 3.检索并转换具体数据类型qDebug()<< vInt.toInt();qDebug() << vDouble.toDouble();qDebug() << vInt.toBool();qDebug() << q.toChar();// 【复杂】类型转换检查与转换if (qv.canConvert<QStringList>()) {QStringList qsl = qv.value<QStringList>();for (QString qsli : qsl) {qDebug() << qsli;}}// 4.自定义类型return 0;
}
信号槽
信号槽是Qt的事件模块(实际就是观察者模式:发布-订阅模式)当某个事件发生后(如按钮被点击)它就会发出一个信号signal,如果有对象对该信号感兴趣,它就会通过connect函数,将待处理的信号和自己的槽函数slot(回调函数)进行绑定(订阅)
- Qt标准信号槽的使用
connect
函数原型如下:
QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);
参数:- sender: 发出信号的对象- signal: 属于sender对象, 信号是一个函数, 这个参数的类型是函数指针, 信号函数地址- receiver: 信号接收者- method: 属于receiver对象, 当检测到sender发出了signal信号, receiver对象调用method方法,信号发出之后的处理动作// 参数 signal 和 method 都是函数地址, 因此简化之后的 connect() 如下:
connect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);
在Qt自身的源码中,信号函数和槽函数是在组件类中用Q_SIGNALS
和Q_SLOTS
进行声明的,如下示例:
QPushButton
继承于QAbstractButton
。QAbstractButton
中的Q_SIGNALS
声明了其按下按钮的信号函数:
QMainWindow
继承于QWidget
。QWidget
中的Q_SLOTS
声明了其关闭窗口的槽函数:
在窗口内将这两个信号函数和槽函数连接
/*
* QtStudy.cpp:主窗口函数
*/
#include "QtStudy.h"/*
* 主窗口函数构造函数的实现
* connect()操作一般写在窗口的构造函数中, 相当于在事件产生之前在qt框架中先进行注册, 这样在程序运行过程中假设产生了按钮的点击事件, 框架就会调用信号接收者对象对应的槽函数了, 如果信号不产生, 槽函数也就一直不会被调用
*/
QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent)
{// 双向绑定,为UI对象设置对应的窗体ui.setupUi(this);// 1.Qt5后的语法,使用函数指针。这种方法在Qt6中仍被推荐connect(ui.pushButton, &QPushButton::clicked, this, &QMainWindow::close);// 2.Qt4的语法,字符串语法。这种方法在Qt5后不再被推荐connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(close()));// 3.lamuda表达式语法,结合C++11语法新特性// 正确:槽函数(回调函数)可以使用拉姆达表达式(实际是匿名函数)connect(ui.pushButton, &QPushButton::clicked, this, [this]() {this->close();});// 错误,原因是:信号是类接口的一部分,需要在类定义中明确,不能使用拉姆达表达式。且此处手动触发按钮按下,逻辑也是错误的。//connect(ui.pushButton, [this]() {// ui.pushButton->clicked(); }, this, [this]() {// this->close();// });// 错误,原因是:信号是类接口的一部分,需要在类定义中明确,不能使用拉姆达表达式。且此处手动触发按钮按下,逻辑也是错误的。//QPushButton* btn = ui.pushButton;//connect(ui.pushButton, [btn]() {// btn->clicked(); }, this, [this]() {// this->close(); // });// 4.使用QObject::connect的返回值:Qt6 中 connect() 返回 QMetaObject::Connection,可用于断开连接// QMetaObject::Connection connection = connect(ui.pushButton, &QPushButton::clicked, this, &QMainWindow::close);// disconnect(connection);// 5.利用自动连接命名约定:如果槽函数命名为 on_对象名_信号名,Qt 会自动连接}QtStudy::~QtStudy()
{}
-
自定义信号槽函数
-
自定义信号函数
- 信号函数的返回值必须是void类型
- 信号函数支持重载
- 信号函数是成员函数,只需要声明,不能实现
- 信号函数使用signals进行声明,无需加访问修饰符(实际默认是protect),由此只能类内访问,这是为了保护信号的发送在类内进行(但也可以增加其他访问修饰符)如果要在类外发送信号,则可以增加一个Public修饰的中间函数,在中间函数中使用
emit
(其作用只是声明信号要被发送了,可省略不写,底层是emit==#define emit
)调用信号函数 - 信号函数、槽函数的返回值必须是void类型
-
自定义槽函数
- 槽函数的返回值必须是void类型
- 槽函数支持重载
- 槽函数的类型可以是:类成员函数、全局函数、静态函数、拉姆达表达式(如上例)
- 信号函数用【用slots进行修饰】(Qt5之后,slots可以省略)槽函数的访问修饰符通常为
public slot
,使得该类的槽函数可以被Qt框架回调(如下图,signals和slots实际同Qt框架内的Q_SIGNALS和Q_SLOTS一致)
- 示例:Qt标准控件自带的信号函数和自定义的槽函数
/* * QtStudy.cpp:窗口类声明 */ #pragma once#include <QtWidgets/QMainWindow> #include "ui_QtStudy.h"class QtStudy : public QMainWindow {Q_OBJECTpublic:QtStudy(QWidget *parent = nullptr);~QtStudy();private:Ui::QtStudyClass ui;// 按钮点击计数函数,在按钮点击信号绑定的槽函数中使用int click_count = 0;// 声明窗口类的自定义槽函数 public slots:void on_push_button_click(); };
/* * 窗口类的实现函数 */ #include "QtStudy.h"QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent) {ui.setupUi(this);// 连接自定义信号槽connect(ui.pushButton, &QPushButton::clicked, this, &QtStudy::on_push_button_click);}QtStudy::~QtStudy() {}/* * 按键计数槽函数 */ void QtStudy:: on_push_button_click() {QString s;s = s.fromStdString(std::to_string(this->click_count++));qDebug() << s;ui.pushButton->setText(s); }
- 示例:自定义信号和槽
#pragma once#include <QtWidgets/QMainWindow> #include "ui_QtStudy.h"class QtStudy : public QMainWindow {// 该宏是为了能够使用Qt中的信号槽机制Q_OBJECTpublic:QtStudy(QWidget *parent = nullptr);~QtStudy();private:Ui::QtStudyClass ui;public slots:void get_message();void get_message(int x); };/* * 自定义信号类,需继承QObject类,必须使用 public 继承 */ class TestSignal : public QObject {// 必须的宏,启用信号槽功能Q_OBJECTsignals:// 信号只能声明,不能有实现void send_signal();void send_signal(int x);public:// 提供公共方法来触发信号void triggerSignal();void triggerSignal(int x); };
#include "QtStudy.h"/* * 提供公共方法来触发信号 */ void TestSignal::triggerSignal() {qDebug() << "Emitting send signal with no args";// 发射无参信号,使用emitemit send_signal(); }void TestSignal::triggerSignal(int x) {qDebug() << "Emitting send signal with arg:" << x;// 发射无参信号,使用emitemit send_signal(x); }QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent) {ui.setupUi(this);// 1.自定义信号和槽函数// 1.1创建信号源对象TestSignal ts;// 1.2.1针对信号槽重载的情况,Qt4版本的绑定信号函数和槽函数:字符串连接。正确connect(&ts, SIGNAL(send_signal()), this, SLOT (get_message()));connect(&ts, SIGNAL(send_signal(int)), this, SLOT(get_message(int)));// 1.2.2针对信号槽重载的情况,若直接Qt4版本的绑定信号函数和槽函数:函数指针,则会出错。因为不知道指向// connect(&ts, &TestSignal::send_signal, this, &QtStudy::get_message);// 不过可以手动定义对应类型的函数指针,给connect传参void (TestSignal:: *send_signal_with_no_arg)() = &TestSignal::send_signal;void (QtStudy:: *get_message_with_no_arg)() = &QtStudy::get_message;connect(&ts, send_signal_with_no_arg, this, get_message_with_no_arg);void (TestSignal:: * send_signal_with_arg)(int) = &TestSignal::send_signal;void (QtStudy:: * get_message_with_arg)(int) = &QtStudy::get_message;connect(&ts, send_signal_with_arg, this, get_message_with_arg);// 1.3调用信号函数// 无参ts.triggerSignal();// 有参ts.triggerSignal(9);}QtStudy::~QtStudy() {}/* * 无参接收信息槽函数 */ void QtStudy::get_message() {qDebug() << "get message with no args"; }/* * 含参接收信息槽函数 */ void QtStudy::get_message(int x) {qDebug() << "get message with no arg: " << x; }
-
-
扩展
- 一个信号可以连接多个槽函数,槽函数的执行顺序是随机的,与connect顺序无关
- 一个槽函数可以连接多个信号函数,响应不同信号
- 信号可以连接信号,进行信号的链式传递
定时器
Qt中的定时器类是QTimer,使用时:创建定时器实例 → 设置相关参数 → 通过connect与定时器的信号函数绑定 → 周期性启用或一次性启用定时器 → 超时发送信号(调用定时器自身的信号函数[signal] void QTimer::timeout();
)→ 通过绑定的槽函数执行回调,实现定时任务
#pragma once#include <QtWidgets/QMainWindow>
#include "ui_QtStudy.h"
#include "QTimer"
#include "QDateTime"class QtStudy : public QMainWindow
{// 该宏是为了能够使用Qt中的信号槽机制Q_OBJECTpublic:QtStudy(QWidget *parent = nullptr);~QtStudy();private:Ui::QtStudyClass ui;// 注意定时器实例应创建在成员变量(或全局变量)中,若在成员函数中创建,周期性的定时器会随着局部函数执行结束而被销毁,因此无法实现周期性定时功能QTimer qtimer;public slots:void update_current_time();
};
#include "QtStudy.h"
#include "basic_data_type.h"QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent)
{ui.setupUi(this);/* 三.定时器 */// 1.设置、读取时间精度qtimer.setTimerType(Qt::PreciseTimer);Qt::TimerType timer_type = qtimer.timerType();qDebug() << "时间精度: " << timer_type;// 2.设置、读取时间间隔qtimer.setInterval(1);int interval = qtimer.interval();qDebug() << "时间间隔: " << interval;// 3.设置定时器只触发一次// qtimer.setSingleShot(true);// 判断定时器是否只触发一次bool single_mark = qtimer.isSingleShot();qDebug() << "是否只执行一次: " << interval;// 4.启动或重启定时器,若无参数传入,则需通过setInterval设置qtimer.start();// 启动或重启定时器,若有参数传入,则无需通过setInterval设置// qtimer.start(1);// 5.判断定时器是否正在运行bool active_mark = qtimer.isActive();qDebug() << "是否运行: " << active_mark;// 6.停止定时器// qtimer.stop();// 7.绑定定时器connect(&qtimer, &QTimer::timeout, this, &QtStudy::update_current_time);// 8.定时器的另一种用法:static pubic 静态公共方法,定时器仅执行一次// [static] void QTimer::singleShot(// int msec, const QObject * receiver,// PointerToMemberFunction method);QTimer::singleShot(1, this, []() {qDebug() << "静态单次定时器被执行";});}QtStudy::~QtStudy()
{}/*
* 更新当前时间
*/
void QtStudy::update_current_time()
{QDateTime qdt = QDateTime::currentDateTime();QString s = qdt.toString();ui.lineEdit_2->setText(s);
}
窗口
窗口的坐标系:以窗口的左上角为坐标系原点,向右和向下分别为X、Y轴的延申轴
窗口的相对坐标:每个窗口都有自己的坐标原点,内嵌窗口的原点位置为其父窗口坐标系中的相对坐标
QWidget
QWidget类是所有窗口类的父类(控件类是也属于窗口类)
#pragma once#include <QtWidgets/QMainWindow>
#include "ui_QtStudy.h"class QtStudy : public QMainWindow
{Q_OBJECTpublic:QtStudy(QWidget *parent = nullptr);~QtStudy();private:// 子窗口QWidget sub_window;
};
#include "QtStudy.h"
#include "basic_data_type.h"QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent)
{ui.setupUi(this);/* 四.窗口 */// 1.widgit// 1.1设置父对象// void QWidget::setParent(QWidget *parent);// void QWidget::setParent(QWidget* parent, Qt::WindowFlags f);sub_window.setParent(this);// 1.2获取窗口的父对象QWidget* sub_parent = sub_window.parentWidget();qDebug() << sub_parent;// 1.3设置当前窗口的几何信息(位置和尺寸信息), 不包括边框sub_window.setGeometry(0,300,50,50);// 1.4获取当前窗口相对于父窗口的位置(包括边框)QRect r_no_edge = sub_window.frameGeometry();qDebug() << r_no_edge;// 1.t获取当前窗口相对于父窗口的位置(不包括边框)QRect r_with_edge = sub_window.geometry();qDebug() << r_with_edge;// 1.6设置窗体背景色(样式表)sub_window.setStyleSheet("background-color: red;");// 1.7显示窗口sub_window.show();// 1.8移动窗口位置connect(ui.pushButton_3, &QPushButton::clicked, this, [this]() {if (sub_window.frameGeometry().x() + 10 > this->frameGeometry().x()) {sub_window.move(0, sub_window.frameGeometry().y());}else {sub_window.move(sub_window.frameGeometry().x() + 10, sub_window.frameGeometry().y());}});// 1.9获取、设置窗口的尺寸信息QSize qs = sub_window.size();qDebug() << qs;sub_window.resize(50,50);// 1.10 获取、设置窗口的固定尺寸// 1.11 获取、设置窗口的最大尺寸// 1.12 获取、设置窗口的最小尺寸// 1.13 获取、设置窗口的高度、最小高度、最大高度// 1.14 获取、设置窗口的标题sub_window.setWindowTitle(QString("子窗口"));QString sub_window_tittle = sub_window.windowTitle();qDebug() << sub_window_tittle;// 1.15 获取、设置窗口的图标// 使用.qrc维护项目中的资源文件QIcon set_sub_window_icon(":/QtStudy/icon.ico");sub_window.setWindowIcon(set_sub_window_icon);QIcon get_sub_window_icon = sub_window.windowIcon();qDebug() << get_sub_window_icon;}QtStudy::~QtStudy()
{}
QWidget的信号函数:
// QWidget::setContextMenuPolicy(Qt::ContextMenuPolicy policy);
// 窗口的右键菜单策略 contextMenuPolicy() 参数设置为 Qt::CustomContextMenu, 按下鼠标右键发射该信号
[signal] void QWidget::customContextMenuRequested(const QPoint &pos);
// 窗口图标发生变化, 发射此信号
[signal] void QWidget::windowIconChanged(const QIcon &icon);
// 窗口标题发生变化, 发射此信号
[signal] void QWidget::windowTitleChanged(const QString &title);
QWidget的槽函数:
//------------- 窗口显示 -------------
// 关闭当前窗口
[slot] bool QWidget::close();
// 隐藏当前窗口
[slot] void QWidget::hide();
// 显示当前创建以及其子窗口
[slot] void QWidget::show();
// 全屏显示当前窗口, 只对windows有效
[slot] void QWidget::showFullScreen();
// 窗口最大化显示, 只对windows有效
[slot] void QWidget::showMaximized();
// 窗口最小化显示, 只对windows有效
[slot] void QWidget::showMinimized();
// 将窗口回复为最大化/最小化之前的状态, 只对windows有效
[slot] void QWidget::showNormal();//------------- 窗口状态 -------------
// 判断窗口是否可用
bool QWidget::isEnabled() const; // 非槽函数
// 设置窗口是否可用, 不可用窗口无法接收和处理窗口事件
// 参数true->可用, false->不可用
[slot] void QWidget::setEnabled(bool);
// 设置窗口是否可用, 不可用窗口无法接收和处理窗口事件
// 参数true->不可用, false->可用
[slot] void QWidget::setDisabled(bool disable);
// 设置窗口是否可见, 参数为true->可见, false->不可见
[slot] virtual void QWidget::setVisible(bool visible);
QMainWindow
- 菜单栏
可视化添加方式:
代码添加方式:
// 给菜单栏添加菜单
QAction *QMenuBar::addMenu(QMenu *menu);
QMenu *QMenuBar::addMenu(const QString &title);
QMenu *QMenuBar::addMenu(const QIcon &icon, const QString &title);// 给菜单对象添加菜单项(QAction)
QAction *QMenu::addAction(const QString &text);
QAction *QMenu::addAction(const QIcon &icon, const QString &text);// 添加分割线
QAction *QMenu::addSeparator();
单击菜单项的信号函数:
[signal] void QAction::triggered(bool checked = false);// ui->menu_item为QAction的名称(菜单项)
connect(ui->actionClick, &QAction::triggered, this, [=]()
{qDebug()<< "点击菜单项";
});
- 工具栏
可视化添加方式:在菜单栏中新增菜单项 → 将菜单项拖动至工具栏中
代码添加:
// 在QMainWindow窗口中添加工具栏
void QMainWindow::addToolBar(Qt::ToolBarArea area, QToolBar *toolbar);
void QMainWindow::addToolBar(QToolBar *toolbar);
QToolBar *QMainWindow::addToolBar(const QString &title);// 将Qt控件放到工具栏中
// 工具栏类: QToolBar
// 添加的对象只要是QWidget或者启子类都可以被添加
QAction *QToolBar::addWidget(QWidget *widget);// 添加QAction对象
QAction *QToolBar::addAction(const QString &text);
QAction *QToolBar::addAction(const QIcon &icon, const QString &text);// 添加分隔线
QAction *QToolBar::addSeparator();/* 示例 */
// 添加工具栏
QToolBar* toolbar = new QToolBar("toolbar");
this->addToolBar(Qt::LeftToolBarArea, toolbar);// 给工具栏添加按钮和单行输入框
ui->toolBar->addWidget(new QPushButton("搜索"));
QLineEdit* edit = new QLineEdit;
edit->setMaximumWidth(200);
edit->setFixedWidth(100);
ui->toolBar->addWidget(edit);
// 添加QAction类型的菜单项
ui->toolBar->addAction(QIcon("{资源路径}"), "");
- 状态栏
一般情况下, 需要在状态栏中添加某些控件, 显示某些属性, 使用最多的就是添加标签 QLabel
// 类型: QStatusBar
void QStatusBar::addWidget(QWidget *widget, int stretch = 0);[slot] void QStatusBar::clearMessage();
[slot] void QStatusBar::showMessage(const QString &message, int timeout = 0);
// 状态栏添加子控件
// 按钮
QPushButton* button = new QPushButton("按钮");
ui->statusBar->addWidget(button);
// 标签
QLabel* label = new QLabel("hello,world");
ui->statusBar->addWidget(label);
- 停靠窗口
停靠窗口Dock Widget可以通过鼠标拖拽停靠到窗口的上下左右位置上或浮动在窗口上方(若在非QMainWindows中添加了停靠窗口,则该窗口是不能浮动的)
QDialog
// 构造函数
QDialog::QDialog(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());// 模态显示窗口
[virtual slot] int QDialog::exec();// 隐藏模态窗口, 并且解除模态窗口的阻塞, 将 exec() 的返回值设置为 QDialog::Accepted
[virtual slot] void QDialog::accept();
// 隐藏模态窗口, 并且解除模态窗口的阻塞, 将 exec() 的返回值设置为 QDialog::Rejected
[virtual slot] void QDialog::reject();
// 关闭对话框并将其结果代码设置为r。finished()信号将发出r;
// 如果r是QDialog::Accepted 或 QDialog::Rejected,则还将分别发出accept()或Rejected()信号。
[virtual slot] void QDialog::done(int r);// 信号函数
[signal] void QDialog::accepted();
[signal] void QDialog::rejected();
[signal] void QDialog::finished(int result);
示例:
自定义一个对话框
#include <QDialog>
class MyDialog : public QDialog
{Q_OBJECT
public:MyDialog(QWidget * parent = nullptr);~MyDialog();private:QPushButton dialog_button1;QPushButton dialog_button2;QPushButton dialog_button3;
};
写该对话框的实现,在其初始化函数中,对其三个按钮进行设置,按下时分别调用其3个方法、并发送信号;在主窗体中显示模态对话框
#include "QtStudy.h"QtStudy::QtStudy(QWidget *parent) : QMainWindow(parent)
{ui.setupUi(this);// 3.QDialog自定义对话框connect(ui.pushButton_4, &QPushButton::clicked, this, [this]() {MyDialog my_dialog(this);// 模态显示窗口int ret = my_dialog.exec();if (ret == QDialog::Accepted){qDebug() << "Accept: " << ret;}else if (ret == QDialog::Rejected){qDebug() << "Accept: " << ret;}else{qDebug() << "Others: " << ret;}});
}QtStudy::~QtStudy()
{}/*
* 自定义对话框的初始化函数
*/
MyDialog::MyDialog(QWidget* parent) : QDialog(parent)
{this->setParent(parent);this->dialog_button1.setParent(this);this->dialog_button1.setText("accept");this->dialog_button1.setGeometry(50,50,50,30);connect(&this->dialog_button1, &QPushButton::clicked, this, [this]() {// 隐藏模态窗口, 并且解除模态窗口的阻塞, 将 exec() 的返回值设置为 QDialog::Acceptedthis->accept();});this->dialog_button1.show();this->dialog_button2.setParent(this);this->dialog_button2.setText("reject");this->dialog_button2.setGeometry(150, 50, 50, 30);connect(&this->dialog_button2, &QPushButton::clicked, this, [this]() {// 隐藏模态窗口, 并且解除模态窗口的阻塞, 将 exec() 的返回值设置为 QDialog::Rejectedthis->reject();});this->dialog_button2.show();this->dialog_button3.setParent(this);this->dialog_button3.setText("done");this->dialog_button3.setGeometry(250, 50, 50, 30);connect(&this->dialog_button3, &QPushButton::clicked, this, [this]() {// 关闭对话框并将其结果代码设置为r。finished()信号将发出r;// 如果r是QDialog::Accepted 或 QDialog::Rejected,则还将分别发出accept()或Rejected()信号。this->done(9);});this->dialog_button3.show();}/*
* 自定义对话框的析构函数
*/
MyDialog::~MyDialog()
{}
Qt中QDialog对话框的子类(调用其静态方法)
-
QMessageBox:通过这个类可以显示一些简单的提示框, 用于展示警告、错误、问题等信息
-
QFileDialog:通过这个类可以选择要打开/保存的文件或者目录
-
QFontDialog:通过这个类我们可以得到一个进行字体属性设置的对话框窗口
-
QColorDialog:通过这个类我们可以得到一个选择颜色的对话框窗口
-
QInputDialog:通过这个类我们可以得到一个输入对话框窗口。
-
根据实际需求我们可以在这个输入窗口中输入整形, 浮点型, 字符串类型的数据, 并且还可以显示下拉菜单供使用者选择
-
QProgressDialog:
-
通过这个类我们可以得到一个带进度条的对话框窗口, 这种类型的对话框窗口一般常用于文件拷贝、数据传输等实时交互的场景中
窗口布局
-
在UI中设置布局
-
方式1:使用Qt提供的布局, 从工具箱中找到相关的布局, 然后将其拖拽到UI窗口中,再将控件放入布局对应的区域内
-
方式2:将控件添加到容器类中(一般使用QWidget),然后右键容器,修改容器的布局方式
注意:通过UI编辑窗口的树状列表我们可以对所有窗口的布局进行检查, 如果发现某个窗口没有布局, 一定要对其进行设置, 如果某个窗口没有进行布局, 那么当这个窗口显示出来之后里边的子部件就可能无法被显示出来。
-
-
布局属性
- 弹簧:弹簧可以用来调整控件的位置及其控件间的距离,有水平弹簧
Horizontal Spacer
和垂直弹簧Vertical Spacer
,弹簧也有其自己的属性
-
通过API设置布局
QHBoxLayout
、QVBoxLayout
、QGridLayout
分别为水平、垂直、网格布局- QLayout
// 在布局最后面添加一个窗口 void QLayout::addWidget(QWidget *w); // 将某个窗口对象从布局中移除, 窗口对象如果不再使用需要自己析构 void QLayout::removeWidget(QWidget *widget); // 设置布局的四个边界大小, 即: 左、上、右和下的边距。 void QLayout::setContentsMargins(int left, int top, int right, int bottom); // 设置布局中各个窗口之间的间隙大小 void setSpacing(int);
- QHBoxLayout、QVBoxLayout
// 创建符窗口对象 QWidget *window = new QWidget; // 创建若干个子窗口对象 QPushButton *button = new QPushButton("One");// 创建水平布局对象 QHBoxLayout *layout = new QHBoxLayout; // 将子窗口添加到布局中 layout->addWidget(button); // 将水平布局设置给父窗口对象 window->setLayout(layout); // 显示父窗口 window->show();
- QGridLayout
// 创建父窗口对象 QWidget* window = new QWidget; // 创建子窗口对象 QPushButton *button1 = new QPushButton("One"); QPushButton *button2 = new QPushButton("Two"); QPushButton *button3 = new QPushButton("Three"); QPushButton *button4 = new QPushButton("Four"); QPushButton *button5 = new QPushButton("Five"); QPushButton *button6 = new QPushButton("Six"); // 多行文本编辑框 QTextEdit* txedit = new QTextEdit; txedit->setText("我占用了两行两列的空间哦。");QGridLayout* layout = new QGridLayout; // 两参数:第n行,第n列;四参数:第n行,第n列,长占几行,宽占几行 // 按钮起始位置: 第1行, 第1列, 该按钮占用空间情况为1行1列 layout->addWidget(button1, 0, 0); // 按钮起始位置: 第1行, 第2列, 该按钮占用空间情况为1行1列 layout->addWidget(button2, 0, 1); // 按钮起始位置: 第1行, 第3列, 该按钮占用空间情况为1行1列 layout->addWidget(button3, 0, 2); // 编辑框起始位置: 第2行, 第1列, 该按钮占用空间情况为2行2列 layout->addWidget(txedit, 1, 0, 2, 2); // 按钮起始位置: 第2行, 第3列, 该按钮占用空间情况为1行1列 layout->addWidget(button4, 1, 2); // 按钮起始位置: 第3行, 第3列, 该按钮占用空间情况为1行1列 layout->addWidget(button5, 2, 2); // 按钮起始位置: 第4行, 第1列, 该按钮占用空间情况为1行3列 layout->addWidget(button6, 3, 0, 1, 3);// 设置布局中水平方向窗口之间间隔的宽度 // void QGridLayout::setHorizontalSpacing(int spacing); // 设置布局中垂直方向窗口之间间隔的宽度 // void QGridLayout::setVerticalSpacing(int spacing);// 网格布局设置给父窗口对象 window->setLayout(layout); // 显示父窗口 window->show();
窗口中添加右键菜单
- 基于鼠标事件的实现:鼠标事件配合QMenu
- 基于窗口菜单的实现:
- QWidget类中的右键菜单函数 QWidget::setContextMenuPolicy(Qt::ContextMenuPolicy policy) 其可选参数如下:
- Qt::NoContextMenu:不能实现右键菜单
- Qt::PreventContextMenu:不能实现右键菜单
- Qt::DefaultContextMenu:基于事件处理器函数 QWidget::contextMenuEvent() 实现
- Qt::ActionsContextMenu:添加到当前窗口中所有 QAction 都会作为右键菜单项显示出来
- Qt::CustomContextMenu:基于 QWidget::customContextMenuRequested() 信号实现
控件
所有控件的基类都是QWidget(参考窗口类中的QWidget)
按钮类
-
QAbstractButton
- 按钮状态: Normal, Hover, Pressed
- check属性
// 判断按钮是否设置了checkable属性, 如果设置了点击按钮, 按钮一直处于选中状态(默认这个属性是关闭的, not checkable) bool QAbstractButton::isCheckable() const;// 设置按钮的checkable属性: // 参数为true: 点击按钮, 按钮被选中, 松开鼠标, 按钮不弹起 // 参数为false: 点击按钮, 按钮被选中, 松开鼠标, 按钮弹起 void QAbstractButton::setCheckable(bool);// 判断按钮是不是被按下的选中状态 bool QAbstractButton::isChecked() const;// 设置按钮的选中状态: true-选中, false-没选中(设置该属性前, 必须先进行 checkable属性的设置) void QAbstractButton::setChecked(bool);
- 信号函数
/* 当按钮被激活时(即,当鼠标光标在按钮内时按下然后释放),当键入快捷键时,或者当click()或animateClick()被调用时,这个信号被发出。值得注意的是,如果调用setDown()、setChecked()或toggle(),则不会触发此信号。 */ [signal] void QAbstractButton::clicked(bool checked = false); // 在按下按钮的时候发射这个信号 [signal] void QAbstractButton::pressed(); // 在释放这个按钮的时候发射直观信号 [signal] void QAbstractButton::released(); // 每当可检查按钮改变其状态时,就会发出此信号。checked在选中按钮时为true,在未选中按钮时为false。 [signal] void QAbstractButton::toggled(bool checked);
- 槽函数
// 执行一个动画点击:按钮被立即按下,并在毫秒后释放(默认是100毫秒)。 [slot] void QAbstractButton::animateClick(int msec = 100);// 执行一次按钮点击, 相当于使用鼠标点击了按钮 [slot] void QAbstractButton::click();// 参考 1.2 中的函数介绍 [slot] void QAbstractButton::setChecked(bool); // 设置按钮上图标大小 [slot]void setIconSize(const QSize &size); // 切换可检查按钮的状态。 checked <==> unchecked [slot] void QAbstractButton::toggle();
- 关联菜单(通常在QToolButton子类中使用)
/* 将弹出菜单菜单与此按钮关联起来。这将把按钮变成一个菜单按钮, 在某些样式中会在按钮文本的右边产生一个小三角形。 */ void QPushButton::setMenu(QMenu *menu);/* 显示(弹出)相关的弹出菜单。如果没有这样的菜单,这个函数什么也不做。 这个函数直到弹出菜单被用户关闭后才返回。 */ [slot] void QPushButton::showMenu();
-
QPushButton
默认按钮:
// 判断按钮是不是默认按钮
bool isDefault() const;
// 一般在对话框窗口中使用, 将按钮设置为默认按钮, 自动关联 Enter 键
void setDefault(bool);
容器类
自定义控件
- 自定义控件定义:在代码里写继承于基础控件的类
- GUI使用自定义控件:将基础控件拖拽到窗口中,右键控件,提升为:写入提升的类的名称
事件
事件处理机制
事件是由系统(如鼠标、键盘事件)在不同场景下发出的。每个Qt应用程序都对应唯一的QApplication应用对象,程序启动后,会调用该对象的exec
函数,这样Qt框架内部的事件检测就开始了。具体为,当事件产生时,事件会经过:事件派发→事件过滤→事件分发→ 事件处理
这几个阶段。
注意区分:事件和信号,事件是由系统产生的(事件的发送与传递是嵌套容器/控件,由Qt链式传递的),信号是由Qt对象主动发出的(发送和接收通过connect对两个对象进行绑定)
- 事件派发
当事件产生之后,Qt使用用应用程序对象调用notify()函数将事件发送到指定的窗口:
[override virtual] bool QApplication::notify(QObject *receiver, QEvent *e);
- 事件过滤
事件在发送过程中可以通过事件过滤器进行过滤,默认不对任何产生的事件进行过滤。
// 需要先给窗口安装过滤器, 该事件才会触发
[virtual] bool QObject::eventFilter(QObject *watched, QEvent *event);
- 事件分发
当事件发送到指定窗口之后,窗口的事件分发器会对收到的事件进行分发。
[override virtual protected] bool QWidget::event(QEvent *event);
- 事件处理
事件分发器会将分类之后的事件(鼠标事件、键盘事件、绘图事件…)分发给对应的事件处理器函数进行处理,每个事件处理器函数都有默认的处理动作
事件过滤器
当Qt的事件通过应用程序对象发送给相关窗口之后,窗口接收到数据之前这个期间可对事件进行过滤,过滤掉的事件就不能被继续处理了。
// 窗口头文件: mainwindow.h
class MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();// 声明主窗口的eventFilter方法(在窗口的实现类中对该方法进行重写),该方法用于主窗口对传递给它的事件进行过滤// 参数1 watched:要过滤的事件的所有者对象// 参数2 event:要过滤的具体的事件// 返回值 如果想过滤掉这个事件,停止它被进一步处理,返回true,否则返回 falsebool eventFilter(QObject *watched, QEvent *event);private:Ui::MainWindow *ui;
};
// 窗口源文件: mainwindow.cppMainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 给要被过滤事件的类对象安装事件过滤器:给窗口中的textEdit安装事件过滤器,由此textEvent可作为watch被监控ui->textEdit->installEventFilter(this);
}MainWindow::~MainWindow()
{delete ui;
}// 重写窗口的事件过滤器
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{// 判断对象和事件if(watched == ui->textEdit && event->type() == QEvent::KeyPress){QKeyEvent* keyEv = (QKeyEvent*)event;if(keyEv->key() == Qt::Key_Enter || // 小键盘确认keyEv->key() == Qt::Key_Return) // 大键盘回车{qDebug() << "我是回车, 被按下了...";return true;}}return false;
}
事件传递顺序:如容器A 包含了 容器B 再包含 容器C,容器C包含了控件,这样的嵌套结构。其鼠标点击控件的事件传递顺序为:控件→ 容器C → 容器B → 容器A(由内到外)。上述在控件installEventFilter,在容器中重写EventFilter,实际是事件先到达控件,但发现控件注册了容器的事件过滤器,然后系统会先到容器的事件过滤器进行检查(而非事件先到容器,从外到内)
如果一个对象安装了多个事件过滤器,那么最后安装的一个会最先执行。
**注意:**事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
事件分发器
事件分发器,是QWidget窗体基类的成员方法[override virtual protected] bool QWidget::event(QEvent *event);
,该函数接受一个QEvent类型的事件参数,该QEvent类具有向外传递(忽略)setAccepted(false) == ignore()
和接收(自己处理,不再向外传递)setAccepted(true) == accept()
- 事件分发
在无人为干预的情况下,事件分发器会自主完成事件的分发,如下部分源码:
bool QWidget::event(QEvent *ev)
{switch(ev->type()){// 鼠标移动case QEvent::MouseMove: mouseMoveEvent((QMouseEvent*)event);break;// 鼠标按下case QEvent::MouseButtonPress: mousePressEvent((QMouseEvent*)event);break;// 鼠标释放case QEvent::MouseButtonRelease: mouseReleaseEvent((QMouseEvent*)event);break;// 鼠标双击case QEvent::MouseButtonDblClick: mouseDoubleClickEvent((QMouseEvent*)event);break;// 键盘按键被按下事件case QEvent::KeyPress:break;.........default:break;}
}
- 事件拦截/过滤
通过重写该QWidget的成员函数即可,根据QEvent传入参数的类型进行判断,然后选择接收还是忽略,以做到对事件的拦截和过滤
bool MainWindow::event(QEvent *ev)
{if(ev->type() == QEvent::MouseButtonPress || ev->type() == QEvent::MouseButtonDblClick){// 过滤调用鼠标按下的事件return true;}// 注意:对于这两个事件以外的其他事件是没有任何影响的,因为在重写的事件分发器函数的最后调用了父类的事件分发器函数return QWidget::event(ev);
}
事件处理器
事件处理器位于Qt事件机制的末端,即事件的回调函数
- 一些事件处理器
// 1.鼠标事件
// 1.1鼠标按下:当鼠标左键、鼠标右键、鼠标中键被按下,该函数被自动调用,通过参数可以得到当前按下的是哪个鼠标键
[virtual protected] void QWidget::mousePressEvent(QMouseEvent *event);
// 1.2鼠标释放:当鼠标左键、鼠标右键、鼠标中键被释放,该函数被自动调用,通过参数可以得到当前释放的是哪个鼠标键
[virtual protected] void QWidget::mouseReleaseEvent(QMouseEvent *event);
// 1.3鼠标移动:当鼠标移动(也可以按住一个或多个鼠标键移动),该函数被自动调用,通过参数可以得到在移动过程中哪些鼠标键被按下了
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent *event);
// 1.4鼠标双击:当鼠标双击该函数被调用,通过参数可以得到是通过哪个鼠标键进行了双击操作
[virtual protected] void QWidget::mouseDoubleClickEvent(QMouseEvent *event);
// 1.5鼠标进入:当鼠标进入窗口的一瞬间,触发该事件,注意:只在进入的瞬间触发一次该事件
[virtual protected] void QWidget::enterEvent(QEvent *event);
// 1.6鼠标离开:当鼠标离开窗口的一瞬间,触发该事件,注意:只在离开的瞬间触发一次该事件// 2.键盘事件
// 2.1键盘按下:当键盘上的按键被按下了,该函数被自动调用,通过参数可以得知按下的是哪个键
[virtual protected] void QWidget::keyPressEvent(QKeyEvent *event);
// 2.2键盘释放:当键盘上的按键被释放了,该函数被自动调用,通过参数可以得知释放的是哪个键
[virtual protected] void QWidget::keyReleaseEvent(QKeyEvent *event);// 3.窗口绘制事件
// 3.1窗口重绘:当窗口需要刷新的时候,该函数就会自动被调用。窗口需要刷新的情景很多,比如:窗口大小发生变化,窗口显示等
[virtual protected] void QWidget::paintEvent(QPaintEvent *event);
// 3.2窗口关闭
[virtual protected] void QWidget::closeEvent(QCloseEvent *event);
// 3.3重置窗口大小
[virtual protected] void QWidget::resizeEvent(QResizeEvent *event);
- 重新事件处理函数
// 窗体头文件
#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:// 重写事件处理器函数void closeEvent(QCloseEvent* ev);private:Ui::MainWindow *ui;
};
// 窗体源文件
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QCloseEvent>
#include <QMessageBox>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::closeEvent(QCloseEvent *ev)
{QMessageBox::Button btn = QMessageBox::question(this, "关闭窗口", "您确定要关闭窗口吗?");if(btn == QMessageBox::Yes){// 接收并处理这个事件ev->accept();}else{// 忽略这个事件ev->ignore();}
}
多线程
- 主线程
默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新
- 子线程
子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
- 线程通信
主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制
QThread线程类
- 常用成员函数
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;// 判断子线程是不是在执行任务
bool QThread::isRunning() const;// 得到当前线程的优先级
Priority QThread::priority() const;// 设置Qt中线程的优先级
void QThread::setPriority(Priority priority);
// QThread::IdlePriority // --> 最低的优先级
// QThread::LowestPriority
// QThread::LowPriority
// QThread::NormalPriority
// QThread::HighPriority
// QThread::HighestPriority
// QThread::TimeCriticalPriority // --> 最高的优先级
// QThread::InheritPriority // --> 子线程和其父线程的优先级相同, 默认是这个// 退出线程, 停止底层的事件循环
void QThread::exit(int returnCode = 0);// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是;等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);
- 信号槽
// 和调用 exit() 效果是一样的 调用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate();// 线程中执行的任务完成了, 发出该信号
[signal] void QThread::finished();
// 开始工作之前发出这个信号
[signal] void QThread::started();
- 静态函数
// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs); // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs); // 单位: 秒
[static] void QThread::usleep(unsigned long usecs); // 单位: 微秒
- 任务执行函数
// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
run()是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承QThread,并且在子类中重写父类的run()方法,函数体就是对应的任务处理流程。另外,这个函数是一个受保护的成员函数,不能够在类的外部调用,如果想要让线程执行这个函数中的业务流程,需要通过当前线程对象调用槽函数start()启动子线程,当子线程被启动,这个run()函数也就在线程内部被调用了。
- 使用方法1
// 自定义线程类头文件
#ifndef MYTHREAD_H
#define MYTHREAD_H#include <QThread>class MyThread : public QThread
{Q_OBJECT
public:explicit MyThread(QObject *parent = nullptr);protected:void run();signals:// 自定义信号, 给主线程,传递数据(线程通信)void curNumber(int num);public slots:
};#endif // MYTHREAD_H
// 自定义线程类的实现文件
#include "mythread.h"
#include <QDebug>MyThread::MyThread(QObject *parent) : QThread(parent)
{}void MyThread::run()
{qDebug() << "当前线程对象的地址: " << QThread::currentThread();int num = 0;while(1){emit curNumber(num++);if(num == 10000000){break;}QThread::usleep(1);}qDebug() << "run() 执行完毕, 子线程退出...";
}
// 主窗体实现类
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
#include <QDebug>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);qDebug() << "主线程对象地址: " << QThread::currentThread();// 创建子线程MyThread* subThread = new MyThread;connect(subThread, &MyThread::curNumber, this, [=](int num){ui->label->setNum(num);});// 启动自定义线程subThread->start();
}MainWindow::~MainWindow()
{delete ui;
}
该使用方法的缺点:假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。
- 使用方法2
弥补使用方法1的缺陷,创建多个类似于业务流程类的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中moveToThread()就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。
// 业务流程类的头文件
#ifndef MYWORK_H
#define MYWORK_H#include <QObject>class MyWork : public QObject
{Q_OBJECT
public:explicit MyWork(QObject *parent = nullptr);// 工作函数:在子线程中实际执行任务的函数void working();signals:void curNumber(int num);
};#endif // MYWORK_H
// 业务流程类的实现
#include "mywork.h"
#include <QDebug>
#include <QThread>MyWork::MyWork(QObject *parent) : QObject(parent)
{}void MyWork::working()
{qDebug() << "当前线程对象的地址: " << QThread::currentThread();int num = 0;while(1){emit curNumber(num++);if(num == 10000000){break;}QThread::usleep(1);}qDebug() << "run() 执行完毕, 子线程退出...";
}
// 窗体实现函数:主线程创建并执行子线程
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>
#include "mywork.h"
#include <QDebug>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);qDebug() << "主线程对象的地址: " << QThread::currentThread();// 创建线程对象QThread* sub = new QThread;// 创建工作的类对象// 千万不要指定给创建的对象指定父对象如果指定了: QObject::moveToThread: Cannot move objects with a parent// MyWork* work = new MyWork(this); 错误MyWork* work = new MyWork;// 将工作的类对象移动到创建的子线程对象中work->moveToThread(sub);// 绑定信号槽connect(work, &MyWork::curNumber, this, [=](int num){ui->label->setNum(num);});// 启动线程sub->start();// 让工作的对象开始工作, 开始工作work.working();
}MainWindow::~MainWindow()
{delete ui;
}
线程池
- 线程池组成和原理
主要组成:任务队列:存储需要处理任务的队列(M个任务);工作线程(任务队列的消费者,N个线程);管理线程者(对任务队列和工作线程进行管理:接收、拒绝新任务,任务优先级排序;线程添加和销毁,1个线程)
- 线程池任务QRunnable(待执行的任务)
在Qt中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个QRunnable类型,因此在程序中需要创建子类继承QRunnable这个类,然后重写 run() 方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。
// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run();// 参数设置为 true: 任务对象处理完毕后会自动销毁;false: 任务对象处理完毕后需要手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
// 获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构
bool QRunnable::autoDelete() const;
- 线程池QThreadPool
Qt中的 QThreadPool 类管理了一组 QThreads, 里边还维护了一个任务队列。QThreadPool 管理和回收各个 QThread 对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。也可以单独创建一个 QThreadPool 对象使用。(一般情况下,我们不需要在Qt程序中创建线程池对象,直接使用Qt为每个应用程序提供的线程池全局对象即可)
// 获取和设置线程中的最大线程个数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);// 给线程池添加任务, 任务是一个 QRunnable 类型的对象,如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理
void QThreadPool::start(QRunnable * runnable, int priority = 0);// 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中
bool QThreadPool::tryStart(QRunnable * runnable);// 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const;// 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear();// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();
- 使用示例
// 任务类声明// 如果需要在这个任务中使用Qt的信号槽机制进行数据的传递就必须继承QObject这个类,如果不使用信号槽传递数据就可以不继承了,只继承QRunnable即可
class MyWork public QObject, :public QRunnable
{Q_OBJECT
public:explicit MyWork();~MyWork();void run() override;
}
// 任务类实现MyWork::MyWork() : QRunnable()
{// 任务执行完毕,该对象自动销毁setAutoDelete(true);
}
void MyWork::run()
{// 业务处理代码
}
// 窗口实现类,使用线程池MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);// 线程池初始化,设置最大线程池数QThreadPool::globalInstance()->setMaxThreadCount(4);// 添加任务MyWork* task = new MyWork;QThreadPool::globalInstance()->start(task);
}
得到线程池对象之后,调用start()方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。
网络编程
基于TCP套接字的网络通信
QTcpServer、QTcpSocket
基于WebSocket的双向网络通信
QWebSocket、QWebSocketServer
数据持久化
数据库插件与操作
JSON
类 | 说明 |
---|---|
QJsonDocument | 封装了一个完整的JSON文档,并可以从UTF-8编码的基于文本的表示以及Qt自己的二进制格式读取和写入该文档 |
QJsonArray | JSON数组是一个值列表。可以通过从数组中插入和删除QJsonValue来操作该列表 |
QJsonObject | JSON对象是键值对的列表,其中键是唯一的字符串,值由QJsonValue表示 |
QJsonValue | 该类封装了JSON支持的数据类型 |
程序发布与打包
发布
在VS中编译的可执行文件离开开发环境不能独立运行,要使它能够独立运行则需要进行发布(发布工具包含在上面下载安装Qt 的MSVC工具链中)将可执行程序放在某一空目录中,并执行下面的命令
# Qt6之前的版本
windeployqt --release --compiler-runtime {程序名}.exe
# Qt6版本
windeployqt6 --release --compiler-runtime {程序名}.exe
打包
NIS Edit、Inno Setup
C++语法补充
Lambda表达式
Lambda表达式是 C++ 11 最重要也是最常用的特性之一,是现代编程语言的一个特点,简洁,提高了代码的效率并且可以使程序更加灵活,Qt是完全支持c++语法的, 因此在Qt中也可以使用Lambda表达式。其本质就是一个匿名函数
- 捕获列表
列表项 | 解释 |
---|---|
[] | 不捕捉任何变量 |
[&] | 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获) |
[=] | 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获),拷贝的副本在匿名函数体内部是只读的 |
[=, &foo] | 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo |
[bar] | 按值捕获 bar 变量, 同时不捕获其他变量 |
[&bar] | 按引用捕获 bar 变量, 同时不捕获其他变量 |
[this] | 捕获当前类中的this指针;让lambda表达式拥有和当前类成员函数同样的访问权限;如果已经使用了 & 或者 =, 默认添加此选项 |
- 参数列表:和普通函数的参数列表一样
- opt 选项(可省略)
- mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
- xception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
- 返回值类型:
- 标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,可省略
- 函数体
// 定义:不会被调用
[](){qDebug() << "hello, 我是一个lambda表达式...";
};// 调用
[](){qDebug() << "hello, 我是一个lambda表达式...";
}();