21. Qt 样式表
21.1 定义和一般语法
- Qt 样式表(Qt Style Sheets, QSS)是 Qt 框架中的一种样式表语言,用于设置和定制控件的外观
- 样式表使用 CSS 语法,可以定义界面元素的属性、颜色、字体等
- 样式表主要用于 QWidget 及其子类(如 QPushButton、QLineEdit 等),还可以应用于整个应用程序或特定的控件,使得应用程序的界面可以与众不同
- Qt 还提供了 QStyle 类和 QStyleFactory 类,用于管理系统默认样式和自定义样式
- 一般语法
- 可以设置各种属性,如颜色、背景色、字体、边框等
QPushButton { background-color: blue; color: white; border: 2px solid black; } /* background-color:背景颜色 color:文本颜色 border:边框样式 font:字体设置 padding:内边距 margin:外边距 */
21.2 选择器
- 选择器:可以根据控件的类型、名称、状态等选择控件进行样式设置
21.3 子控件
- 对于一些组合的界面组件,需要对其子控件进行选择,例如 QComboBox 的下拉按钮,或 QSpinBox 的向上、向下按钮。通过选择器的子控件可以对这些界面元素进行显示效果控制
QComboBox::drop-down { image : url(:/images/images/down.bmp) }
- Qt 常用子控件
21.4 伪状态
- 选择器可以包含伪状态,使得样式规则只能应用于界面组件的某个状态,这就是一种条件应用规则
- 伪状态出现在选择器的后面,用一个冒号 : 隔开
/* 当鼠标移动到 OLineEdit 上方 (hover) 时,改变 QLineEdit 的背景色和前景色 */ QLineEdit:hover { background-color: black; color: yellow; }
- 可以对伪状态取反,方法是在伪状态前面加一个感叹号 !
QLineEdit:!read-only { background-color: rgb(235, 255, 251); }
- 伪状态可以串联使用,相当于逻辑与运算
QCheckBox:hover:checked { color: red; }
- 伪状态可以并联使用,相当于逻辑或运算
QCheckBox:hover, QCheckBox:checked { color: red; }
- Qt 常见伪状态
21.5 使用方式
- 方式一:在 QT Designer 设计界面中直接用样式表编辑器
- 这样设计的样式表会保存在窗口的 UI 文件里,创建窗口时会被自动应用
- 这样设计的样式表对于应用程序是固定的,而且为每个窗口都设计样式表,重复性工作量大
- 方式二:使用函数 setStyleSheet() 设置样式表
- 使用 QApplication::setStyleSheet() 函数可以为应用程序设置全局的样式表,使用 QWidget::setStyleSheet() 可以为一个窗口或一个界面组件设置样式
- 这样将样式表固定在程序中,无法实现界面主题效果的切换
qApp->setStyleSheet("QLineEdit { background-color: gray }")
- 方式三:通过程序给主窗口加载 .qss 文件
- 为了实现界面主题效果的切换,一般将样式表保存为后缀为 .qss 的纯文本文件,然后在程序中打开文件,读取文本内容,再调用函数 setStyleSheet() 应用样式
QFile file(":/qss/mystyle.qss"); file.open(QFile::ReadOnly); QString styleSheet = QString::fromLatinl(file.readAll()); qApp->setStyleSheet(styleSheet);
样式表使用注意事项
- 父控件采用样式表设置属性后,该属性会传递到其子控件上,除非子控件使用同样的方法修改属性
- 如:利用样式表设置父控件最小高度为 x,则子控件的最小高度也为 x,使用 setFixHeight() 修改也无法消除,只能通过样式表重新设置子控件的高度才有效,因此一般只对子控件使用样式设置
22. 修改 QPushButton 的大小,文字颜色等属性
- 方法一
- 使用 Qt Designer:可以使用 Qt Designer 来设置 QPushButton 的大小、文字颜色等属性
- 方法二
- 使用 Qt 的 API:例如 setMinimumSize()、setMaximumSize()、setStyleSheet() 等
- 方法三
- 使用 QSS 语言设置
QPushButton { background-color: blue; color: white; border: 2px solid black; }
23. Qt 窗口圆角如何实现
- 可以使用 Qt 的样式表实现
QWidget { border-radius: 10px; }
- 可以使用如下代码来应用样式表
QFile file("style.qss"); // style.qss 中已写好窗口圆角样式 file.open(QFile::ReadOnly); QString styleSheet = QLatin1String(file.readAll()); qApp->setStyleSheet(styleSheet);
24. Qt 事件机制
24.1 常见 QT 事件类型
-
按事件的来源,可以将事件划分为 3 类
- 自生事件(spontaneous event)
- 是由窗口系统产生的事件,例如 QKeyEvent 事件、QMouseEvent 事件
- 自生事件会进入系统队列,然后被应用程序的事件循环逐个处理
- 发布事件(postedevent)
- 是由 Qt 或应用程序产生的事件,例如 QTimer 定时器发生定时溢出时 Qt 会自动发布 QTimerEvent 事件
- 应用程序使用静态函数 QCoreApplication::postEvent() 产生发布事件,发布事件会进入 Qt 事件队列,然后由应用程序的事件循环进行处理
- 发送事件(sent event)
- 是由 Qt 或应用程序定向发送给某个对象的事件
- 应用程序使用静态函数 QCoreApplication::sendEvent() 产生发送事件,由对象的 event() 函数直接处理
- 自生事件(spontaneous event)
-
常见 QT 事件举例
- 键盘事件: 按键按下和松开
- 鼠标事件: 鼠标移动,鼠标按键的按下和松开
- 拖放事件: 用鼠标进行拖放
- 滚轮事件: 鼠标滚轮滚动
- 绘屏事件: 重绘屏幕的某些部分
- 定时事件: 定时器到时
- 焦点事件: 键盘焦点移动
- 进入和离开事件: 鼠标移入 widget 之内,或是移出
- 移动事件: widget 的位置改变
- 大小改变事件: widget 的大小改变
- 显示和隐藏事件: widget 显示和隐藏
- 窗口事件: 窗口是否为当前窗口
24.2 Qt 事件循环
- 事件循环(Event Loop)是 Qt 中用于处理事件和消息的机制,它负责接收、分发和处理应用程序接收到的事件,事件循环是一个持续运行的循环结构,不断地监听事件并将其传递给对应的事件接收者进行处理
- 主事件循环通过调用 QCoreApplication::exec() 启动,随着 QCoreApplication::exit() 结束,本地的事件循环可用利用 QEventLoop 构建
- 事件来源
- 事件可以来自多种来源,包括用户操作(如鼠标点击、键盘输入)、系统信号(如定时器事件、网络事件)、系统消息(如窗口管理、程序状态变化)等
- 事件分发
- 当应用程序接收到事件时,Qt 会将事件排入事件队列中。事件循环会不断地从事件队列中取出事件,并将其发送给对应的事件接收者(信号接收者)进行处理
- 事件处理
- 事件接收者可以是应用程序中的窗口部件(QWidget)、控件(QObject)、线程等。每个事件接收者都有一个事件处理函数(event handler),用于接收和处理特定类型的事件
- 事件类型
- Qt 中的事件分为系统事件和用户定义事件。系统事件由 Qt 框架负责生成和传递,用户定义事件则是由开发人员自行创建和处理
- 事件循环机制
- Qt 的事件循环是通过 QEventLoop 类实现的,它会在事件队列中有事件时进入循环,不断地等待和处理事件。事件循环的存在保证了应用程序能够响应用户输入、系统消息等各种事件
- 事件优先级
- 每个事件都有一个优先级,Qt 会按照事件优先级的顺序来处理事件。高优先级的事件会在低优先级事件之前得到处理
24.3 Qt 事件过滤
-
Qt 中的事件过滤器可以用于对某个对象的事件进行拦截和处理,事件过滤器是一个 QObject 对象,它可以安装到任何 QObject 派生类中,并且可以监听该对象的所有事件。当事件发生时,事件过滤器可以拦截并处理该事件,也可以将该事件转发给原始的事件接收者对象进行处理
-
事件过滤器的处理流程
- 1、创建一个 QObject 派生类,实现其 eventFilter() 函数,该函数会在事件发生时被调用
class MyEventFilter : public QObject { Q_OBJECT public: explicit MyEventFilter(QObject *parent = nullptr); protected: bool eventFilter(QObject *watched,QEvent *event) override; };
- 2、在需要监听的对象中,调用 installEventFilter() 函数安装事件过滤器
MyEventFilter *eventFilter = new MyEventFilter; QLabel *label = new QLabel("Hello,World!"); label->installEventFilter(eventFilter);
- 3、在 eventFilter() 函数中处理事件,可以通过判断事件类型和事件源来对事件进行不同的处理
bool MyEventFilter::eventFilter(QObject *watched,QEvent *event) { if (watched == label && event->type() == QEvent::MouseButtonPress) { QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); // 处理鼠标事件 return true; } return false; }
-
Qt 事件机制有几种级别的事件过滤:5 种级别的事件过滤处理办法,以功能从弱到强排列如下
- 1. 重载特定事件处理函数
- 最常见的事件过滤办法就是重载 mousePressEvent(),keyPressEvent() 这样的特定事件处理函数
- 2. 重载 event() 函数
- 通过重载 event() 函数,可以在事件被特定的事件处理函数处理之前(如 keyPressEvent())处理它,比如,当想改变 tab 键的默认动作时,一般要重载这个函数
- 在处理一些不常见的事件(如 LayoutDirectionChange)时,evnet() 也很有用,因为这些函数没有相应的特定事件处理函数
- 当重载 event() 函数时,需要调用父类的 event() 函数来处理不需要处理或是不清楚如何处理的事件
- 3. 在 Qt 对象上安装事件过滤器
- 安装事件过滤器有两个步骤:假设要用 A 来监视过滤 B 的事件
- 首先调用 B 的 installEventFilter(const QOject *obj),以 A 的指针作为参数,这样所有发往 B 的事件都将先由 A 的 eventFilter() 处理
- 然后,A 要重载 QObject::eventFilter() 函数,在 eventFilter() 中书写对事件进行处理的代码
- 4. 给 QAppliction 对象安装事件过滤器
- 一旦给 qApp(每个程序中唯一的 QApplication 对象)装上过滤器,那么所有的事件在发往任何其他的过滤器时,都要先经过当前这个 eventFilter()
- 在 debug 的时候,这个办法就非常有用,也常常被用来处理失效了的 widget 的鼠标事件,通常这些事件会被 QApplication::notify() 丢掉(在QApplication::notify() 中,是先调用 qApp 的过滤器,再对事件进行分析,以决定是否合并或丢弃)
- 5. 继承 QApplication 类,并重载 notify() 函数
- Qt 是用 QApplication::notify() 函数来分发事件的,想要在任何事件过滤器查看任何事件之前先得到这些事件,重载这个函数是唯一的办法
- 通常来说事件过滤器更好用一些,因为不需要去继承 QApplication 类,而且可以给 QApplication 对象安装任意个数的事件
- 1. 重载特定事件处理函数
25. Qt 对话框
- 文件对话框(QFileDialog)
- 用于打开和保存文件的对话框类,它可以让用户选择文件的路径和名称,并且支持多种文件格式的过滤
QString fileName = QFileDialog::getOpenFileName(this, "Open File", QDir::homePath(), "Text Files (*.txt)"); if(!fileName.isEmpty()) { QFile file(fileName); if(file.open(QIODevice::ReadOnly)) { // 读取文件内容 file.close(); } }
- 字体对话框(QFontDialog)
- 用于选择字体的对话框类,它可以让用户选择字体的名称、大小、颜色等属性
bool ok; QFont font = QFontDialog::getFont(&ok, QFont("Helvetica [Cronyx]", 10), this); if(ok) { // 应用选择的字体 setFont(font); }
- 输入对话框(QInputDialog)
- 用于输入文本、数字、列表等的对话框类,它可以让用户输入各种类型的数据,并且支持自定义对话框标题、提示信息等
QString text = QInputDialog::getText(this, "Input Dialog", "Enter your name:", QLineEdit::Normal, "", &ok); if(ok && !text.isEmpty()) { // 处理用户输入的文本 }
- 消息对话框(QMessageBox)
- 用于显示消息、提示、警告等的对话框类,它可以让用户选择不同的按钮选项,并且支持自定义对话框标题、提示信息等
QMessageBox::StandardButton reply; reply = QMessageBox::question(this, "Message Box", "Are you sure to quit?", QMessageBox::Yes | QMessageBox::No); if(reply == QMessageBox::Yes) { // 处理用户选择 }
// 显示一个警告框,提示用户用户名或密码错误 QMessageBox::warning(this, tr("警告"), tr("用户名或密码错误!"), QMessageBox::Yes);
26. Qt 中的 show 和 exec 区别
- show 和 exec 都是用来显示对话框的方法,它们的主要区别在于它们的运行机制和返回值
- show 方法是在当前线程中显示对话框并立即返回,该方法不会阻塞当前线程,因此当对话框显示后,程序会继续执行后续代码
- 如果在对话框显示期间需要执行一些其他操作,可以在对话框关闭事件中处理,或者使用 Qt 的事件循环机制(如 QEventLoop)来阻塞程序执行
- show 方法通常用于显示模态对话框和非模态对话框
- exec 方法是在当前线程中显示对话框,并且阻塞当前线程,直到用户关闭对话框为止
- 在执行 exec 方法后,程序会进入一个事件循环(QEventLoop),直到对话框关闭事件被触发。因此,如果需要在对话框关闭之前执行一些其他操作,需要在对话框关闭事件之前处理
- exec 方法通常用于显示模态对话框
如果需要在对话框显示期间执行一些其他操作,建议使用 show 方法,如果需要等待用户关闭对话框后再执行其他操作,建议使用 exec 方法
- show 方法是在当前线程中显示对话框并立即返回,该方法不会阻塞当前线程,因此当对话框显示后,程序会继续执行后续代码
27. Qt 多线程
27.1 Qt 多线程使用方法
- 方法一:继承 QThread 类
- 该方法需要定义一个新类,继承自 QThread 类,并重写 run() 函数,run() 函数中包含了需要在新线程中执行的代码,在主线程中创建该类的实例对象,调用 start() 函数启动新线程
class MyThread : public QThread { Q_OBJECT public: void run() override { // 在新线程中执行的代码 } }; MyThread thread; thread.start();
- 方法二:继承 QObject 类
- 使用 QThread 对象继承 QObject 类,该方法需要定义一个新类,继承自 QObject 类,并在该类中定义需要在新线程中执行的槽函数。在主线程中创建 QThread 对象,将新类的实例对象移动到该 QThread 对象所在的线程中,然后启动该 QThread 对象
class MyObject : public QObject { Q_OBJECT public slots: void doWork() { // 在新线程中执行的代码 } }; QThread thread; MyObject *obj = new MyObject(); obj->moveToThread(&thread); QObject::connect(&thread, &QThread::started, obj, &MyObject::doWork); thread.start();
- 方法三:继承 QRunnable 类
- 派生于 QRunnable,重写 run() 方法,在 run() 方法里处理其它任务,调用时需要借助 Qt 线程池
- 这种方法的缺点是:不能使用 Qt 的信号与槽机制,因为 QRunnable 不是继承自 QObject,但这种方法的好处是:可以让 QThreadPool 来管理线程,QThreadPool 会自动清理新建的 QRunnable 对象
27.2 Qt 线程同步方法
-
1. 互斥量(QMutex)
QMutex m_Mutex; m_Mutex.lock(); m_Mutex.unlock();
-
2. 互斥锁(QMutexLocker)
- 从声明处开始(在构造函数中加锁),出了作用域自动解锁(在析构函数中解锁)
QMutexLocker mutexLocker(&m_Mutex);
-
3. 等待条件(QWaitCondition)
QWaitCondtion m_WaitCondition; m_WaitConditon.wait(&m_muxtex, time); m_WaitCondition.wakeAll();
-
4. 读写锁(QReadWriteLock)
- 一个线程试图对一个加了读锁的互斥量进行上读锁,允许
- 一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞
- 一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞
- 一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞
- 适用情况:需要多次对共享的数据进行读操作的阅读线程
QReadWriterLock 与 QMutex 相似,除了它对 “read”,“write” 访问进行区别对待。它使得多个读者可以共时访问数据,使用 QReadWriteLock 而不是QMutex,可以使得多线程程序更具有并发性
-
5.信号量(QSemaphore)
- 有些互斥量(资源)的数量并不止一个,比如一个电脑安装了 2 个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候如果发现还有空闲的仍然可以申请到的,于是这个互斥量可以分为两部分,已使用和未使用
-
6.QReadLocker 便利类和 QWriteLocker 便利类对 QReadWriteLock 进行加解锁
27.3 Qthread 和 QtConcurrent 对比
- QThread 可以使用信号和槽机制,而 QtConcurrent 不支持
- QThread 可以设置线程优先级,而 QtConcurrent 不支持
- QThread 可以实现跨平台多线程,而 QtConcurrent 只能在支持 C++11 的平台上实现
- QThread 可以实现更复杂的多线程任务,而 QtConcurrent 只能实现简单的多线程任务
28. Qt 网络通信
28.1 Qt 中 TCP/UDP 网络通信流程
-
1. 服务器端的创建和监听
- 服务器端首先需要创建一个 QTcpServer 或 QUdpSocket 对象,用于监听客户端的连接或接收数据。创建时需要指定端口号和 IP 地址(如果有多个网卡,则需要指定监听的网卡地址)。然后调用 listen() 函数开始监听客户端的连接或数据包的到来
-
2. 客户端的连接或数据发送
- 客户端需要创建一个 QTcpSocket 或 QUdpSocket 对象,用于连接服务器或发送数据包
- 对于 TCP 通信,客户端需要调用 connectToHost() 函数连接服务器
- 对于 UDP 通信,客户端可以直接使用 writeDatagram() 函数发送数据包
-
3. 服务器端的响应和数据处理
- 当客户端连接到服务器或发送数据包时,服务器端会触发相应的信号(如 newConnection()、readyRead() 等),在相应的槽函数中进行响应和数据处理
- 对于 TCP 通信,服务器端需要在 newConnection() 槽函数中调用 nextPendingConnection() 函数获取客户端的 QTcpSocket 对象,然后在客户端的 QTcpSocket 对象上调用 read() 函数读取数据
- 对于 UDP 通信,服务器端可以直接在 readyRead() 槽函数中调用 readDatagram() 函数读取数据包
-
4. 客户端的数据接收和响应
- 当客户端连接到服务器或发送数据包时,客户端会触发相应的信号(如 connected()、readyRead() 等),在相应的槽函数中进行数据接收和响应
- 对于 TCP 通信,客户端需要在 connected() 槽函数中调用 write() 函数发送数据,然后在 readyRead() 槽函数中调用 read() 函数读取服务器端的响应数据
- 对于 UDP 通信,客户端可以直接在 readyRead() 槽函数中调用 readDatagram() 函数读取服务器端的响应数据包
-
5. 断开连接和清理资源
- 当通信完成或出现错误时,需要断开连接并清理资源
- 对于 TCP 通信,可以在客户端或服务器端的 QTcpSocket 对象上调用 disconnectFromHost() 函数或 close() 函数断开连接,并在 socketDisconnected() 槽函数中清理资源
- 对于 UDP 通信,不需要显式地断开连接,系统会自动管理资源
28.2 Qt 网络应用层编程
Qt4 以前的版本提供 QHttp 类用于构建 HTTP 客户端,提供 QFtp 类用于开发 FTP 客户端。从 Qt5 开始,已经不再分别提供 QHttp 类、QFtp 类,应用层的编程使用 QNetworkRequest、QNetworkReply 和 QNetworkAccessManager 这几个高层次的类,它们提供更加简单和强大的接口
- 网络请求由 QNetworkRequest 类来表示,作为与请求有关的信息的统一容器,在创建请求对象时指定的 URL 决定了请求使用的协议,目前支待 HTTP FTP 和本地文件 URLs 上传和下载
- QNetworkAceessManager 类用于协调网络操作,每当创建一个请求后,该类用来调度它,并发送信号以报告进度
- 而对于网络请求的应答则使用 QNetworkReply 类表示,它会在请求被完成调度时由 QNetworkAccessManager 类创建
29. Qt 文件操作
29.1 Qt 如何读写文件
- 读文件
// 首先使用 QFile 类打开要读取的文件 QFile file("test.txt"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; // 然后使用 QTextStream 类对文件进行读取 QTextStream in(&file); // 通过while循环,每次读取一行数据,然后进行处理 while (!in.atEnd()) { QString line = in.readLine(); // 处理每一行数据 } // 最后关闭文件 file.close();
- 写文件
// 首先使用 QFile 类打开要写入的文件 QFile file("test.txt"); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; // 然后使用 QTextStream 类对文件进行写入 QTextStream out(&file); // 通过向 QTextStream 对象中写入数据,可以将数据写入文件中 out << "Hello world" << endl; out << "Qt is awesome" << endl; // 最后关闭文件 file.close();
在进行文件读写操作时,需要确保文件路径正确,并且文件权限足够。同时,需要根据具体情况使用不同的打开模式,例如只读模式、只写模式、追加模式等
29.2 Qt 常用文件格式处理方式
- 1. 操作 INI 文件
- INI 文件是一种常用的配置文件格式,在 Qt 中可以通过 QSettings 类来读写 INI 文件
QSettings settings("myApp.ini", QSettings::IniFormat); // 写入配置项 settings.setValue("General/Name", "Tom"); settings.setValue("General/Age", 20); // 读取配置项 QString name = settings.value("General/Name").toString(); int age = settings.value("General/Age").toInt();
- 2. 操作 JSON 文件
- JSON 是一种轻量级数据交换格式,Qt 中可通过 QJsonDocument 类和 QJsonObject 类来读写 JSON 文件
// 读取 JSON 文件 QFile file("data.json"); if (file.open(QIODevice::ReadOnly)) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error == QJsonParseError::NoError && doc.isObject()) { QJsonObject obj = doc.object(); QString name = obj.value("name").toString(); int age = obj.value("age").toInt(); } file.close(); } // 写入 JSON 文件 QJsonObject obj; obj.insert("name", "Tom"); obj.insert("age", 20); QJsonDocument doc(obj); QFile file("data.json"); if (file.open(QIODevice::WriteOnly)) { file.write(doc.toJson()); file.close(); }
- 3. 操作 XML 文件
- XML 是一种常用的文本格式,用于表示结构化的数据,Qt 中可通过 QXmlStreamReader 类和 QXmlStreamWriter 类来读写 XML 文件
// 读取 XML 文件 QFile file("data.xml"); if (file.open(QIODevice::ReadOnly)) { QXmlStreamReader reader(&file); while (!reader.atEnd()) { reader.readNext(); if (reader.isStartElement() && reader.name() == "person") { QString name = reader.attributes().value("name").toString(); int age = reader.attributes().value("age").toInt(); // 处理读取到的数据 } } file.close(); } // 写入 XML 文件 QFile file("data.xml"); if (file.open(QIODevice::WriteOnly)) { QXmlStreamWriter writer(&file); writer.setAutoFormatting(true); writer.writeStartDocument(); writer.writeStartElement("persons"); writer.writeStartElement("person"); writer.writeAttribute("name", "Tom"); writer.writeAttribute("age", "20"); writer.writeEndElement(); writer.writeEndElement(); writer.writeEndDocument(); file.close(); }
30. QDataStream 和 QTextStream 区别
- Qt 中的数据流(QDataStream)和文件流(QTextStream)都是用于读写数据的流类,但它们有以下区别
- 数据类型
- 数据流(QDataStream)支持 Qt 中的所有基本数据类型和自定义类型,如 QString、QByteArray 等
- 文件流(QTextStream)只能读写文本数据,不支持二进制数据
- 数据格式
- 数据流(QDataStream)是二进制格式,可以直接读写二进制数据
- 文件流(QTextStream)是文本格式,只能读写文本数据,对于二进制数据需要进行编码和解码
- 数据编码
- 文件流(QTextStream)默认使用 Unicode 编码,可以通过设置不同的编码格式来读写不同的文本数据
- 数据流(QDataStream)不需要进行编码,它直接以二进制形式读写数据
- 应用场景
- 数据流(QDataStream)适用于读写二进制数据,例如读写文件、网络传输等场景
- 文件流(QTextStream)适用于读写文本数据,例如读写配置文件、日志文件等场景
- 数据类型