QT的C++接口基础用法

1. QT是什么

QT是一个跨平台的C++开发库,主要用于开发图形界面程序。简而言之,就是用来做UI界面的。下边笔者将介绍QT的基础用法,包括信号与槽机制、常用控件以及多线程。建议大家在学习QT时,先掌握QT的用法即可,不用深究原理。

2. QT的基础用法

2.1 信号与槽

**信号(signal)**指的是在特定情况下被发射的事件。比如说你在QT窗口里创建了一个按钮,点击这个按钮就会发送一个信号,然后界面窗口会对你所发送的信号进行响应。

**槽(slot)**就是对信号响应的函数,成为槽函数。它可以具有参数,也可以直接被调用。当信号发射时,所关联的槽函数会自动执行。

2.1.1 信号的创建

信号仅需要声明,不需要定义,一般在mainwindow.h里声明信号即可,如下所示:

cpp 复制代码
#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、槽函数和信号的参数、返回值要保持一致。槽的创建如下所示:**

cpp 复制代码
#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函数来实现。其基本格式为:

cpp 复制代码
connect(sender, SIGNAL(signal()), receiver, SLOT(slot())); 

sender是发射对象的名称,signal() 是信号名称。信号可以看做是特殊的函 数,需要带括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,slot() 是槽函数 的名称,需要带括号,有参数时还需要指明参数。

而SIGNAL 和 SLOT 是 Qt 的宏,用于指明信号和槽,并将它们的参数转换为相应的字符 串。note:当信号和槽函数带有参数时,在 connect()函数里,要写明参数的类型,但可以不写参数名称。用法如下所示:

cpp 复制代码
    /*当信号和槽函数带有参数时,在连接函数里边要写明参数的类型,但可以不写参数名称*/
    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里引入按钮库并声明按钮对象,代码如下:

cpp 复制代码
#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构造函数中实例化按钮对象并连接信号与槽,如下所示:

cpp 复制代码
/*实例化按钮对象*/
load_image_button_ = new QPushButton(QString::fromUtf8("加载图片"), central);

/*连接信号与槽*/
 connect(load_image_button_, SIGNAL(clicked()),
            this, SLOT(on_load_image_clicked()));     // 连接加载图片按钮点击信号和槽函数

2.2.2 文本编辑框QLineEdit

文本编辑框的意思就是提供一个可以输入信息的文本框,用法如下:

cpp 复制代码
#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文件里进行实例化对象,以及构建文本框,如下所示:

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提供了一种用于文本或图像显示的小部件,其用法如下,也是需要先在头文件里进行声明:

cpp 复制代码
#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文件里进行实例化对象以及设置控件,如下所示:

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(垂直布局),简单来说,水平布局就是将控件平着放;垂直布局就是将控件垂着着放。

cpp 复制代码
    /* 右侧按钮 + 结果区 */
    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

代表表格控件,用法如下:

cpp 复制代码
#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文件里构造表格控件,用法示例如下:

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用于显示图像。用法如下:

cpp 复制代码
/* 转 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(表示错误提示红色图标)。用法如下:

cpp 复制代码
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主窗口的底部。用法如下:

cpp 复制代码
    /* 初始状态栏提示 */
    statusBar()->showMessage(QString::fromUtf8("就绪"));

2.3 QT多线程

为什么要使用多线程呢?因为如在单线程中,操作都是按顺序执行的,如果UI界面内某个点击按钮的操作是比较耗时的,就会发现点击后没有响应,界面卡住了,必须等到耗时结束后才能恢复。

为了解决这种问题,就要使用多线程方法,而QT中实现多线程的核心是存在QTHread线程类,其有两种多线程方法,一种是继承QThread的run函数,另一种是将继承QObject的类转移到一个线程里,官方主要推荐第二种用法,因此笔者将以第二种用法为准进行介绍。

先来看用法示例,在做讲解:

cpp 复制代码
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 状态,随后事件循环取出队列中的事件并执行。

了解工作原理之后,下边说一个多线程调度的示例帮助理解,如:

bash 复制代码
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会报错。用法如下:

cpp 复制代码
 // 定义一个结构体
    struct SensorData
    {
        int id;
        double temperature;
        QString name;
    };

    // 声明元类型
    Q_DECLARE_METATYPE(SensorData)
    // 注册元类型
    qRegisterMetaType<SensorData>("SensorData");
相关推荐
智者知已应修善业1 小时前
【51单片机模拟生日蜡烛】2023-10-10
c++·经验分享·笔记·算法·51单片机
智者知已应修善业2 小时前
【51单片机如何让LED灯从一亮到八,再从八亮到一】2023-10-13
c++·经验分享·笔记·算法·51单片机
qeen872 小时前
【数据结构】二叉树相关经典函数C语言实现
c语言·数据结构·c++·笔记·学习·算法·二叉树
良木生香2 小时前
【C++初阶】STL——List从入门到应用完全指南(1)
开发语言·数据结构·c++·程序人生·算法·蓝桥杯·学习方法
aqiu1111113 小时前
【并查集专题top】
c++·算法
会周易的程序员3 小时前
aiDgeScanner 工业设备网络扫描与管理工具
网络·c++·物联网·架构·electron·node.js·iot
叼烟扛炮3 小时前
C++ 知识点17 友元
开发语言·c++·算法·友员
计算机安禾3 小时前
【c++面向对象编程】第2篇:类与对象(一):定义第一个类——成员变量与成员函数
开发语言·c++