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 /*主窗口基类*/ #includeQMainWindow /** * 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); }; #endifnote创建信号时最好贴合信号本身的含义此处笔者的项目中代表的意思是点击按钮后发送开始检测信号。2.1.2 槽的创建创建槽需要现在mainwindow.h里边进行声明然后在main.cpp里实现槽的定义。与信号不同声明槽必须写槽的定义否则会编译报错。它有以下特点1、槽可以时任何成员函数2、槽函数和信号的参数、返回值要保持一致。槽的创建如下所示#ifndef MAINWINDOW_H #define MAINWINDOW_H /*主窗口基类*/ #includeQMainWindow /** * 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 /*主窗口基类*/ #includeQMainWindow /*引入按钮控件库*/ #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(); }; #endif2、在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 /*主窗口基类*/ #includeQMainWindow /*引入文本编辑框*/ #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 QLableQLable提供了一种用于文本或图像显示的小部件其用法如下也是需要先在头文件里进行声明#ifndef MAINWINDOW_H #define MAINWINDOW_H /*主窗口基类*/ #includeQMainWindow /*引入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和QMessageBoxQFileDialog和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.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) // 注册元类型 qRegisterMetaTypeSensorData(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()));
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 /*主窗口基类*/ #includeQMainWindow /** * 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); }; #endifnote创建信号时最好贴合信号本身的含义此处笔者的项目中代表的意思是点击按钮后发送开始检测信号。2.1.2 槽的创建创建槽需要现在mainwindow.h里边进行声明然后在main.cpp里实现槽的定义。与信号不同声明槽必须写槽的定义否则会编译报错。它有以下特点1、槽可以时任何成员函数2、槽函数和信号的参数、返回值要保持一致。槽的创建如下所示#ifndef MAINWINDOW_H #define MAINWINDOW_H /*主窗口基类*/ #includeQMainWindow /** * 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 /*主窗口基类*/ #includeQMainWindow /*引入按钮控件库*/ #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(); }; #endif2、在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 /*主窗口基类*/ #includeQMainWindow /*引入文本编辑框*/ #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 QLableQLable提供了一种用于文本或图像显示的小部件其用法如下也是需要先在头文件里进行声明#ifndef MAINWINDOW_H #define MAINWINDOW_H /*主窗口基类*/ #includeQMainWindow /*引入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和QMessageBoxQFileDialog和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.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) // 注册元类型 qRegisterMetaTypeSensorData(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()));