QT的C++接口基础用法
1. QT是什么
QT是一个跨平台的C++开发库,主要用于开发图形界面程序。简而言之,就是用来做UI界面的。下边笔者将介绍QT的基础用法,包括信号与槽机制、常用控件以及多线程。建议大家在学习QT时,先掌握QT的用法即可,不用深究原理。
2. QT的基础用法
2.1 信号与槽
信号(signal)指的是在特定情况下被发射的事件。比如说你在QT窗口里创建了一个按钮,点击这个按钮就会发送一个信号,然后界面窗口会对你所发送的信号进行响应。
槽(slot)就是对信号响应的函数,成为槽函数。它可以具有参数,也可以直接被调用。当信号发射时,所关联的槽函数会自动执行。
2.1.1 信号的创建
信号仅需要声明,不需要定义,一般在mainwindow.h里声明信号即可,如下所示:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
/*主窗口基类*/
#include<QMainWindow>
/**
* MainWindow - 主窗口
*/
class MainWindow : public QMainWindow
{
Q_OBJECT /*在使用信号与槽的类中,必须在类的定义中加入此宏!*/
public:
/* 构造函数声明*/
MainWindow(QWidget *parent = nullptr);
~MainWindow(); /* 析构函数声明 */
signals:
/* 声明一个信号,带参数,仅需声明,无需定义*/
void start_inspection(const recipe_config &recipe, const cv::Mat &gray);
};
#endif
note:创建信号时最好贴合信号本身的含义,此处笔者的项目中代表的意思是点击按钮后,发送开始检测信号。
2.1.2 槽的创建
创建槽需要现在mainwindow.h里边进行声明,然后在main.cpp里实现槽的定义。与信号不同,声明槽必须写槽的定义,否则会编译报错。它有以下特点:1、槽可以时任何成员函数;2、槽函数和信号的参数、返回值要保持一致。槽的创建如下所示:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
/*主窗口基类*/
#include<QMainWindow>
/**
* MainWindow - 主窗口
*/
class MainWindow : public QMainWindow
{
Q_OBJECT /*在使用信号与槽的类中,必须在类的定义中加入此宏!*/
public:
/* 构造函数声明*/
MainWindow(QWidget *parent = nullptr);
~MainWindow(); /* 析构函数声明 */
signals:
/* 声明一个信号,带参数,仅需声明,无需定义*/
void start_inspection(const recipe_config &recipe, const cv::Mat &gray);
public slots:
/* 声明一个槽函数,带参数,需要声明+定义*/
void request_inspection(const recipe_config &recipe, const cv::Mat &gray);
};
#endif
/*声明完成后需要在对应cpp文件里进行调用*/
void MainWindow::request_inspection(const recipe_config &recipe, const cv::Mat &gray)
{
/* 准备结果缓冲并调用核心检测引擎 */
inspection_result result{};
int code = run_inspection(recipe, gray, result);
/* 无论成功失败都发射信号,让 UI 侧统一处理 */
emit inspection_finished(code, result);
}
2.1.3 信号与槽的关联
信号与槽的关联通过connect函数来实现。其基本格式为:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
sender是发射对象的名称,signal() 是信号名称。信号可以看做是特殊的函 数,需要带括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,slot() 是槽函数 的名称,需要带括号,有参数时还需要指明参数。
而SIGNAL 和 SLOT 是 Qt 的宏,用于指明信号和槽,并将它们的参数转换为相应的字符 串。note:当信号和槽函数带有参数时,在 connect()函数里,要写明参数的类型,但可以不写参数名称。用法如下所示:
/*当信号和槽函数带有参数时,在连接函数里边要写明参数的类型,但可以不写参数名称*/
connect(worker_, SIGNAL(inspection_finished(int, inspection_result)),
this, SLOT(on_inspection_finished(int, inspection_result)));
如上图的意思是:假设worker_是按钮,代表的意思就是按钮点击后,开始发送inspection_finished信号,然后QT界面窗口会执行槽函数on_inspection_finished。其中,this代表的是主窗口MainWindow(C++中的this指针指向实例化的对象本身)。
2.2 QT常用控件
2.2.1 按钮QPushButton
这是一个按钮控件,通常用于创建一个按钮,点击后,主界面做出相应的响应,用法如下。
1、先在MainWindow.h里引入按钮库并声明按钮对象,代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
/*主窗口基类*/
#include<QMainWindow>
/*引入按钮控件库*/
#include <QPushButton>
/**
* MainWindow - 主窗口
*/
class MainWindow : public QMainWindow
{
Q_OBJECT /*在使用信号与槽的类中,必须在类的定义中加入此宏!*/
public:
/* 构造函数声明*/
MainWindow(QWidget *parent = nullptr);
~MainWindow(); /* 析构函数声明 */
private:
/*声明一个QPushButton对象*/
QPushButton *load_image_button_; /* 加载图片按钮 */
private slots:
/* 加载图片按钮点击槽 */
void on_load_image_clicked();
};
#endif
2、在MainWindow.cpp的MainWindow构造函数中实例化按钮对象并连接信号与槽,如下所示:
/*实例化按钮对象*/
load_image_button_ = new QPushButton(QString::fromUtf8("加载图片"), central);
/*连接信号与槽*/
connect(load_image_button_, SIGNAL(clicked()),
this, SLOT(on_load_image_clicked())); // 连接加载图片按钮点击信号和槽函数
2.2.2 文本编辑框QLineEdit
文本编辑框的意思就是提供一个可以输入信息的文本框,用法如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
/*主窗口基类*/
#include<QMainWindow>
/*引入文本编辑框*/
#include <QLineEdit>
/**
* MainWindow - 主窗口
*/
class MainWindow : public QMainWindow
{
Q_OBJECT /*在使用信号与槽的类中,必须在类的定义中加入此宏!*/
public:
/* 构造函数声明*/
MainWindow(QWidget *parent = nullptr);
~MainWindow(); /* 析构函数声明 */
private:
/* 操作员输入框 */
QLineEdit *operator_edit_;
/* 初始化菜单栏(操作员输入) */
void setup_menu_bar();
};
#endif
如上图,也是需要在头文件里进行声明;接下来在对应的cpp文件里进行实例化对象,以及构建文本框,如下所示:
/**
* setup_menu_bar - 构建菜单栏(操作员输入)
*/
void MainWindow::setup_menu_bar()
{
/*QMenu是QT的菜单类,此处先通过menuBar()获取菜单栏,再添加一个"操作员"菜单 */
QMenu *operator_menu = menuBar()->addMenu(QString::fromUtf8("操作员"));
/* 把 QLineEdit 嵌入菜单栏里作为操作员录入入口 */
/*QWidgetAction是QT的控件动作类,只有通过它才可以将控件嵌入菜单栏
*一般模板为先调用setDefaultWidget()方法,将控件设置为默认小部件
*然后将QWidgetAction对象添加到菜单栏中
*/
QWidgetAction *action = new QWidgetAction(this);
operator_edit_ = new QLineEdit(this);
operator_edit_->setPlaceholderText(QString::fromUtf8("输入操作员名称"));//设置占位文本为“输入操作员名称”
operator_edit_->setText(QString::fromUtf8("unknown"));//设置默认文本为“unknown”
operator_edit_->setFixedWidth(200);//设置固定宽度为200
action->setDefaultWidget(operator_edit_);//将操作员编辑框设置为默认小部件
operator_menu->addAction(action); /* 将操作员编辑框添加到操作员菜单 */
}
2.2.3 QLable
QLable提供了一种用于文本或图像显示的小部件,其用法如下,也是需要先在头文件里进行声明:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
/*主窗口基类*/
#include<QMainWindow>
/*引入QLable*/
#include <QLabel>
/**
* MainWindow - 主窗口
*/
class MainWindow : public QMainWindow
{
Q_OBJECT /*在使用信号与槽的类中,必须在类的定义中加入此宏!*/
public:
/* 构造函数声明*/
MainWindow(QWidget *parent = nullptr);
~MainWindow(); /* 析构函数声明 */
private:
/* 左侧图像显示区 */
QLabel *image_label_;
/* 初始化中央控件与布局 */
void setup_ui();
};
#endif
下边需要在对应CPP文件里进行实例化对象以及设置控件,如下所示:
void MainWindow::setup_ui()
{
/* 创建中央 QWidget 和主水平布局 */
QWidget *central = new QWidget(this);
QHBoxLayout *main_layout = new QHBoxLayout(central);
/* 左侧图像区:至少 640x480,带边框 */
image_label_ = new QLabel(central);
image_label_->setMinimumSize(640, 480); // 设置图像标签的最小尺寸为 640x480
image_label_->setAlignment(Qt::AlignCenter); // 设置图像标签的对齐方式为居中
image_label_->setStyleSheet(QString::fromUtf8("border: 1px solid gray;")); // 设置图像标签的样式表为带边框
image_label_->setText(QString::fromUtf8("未加载图片")); // 设置图像标签的文本为“未加载图片”
main_layout->addWidget(image_label_, 3); // 将图像标签加入主布局,并指定伸缩因子为3,表示该区域占总宽度的3/4
/* 右侧按钮 + 结果区 */
QVBoxLayout *right_layout = new QVBoxLayout(); // 创建右侧垂直布局,用于容纳功能按钮和结果显示区
/*其他代码省略....*/
}
2.2.4 布局管理
如上图所示,QHBoxLayout(水平布局)、QVBoxLayout(垂直布局),简单来说,水平布局就是将控件平着放;垂直布局就是将控件垂着着放。
/* 右侧按钮 + 结果区 */
QVBoxLayout *right_layout = new QVBoxLayout(); // 创建右侧垂直布局,用于容纳功能按钮和结果显示区
/* 四个功能按钮 */
load_image_button_ = new QPushButton(QString::fromUtf8("加载图片"), central);
run_inspection_button_ = new QPushButton(QString::fromUtf8("执行检测"), central);
view_history_button_ = new QPushButton(QString::fromUtf8("查看历史"), central);
switch_recipe_button_ = new QPushButton(QString::fromUtf8("切换配方"), central);
/* 结果显示区:支持换行、左上对齐、浅灰边框 */
result_label_ = new QLabel(central);
result_label_->setWordWrap(true); // 设置结果标签支持换行
result_label_->setAlignment(Qt::AlignTop | Qt::AlignLeft); // 设置结果标签的对齐方式为左上对齐
result_label_->setText(QString::fromUtf8("等待检测...")); // 设置结果标签的文本为“等待检测...”
result_label_->setStyleSheet(QString::fromUtf8("border: 1px solid lightgray; padding: 6px;")); // 设置结果标签的样式表为浅灰边框
result_label_->setMinimumHeight(200); // 设置结果标签的最小高度为 200
result_label_->setMinimumHeight(200); // 设置结果标签的最小高度为 200
/* 按顺序加入右侧垂直布局 */
right_layout->addWidget(load_image_button_);
right_layout->addWidget(run_inspection_button_);
right_layout->addWidget(view_history_button_);
right_layout->addWidget(switch_recipe_button_);
right_layout->addWidget(result_label_, 1);
2.2.5 QTableWidget
代表表格控件,用法如下:
#ifndef HISTORY_DIALOG_HPP
#define HISTORY_DIALOG_HPP
#include <QDialog> /* QDialog 基类 */
#include <QTableWidget> /* 表格控件 */
#include <string> /* std::string */
/**
* HistoryDialog - 历史记录对话框
*/
class HistoryDialog : public QDialog
{
Q_OBJECT
public:
/* 构造函数:db_path 是 SQLite 数据库路径 */
HistoryDialog(const std::string &db_path, QWidget *parent = nullptr);
~HistoryDialog();
private:
/* 从数据库加载最多 limit 条记录填入表格 */
void load_records(int limit);
std::string db_path_; /* 数据库路径 */
QTableWidget *table_; /* 记录表格 */
};
#endif /* HISTORY_DIALOG_HPP */
可以看到也是需要先引入相应的表格控件头文件,并且在头文件里声明表格控件对象。然后在对应CPP文件里构造表格控件,用法示例如下:
HistoryDialog::HistoryDialog(const std::string &db_path, QWidget *parent)
: QDialog(parent), // 调用父类构造函数
db_path_(db_path), // 初始化数据库路径成员变量
table_(nullptr) // 初始化表格成员变量为 nullptr
{
/* 设置窗口标题与几何信息 */
this->setWindowTitle(QString::fromUtf8("检测历史记录"));
this->setGeometry(200, 200, 900, 500);
/* 构造表格控件并设置 7 列表头 */
table_ = new QTableWidget(this);
table_->setColumnCount(7);
/* 准备表头文本 */
/* QStringList是QT中专门用于存储多个字符串的容器,使用
* <<操作符向容器中追加字符串元素,如第0列为时间
*/
QStringList headers;
headers << QString::fromUtf8("时间")
<< QString::fromUtf8("产品")
<< QString::fromUtf8("结果")
<< QString::fromUtf8("失败原因")
<< QString::fromUtf8("操作员")
<< QString::fromUtf8("配方版本")
<< QString::fromUtf8("图片路径");
table_->setHorizontalHeaderLabels(headers); // 调用表格控件方法来设置表头文本
/* 最后一列自适应填充剩余宽度,避免图片路径被截断 */
table_->horizontalHeader()->setStretchLastSection(true);
/* 只读表格,禁止任何编辑 */
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
/* 构造对话框布局并装入表格 */
QVBoxLayout *layout = new QVBoxLayout(this); // 构造对话框布局
layout->addWidget(table_); // 装入表格
}
2.2.6 QPixmap和QImage
这是QT提供的图像处理类,QPixmap更侧重于显示(类似于pdf),而QImage更侧重于图像处理(类似于word,方便编辑)。一般将QImage用于转换图像到QT显示,而QPixmap用于显示图像。用法如下:
/* 转 QImage 并按 Label 尺寸等比缩放 */
QImage image = mat_to_qimage(display);
QPixmap pixmap = QPixmap::fromImage(image).scaled(
image_label_->size(), //参数1:目标尺寸
Qt::KeepAspectRatio, //参数2:缩放模式,保持宽高比
Qt::SmoothTransformation); //参数3:缩放质量,平滑转换
image_label_->setPixmap(pixmap); // 设置图像标签的像素图,显示图像
2.2.7 QFileDialog和QMessageBox
QFileDialog和QMessageBox分别代表文件对话框和消息框,QFileDialog用于打开文件或保存文件,打开文件时用getOpenFileName(参数为:父窗口、对话框标题、默认路径、过滤器),保存文件时用getSaveFileName(参数为:父窗口、对话框标题、默认路径、过滤器);
QMessageBox用于显示消息框,常用函数有QMessageBox::information(表示信息提示蓝色图标)、QMessageBox::warning(表示警告提示黄色图标)、QMessageBox::critical(表示错误提示红色图标)。用法如下:
void MainWindow::on_load_image_clicked()
{
/* 弹出选文件对话框,限制常见图片格式 */
QString file = QFileDialog::getOpenFileName(this,
QString::fromUtf8("选择待检测图片"),
QString(),
QString::fromUtf8("图片文件 (*.png *.jpg *.bmp)"));
/* 用户取消选择则直接返回 */
if (file.isEmpty())
{
return;
}
/* 读图,失败则弹警告 */
cv::Mat bgr = cv::imread(file.toStdString(), cv::IMREAD_COLOR);/*将Qstring类型转换为std string*/
if (bgr.empty())
{
QMessageBox::warning(this,
QString::fromUtf8("读取图片失败"),
QString::fromUtf8("无法读取该文件,请确认格式正确。"));
return;
}
}
2.2.8 QstatusBar
代表状态栏,一般存在于QT主窗口的底部。用法如下:
/* 初始状态栏提示 */
statusBar()->showMessage(QString::fromUtf8("就绪"));
2.2.9 QComboBox
该控件属于下拉框控件,当点击下拉框时,会弹出选择项,选择一个列表项后,会发送一个currentIndexChanged信号,这是一个内置信号,即可调用所连接的槽函数执行。用法如下所示:
resolution_combo_ = new QComboBox(image_label_);
resolution_combo_->addItem("640x480");/* 下拉框的第一项,索引为0 */
resolution_combo_->addItem("1280x720");/* 下拉框的第二项,索引为1 */
resolution_combo_->setFixedWidth(130);
/* 半透明深色背景,白色文字,与 UI 主题一致 */
resolution_combo_->setStyleSheet(
"QComboBox { background-color: rgba(43,43,43,200); color: #ffffff; "
"border: 1px solid #555555; padding: 4px 8px; border-radius: 4px; }"
"QComboBox QAbstractItemView { background-color: #2b2b2b; color: #ffffff; "
"selection-background-color: #0078d4; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: none; border-left: 5px solid transparent; "
"border-right: 5px solid transparent; border-top: 6px solid #ffffff; "
"margin-right: 6px; }");
/* 先连接信号,再设索引;设索引前 blockSignals 避免触发冗余重开 */
connect(resolution_combo_, SIGNAL(currentIndexChanged(const QString &)),
this, SLOT(on_resolution_changed(const QString &)));
resolution_combo_->blockSignals(true);
resolution_combo_->setCurrentIndex(0); /* 默认 640x480,blockSignals 阻止信号发射 */
resolution_combo_->blockSignals(false);
/* 定位到 image_label_ 左上角,偏移 8px */
resolution_combo_->move(8, 8);
resolution_combo_->show();
2.2.10 QPlainTextEdit
QPlainTextEdit 类提供了一个用于编辑和显示纯文本的小部件,常用于显示多行文本或简单 文本。用法如下:
/* 日志内容显示区:只读、等宽字体、深色背景 */
log_text_ = new QPlainTextEdit(this);
log_text_->setReadOnly(true);/*设置为只读*/
log_text_->setFont(QFont(QString::fromUtf8("Monospace"), 10));/*设置等宽字体*/
log_text_->setStyleSheet(QString::fromUtf8(
"QPlainTextEdit {"
" background-color: " THEME_IMAGE_AREA ";"
" color: " THEME_TEXT ";"
" border: 1px solid " THEME_BORDER ";"
"}"));
log_text_->setPlainText(QString::fromUtf8("Log file not found"));
layout->addWidget(log_text_, 1);
/*省略部分代码*/
for (int i = 0; i < all_lines.size(); ++i)
{
const QString &line = all_lines[i]; /* 循环条件已保证边界,无需 at() 额外检查 */
/* 根据日志级别确定文字颜色 */
QTextCharFormat fmt;
if (line.contains("[ERR]"))
{
/* 错误日志用红色 */
fmt.setForeground(QColor(THEME_FAIL));
}
else if (line.contains("[WARN]"))
{
/* 警告日志用橙色 */
fmt.setForeground(QColor(THEME_ORANGE));
}
else if (line.contains("[INFO]"))
{
/* 信息日志用白色 */
fmt.setForeground(QColor(THEME_TEXT));
}
else if (line.contains("[DEBUG]"))
{
/* 调试日志用灰色 */
fmt.setForeground(QColor("#888888"));
}
else
{
/* 无法识别级别时用默认白色 */
fmt.setForeground(QColor(THEME_TEXT));
}
/* 用 QTextCursor 插入着色文本 */
log_text_->moveCursor(QTextCursor::End);
log_text_->textCursor().insertText(line + "\n", fmt);/*逐行插入、逐行显示*/
}
2.2.11 QStackedWidget
QStackedWidget 继承 QFrame。QStackedWidget 类提供了一个小部件堆栈,其中一次只能 看到一个小部件,一般用于做页面切换,也就是你屏幕上一个页面放不下,你还有多余的页面想要放在屏幕上,就可以用此组件来做页面切换,具体用法如下:
/* ---- 新增:页面堆叠容器 ---- */
stacked_widget_ = new QStackedWidget(central);
/* 创建 Main 页面容器,将现有内容移入 */
main_page_ = new QWidget();
/* 主水平布局:左侧图像区 + 右侧按钮区 */
QHBoxLayout *main_layout = new QHBoxLayout(main_page_);
/* 左侧图像区:最小 400x320(适配 1024x600 屏幕布局预算),带边框 */
image_label_ = new QLabel(main_page_);
stacked_widget_->addWidget(main_page_);
从上图可以看出,main_page就是主页面,如果还想增加其他页面,就需要重复执行stacked_widget_->addWidget(xxx);这里需要注意的是:QStackedWidget 要求它管理的每个页面必须是 QWidget 或其子类,因此main_page赋值成指向QWidget对象的指针。QWidget 类是所有用户界面对象的基类, 是用户界面的基本单元。它本身就是一个"矩形区域",能接收事件、绘制自身、管理子控件的布局。所以它既可以是一个独立的顶层窗口(不指定 parent 时),也可以是一个嵌套在其他控件里的子区域。
2.2.12 QFile读写文本
QFile 类提供了读取和写入文件的接口。用法如下:
/* 用 QFile 打开日志文件 */
QFile file(QString::fromUtf8(VISION_LOG_FILE_PATH));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
/* 文件不存在或无法打开时记录错误并显示占位文字 */
LOG_PRINT_ERR("log_page", "cannot open log file: %s\n", VISION_LOG_FILE_PATH);
log_text_->setPlainText(QString::fromUtf8("Log file not found"));
return;
}
2.3 QT多线程
为什么要使用多线程呢?因为如在单线程中,操作都是按顺序执行的,如果UI界面内某个点击按钮的操作是比较耗时的,就会发现点击后没有响应,界面卡住了,必须等到耗时结束后才能恢复。
为了解决这种问题,就要使用多线程方法,而QT中实现多线程的核心是存在QTHread线程类,其有两种多线程方法,一种是继承QThread的run函数,另一种是将继承QObject的类转移到一个线程里,官方主要推荐第二种用法,因此笔者将以第二种用法为准进行介绍。
先来看用法示例,在做讲解:
void MainWindow::setup_worker_thread()
{
/* 工作线程归 MainWindow 所有,但 Worker 对象不能挂父级,否则 moveToThread 会失败 */
worker_thread_ = new QThread(this);
worker_ = new InspectionWorker();
worker_->moveToThread(worker_thread_);//将 worker_ 移动到 worker_thread_ 线程,worker_的槽函数将在该线程中执行
/* 线程结束后自动删除 Worker 对象 */
connect(worker_thread_, SIGNAL(finished()),
worker_, SLOT(deleteLater()));
/* 主窗口信号 -> Worker 槽:按 recipe 和图像发起检测请求 */
/*当主窗口点击执行检测时会发送检测信号,worker_(在工作线程中)接收到信号后会调用request_inspection槽函数进行检测*/
connect(this, SIGNAL(start_inspection(recipe_config, cv::Mat)),
worker_, SLOT(request_inspection(recipe_config, cv::Mat)));
/* Worker 信号 -> 主窗口槽:检测完成回传结果 */
/*当信号和槽函数带有参数时,在连接函数里边要写明参数的类型,但可以不写参数名称*/
connect(worker_, SIGNAL(inspection_finished(int, inspection_result)),
this, SLOT(on_inspection_finished(int, inspection_result)));
/* 启动工作线程 */
/*调用start方法创建线程worker_thread_,并执行该线程,该线程会调用run方法,
*run方法中会调用exec()函数,该函数会启动事件循环,即检测到信号时才会唤醒,否则处于阻塞态。
*/
worker_thread_->start();
}
如上图的代码是运行在UI线程中的, worker_thread_是实例化的线程对象,worker_是实例化的InspectionWorker对象,此处的InspectionWorker就是继承自QObject的派生类。然后接下来调用moveToThread方法将worker_对象移动到所创建的工作线程(worker_thread_)中,代表之后worker_对象的槽函数将在新的工作线程中执行。
比如上图中的第二个connect函数:当UI线程对象发送开始检测信号后,处在工作线程的对象worker_会在工作线程里执行槽函数on_inspection_finished。最后调用工作线程的start方法来创建工作线程并执行。具体来说,start方法内部会调用pthread_create()创建线程,然后执行所创建的线程,最后在该线程中启动事件循环。
2.3.1 跨越信号和槽
像上述所说的,发送信号的对象this和接收对象worker_不在同一线程中时,这种情况就称为跨越信号和槽。其工作机制是:主线程发送信号后,QT会将信号的参数拷贝,并封装成事件,然后将事件发送到工作线程的事件队列里边;等待工作线程执行事件循环取出该队列,才会去调用接收对象的槽函数进行执行。
其中,事件循环指的是底层维护一个事件队列,无事件时线程调用 epoll_wait 进入阻塞,内核将其移出就绪表、加入等待表,此时放弃CPU控制权;当其他线程投递事件时会触发内部唤醒管道,内核立即把线程移回就绪表,恢复 TASK_RUNNING 状态,随后事件循环取出队列中的事件并执行。
了解工作原理之后,下边说一个多线程调度的示例帮助理解,如:
1、假设主线程有一个按钮,点击后发射信号让工作线程开始计算:
2、程序启动,主线程进入 app.exec()(代表事件循环),等待事件。
3、工作线程启动后进入 exec(),等待事件。
4、两者都阻塞,CPU 空闲。
5、用户点击按钮 → 主线程被唤醒,执行按钮对应的槽函数,该槽函数发射信号给工作线程。
6、工作线程的信号被包装为事件,投递到工作线程的事件队列,工作线程的 exec() 被唤醒,处理该事件,执行 Worker 的槽(如 doWork())。
7、工作线程耗时计算期间,完全不需要事件循环,直接占用 CPU 执行 doWork()。
8、计算完成后,Worker 发射 resultReady 信号,该信号被投递到主线程的事件队列,工作线程继续回到事件循环 exec() 等待新事件(如果没有新事件,再次阻塞)。
9、主线程的事件循环被 resultReady 事件唤醒,调用主线程槽函数更新界面。
10、更新完成后,主线程继续回到事件循环,若再无事件,则再次阻塞。
2.3.2 声明和注册元类型
上小节中跨越信号和槽的工作机制中讲到,主线程发送信号后,QT会将信号的参数拷贝,并封装成事件。但是,当QT在拷贝参数时如果碰到你自定义的数据结构类型,它是没办法认识的,因此需要采用Q_DECLARE_METATYPE 和 qRegisterMetaType 进行注册,否则QT会报错。用法如下:
// 定义一个结构体
struct SensorData
{
int id;
double temperature;
QString name;
};
// 声明元类型
Q_DECLARE_METATYPE(SensorData)
// 注册元类型
qRegisterMetaType<SensorData>("SensorData");
2.4 QTimer
它是QT的一个定时器类,但不是硬件定时器。作用是定时一段时间后,定时器到期会唤醒事件循环,事件循环发现该定时器到期,便直接触发 QTimer 的事件处理,发射 timeout() 信号,并同步调用连接的槽函数。用法如下:
#include <QTimer>
8
9
10 class MainWindow : public QMainWindow
11 {
12 Q_OBJECT
13
14 public:
15 MainWindow(QWidget *parent = nullptr);
16 ~MainWindow();
17
18 private:
19 /* 声明QLCDNumber对象 */
20 QLCDNumber *lcdNumber;
21
22 /* 声明QTimer对象 */
23 QTimer *timer;
/*省略......*/
/*在对应的构造函数中*/
timer = new QTimer(this);
25 /* 设置定时器1000毫秒发送一个timeout()信号 */
26 timer->start(1000);
27
28 /* 信号槽连接 */
29 connect(timer, SIGNAL(timeout()), this,
30 SLOT(timerTimeOut()));
2.5 QSocketNotifier
用法如下:
rigger_notifier_ = new QSocketNotifier(trigger_fd_, QSocketNotifier::Read, this);
connect(trigger_notifier_, SIGNAL(activated(int)), this, SLOT(on_trigger_fd_ready()));
当创建QSocketNotifier对象时,会创建一个文件描述符监视器,用于监视指定文件描述符的读事件(具体来讲创建socket对象时会向QT事件循环注册一个事件,调用linux的poll()来监听这个fd)。当文件描述符有数据可读时,QSocketNotifier会发出activated信号,程序即可响应该信号并执行相应的操作。
2.6 invokeMethod函数和lmbda表达式
用法如下:
/* 启动后 5 秒延迟自检:检查 version.txt 和 demo_recipe.json 是否存在,
* 失败时自动回滚到 .bak 版本 ,single shot函数表示5秒后执行大括号的内容
*invokeMethod函数表示跨线程调用,调用此函数时,执行device_agent_所在线程
* 的run_self_check方法
*/
QTimer::singleShot(5000, this, [this]() {
QMetaObject::invokeMethod(device_agent_, "run_self_check",
Qt::QueuedConnection);
});
/* 线程退出后在 UI 线程安全释放 worker 和线程对象,
* worker 按值捕获而非通过 this 访问,确保始终释放正确对象
*注意下边这个连接函数并未定义槽函数,代表是lambda表达式,无需单独声明定义
*/
connect(thread, &QThread::finished, this, [worker, thread]()
{
if (worker != nullptr)
{
delete worker;
}
thread->deleteLater(); /* 用 deleteLater 避免父对象 children 列表出现悬空指针 */
});
lmbda表达式是指一种精简的表达式,比如connect函数的用法,需要声明和定义一个槽函数。但在lmbda表达式中,直接不需要函数名字,自然也无需定义和声明,直接加上函数的参数以及写函数体即可,这就是lmbda表达式;而invokeMethod函数是一种跨越信号和槽机制的函数,指的是执行某个线程对象所在线程的某方法。
2.7 按钮控件/下拉框控件原理解析
当按钮控件按下、或者是下拉框控件选择列表项时,都会发送一个QT内置信号,如clicked或者是currentIndexChanged。那为什么在屏幕上点击了一下控件,就会发送这个信号呢?原理是什么?
其实是由于Linux操作系统,会将所有的输入事件封装成一个input子系统,当点击按钮时,input子系统会读取原始坐标事件,然后Qt QPA 插件读取并翻译为 QEvent → QPushButton 控件自己判断这是不是点击 → 是的话才 emit clicked()
更多推荐

所有评论(0)