C++ Qt 学习(一):Qt 入门

0. 基础知识

0.1 qmake 和 cmake 对比

  • qmake:qt 独有的代码构建工具
  • cmake:C++ 通用的代码构建工具,绝大部分 C++ 开源项目都使用 cmake 管理代码
  • qt 项目,没有特殊要求,使用 qmake 即可

0.2 Qt 3 个窗口类的区别

  • QMainWindow
    • 包含菜单栏、工具栏、状态栏
    • QMainWindow 使用的场景不多
  • QWidget
    • 一个普通的窗口,不包含菜单栏、状态栏,除了登录界面
    • 新建项目时建议使用 Qwidget,因为大部分的窗口可能都要做成无边框窗口,需要自定义标题栏,实现拉伸等
  • QDialog
    • 对话框,常用来做登录窗口、弹出窗口 (例如设置界面)

1. 图片查看软件

1.1 main.cpp

cpp 复制代码
#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

1.2 widget.h

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    void open1();  // 基础版本
    void open2();  // 记住上次打开的路径,并指定默认的路径为 文档/图片
    void open3();  // 图片自适应显示

private slots:
    void on_btnOpen_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

1.3 widget.cpp

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QSettings>  // 用于读取和写入应用程序的设置和配置信息
#include <QDebug>
#include <QStandardPaths>
#include <memory>     // 智能指针

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);

    ui->label_image->clear();
}

Widget::~Widget() {
    delete ui;
}

void Widget::open1() {
    // QFileDialog::getOpenFileName() 用于显示一个打开文件对话框,并返回用户选择的文件路径
    // 参数:指向当前窗口的指针 this,打开的窗口标题,默认打开路径,文件类型过滤器
    QString filename = QFileDialog::getOpenFileName(this, "请选择图片", "D:/", "图片(*.png *jpg);");

    if(filename.isEmpty()) {
        return;
    }

    ui->lineEdit_path->setText(filename);  // 显示图片路径
    ui->label_image->setPixmap((QPixmap(filename)));  // 显示图片
}

// 记住上次打开的路径,并指定默认的路径为 文档/图片
void Widget::open2() {
    // qApp->applicationDirPath() 用于获取当前应用程序所在的目录路径
    // "/config/Setting.ini" 是一个固定的字符串表示配置文件的路径
    QString config_path = qApp->applicationDirPath() + "/config/Setting.ini";
    qDebug() << config_path;

    // 使用智能指针创建了一个 QSettings 对象,并使用指定的配置文件路径和格式进行初始化
    // QSettings::IniFormat 是一个枚举值,用于指定配置文件的格式,此处采用的是 ini 格式的配置文件
    // 通过 pIniSet 指针调用 value() 函数来获取指定键的值,并将其转换为 QString 类型
    std::unique_ptr<QSettings> pIniSet(new QSettings(config_path, QSettings::IniFormat));
    QString lastPath = pIniSet->value("/LastPath/path").toString();

    // 设置默认读取路径为 windows 下 图片 目录
    if(lastPath.isEmpty()) {
        lastPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
    }

    QString filename = QFileDialog::getOpenFileName(this, "请选择图片", lastPath, "图片(*.png *jpg);");

    if(filename.isEmpty()) {
        return;
    }

    ui->lineEdit_path->setText(filename);
    ui->label_image->setPixmap((QPixmap(filename)));

    // 找到给定文件名(filename)中最后一个斜杠"/"的位置
    int end = filename.lastIndexOf("/");
    // 提取文件名中最后一个斜杠"/"之前的部分,即路径部分
    QString _path = filename.left(end);
    // 将键 "/LastPath/path" 的值设置为 _path
    pIniSet->setValue("/LastPath/path", _path);

    qDebug() << _path;
}

// 图片自适应显示
void Widget::open3() {
    QString config_path = qApp->applicationDirPath() + "/config/Setting.ini";
    qDebug() << config_path;

    std::unique_ptr<QSettings> pIniSet(new QSettings(config_path, QSettings::IniFormat));
    QString lastPath = pIniSet->value("/LastPath/path").toString();

    if(lastPath.isEmpty()) {
        lastPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
    }

    QString filename = QFileDialog::getOpenFileName(this, "请选择图片", lastPath, "图片(*.png *jpg);");

    if(filename.isEmpty()) {
        return;
    }

    ui->lineEdit_path->setText(filename);

    // 图片自适应显示
    std::unique_ptr<QPixmap> pix(new QPixmap(filename));  // 通过给定的文件名(filename)加载图像数据
    // 将加载的图像按照 ui->label_image 控件的大小进行缩放
    // Qt::KeepAspectRatio:保持图像的纵横比例不变
    pix->scaled(ui->label_image->size(), Qt::KeepAspectRatio);
    ui->label_image->setScaledContents(true);  // 当图像大于控件大小时,将自动缩放以适应控件的大小
    ui->label_image->setPixmap(*pix);  // 设置 ui->label_image 控件的图像为加载并缩放后的图像

    int end = filename.lastIndexOf("/");
    QString _path = filename.left(end);
    pIniSet->setValue("/LastPath/path", _path);
    qDebug() << _path;
}

void Widget::on_btnOpen_clicked() {
    //open1();
    //open2();
    open3();
}

1.4 widget.ui

2. C++ lambda 函数详解

  • C++ lambda 表达式的本质就是重载了 operator() ,lambda 是一个类,在调用时会进行编译展开,因此 lambda 表达式对象其实就是一个匿名的 functor,所以 lambda 表达式也叫匿名函数对象

    • Qt 槽函数可以使用 lambda 函数来写
  • C++ 中 lambda 表达式的构成

    cpp 复制代码
    [捕获列表](形参列表) mutable 异常列表->返回类型  {
        函数体
    }
    • 捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量
    • 形参列表:和普通函数的形参列表一样。可省略,即无参数列表
    • mutable:如果有,则表示在函数体中可以修改捕获变量,根据具体需求决定是否需要省略
    • 异常列表:noexcept /throw(...),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常
    • 返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型
    • 函数体:代码实现,可省略,但是没意义
  • 捕获方式

    • 值捕获:不能在 lambda 表达式中修改捕获变量的值
    • 引用捕获:使用引用捕获一个外部变量,需在捕获列表变量前面加上一个引用说明符 &
    • 隐式捕获
    cpp 复制代码
    #include <iostream>
    
    using namespace std;
    
    int main() {
        // 1、值捕获
        int value = 100;
        
        auto f = [value](int a, int b)->int {
            //value++;  // 不能在 lambda 表达式中修改捕获变量的值
            return a + b + value;
        };
        
        cout << f(1, 2) << endl;
        
        // 2、引用捕获
        auto f2 = [&value](int a, int b)->int {
            value++;
            return a + b;
        };
        
        cout << f2(1, 3) << endl;
        cout << "value = " << value << endl;
        
        // 3、隐式捕获
            // = 值捕获
            // & 引用捕获
        int age = 123;
        auto f3 = [&](int a, int b)->int {
            value++;
            age++;
            return a + b;
        };
        
        return 0;
    }

3. 槽函数的常见写法

  • Qt 4 写法

    cpp 复制代码
    connect(ui->btnOpen, SIGNAL(clicked), this, SLOT(open()));
    • 不推荐这种写法,如果 SIGNAL写错了,或者信号名字、槽函数名字写错了编译器检查不出来,导致程序无响应,引起不必要的误解
  • Qt 5 写法

    cpp 复制代码
    connect(ui.btnOpen, QPushButton::clicked, this, &Widget::open);
    • 推荐使用这种写法
  • lambda 函数表达式写法

    cpp 复制代码
    connect(ui.btnOpen, &QPushButton::clicked, [=](){
        // 具体代码实现
    });
    • 适用于 slot 代码比较少的逻辑
  • 直接法

    cpp 复制代码
    void on_控件名_信号名();
    • 这种不用 connect,Qt 自动连接

4. 自定义信号及参数注册

4.1 跨 UI 发送自定义信号

  • 如何自定义信号

    • 使用signals声明
    • 返回值是void
    • 在需要发送的地方使用下述方法进行发送
      • emit 信号名字(参数);
    • 在需要链接的地方使用connect进行链
  • widget.h

    cpp 复制代码
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class Widget; }
    QT_END_NAMESPACE
    
    class Widget : public QWidget {
        Q_OBJECT
    
    public:
        Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void on_btnOpen_clicked();
    
    private:
        Ui::Widget *ui;
    };
    #endif // WIDGET_H
  • dialog.h

    • 跨 UI 发送:New File --> Qt Designer Form Class --> Dialog without Buttons
    cpp 复制代码
    #ifndef DIALOG_H
    #define DIALOG_H
    
    #include <QDialog>
    
    namespace Ui { class Dialog; }
    
    class Dialog : public QDialog {
        Q_OBJECT
    
    public:
        explicit Dialog(QWidget *parent = nullptr);
        ~Dialog();
    
    private slots:
        void on_btnAdd_clicked();
    
    signals:
        void sig_addOne(int value);
    
    private:
        Ui::Dialog *ui;
    };
    
    #endif // DIALOG_H
  • widget.cpp

    cpp 复制代码
    #include "widget.h"
    #include "ui_widget.h"
    #include "dialog.h" // 跨 UI 头文件
    
    Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
        ui->setupUi(this);
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    void Widget::on_btnOpen_clicked() {
        Dialog dlg;
    
        // 使用 lambda 函数编写槽函数
        connect(&dlg, &Dialog::sig_addOne, [=](int value) {
            ui->lineEdit->setText(QString::number(value));
        });
    
        dlg.exec();  // 需放在 connect 后,因为此行为事件循环会阻塞 UI
    }
  • dialog.cpp

    cpp 复制代码
    #include "dialog.h"
    #include "ui_dialog.h"
    
    Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog) {
        ui->setupUi(this);
    }
    
    Dialog::~Dialog() {
        delete ui;
    }
    
    void Dialog::on_btnAdd_clicked() {
        static int value = 100;
        emit sig_addOne(value++); // 实现跨 UI 自加操作
    }

4.2 跨线程发送自定义信号

Qt 的子线程无法直接修改 ui,需要发送信号到 ui 线程进行修改

  • widget.h

    cpp 复制代码
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    #include "childthread.h"
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class Widget; }
    QT_END_NAMESPACE
    
    class Widget : public QWidget {
        Q_OBJECT
    
    public:
        Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void on_btnUpdate_clicked();
        void showInfo(Score s);
    
    private:
        Ui::Widget *ui;
    };
    #endif // WIDGET_H
  • widget.cpp

    cpp 复制代码
    #include "widget.h"
    #include "ui_widget.h"
    #include <QDebug>
    
    Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
        ui->setupUi(this);
    
        qDebug() << "ui thread id = " << QThread::currentThreadId();
    }
    
    Widget::~Widget() {
        delete ui;
    }
    
    void Widget::on_btnUpdate_clicked() {
        ChildThread *ch = new ChildThread();
    
        // 以下实现还是在子线程中(不在 ui 线程中),无法直接修改 ui
    //    connect(ch, &ChildThread::sig_SendToUI, [=](Score s) {
    //        string info = s.name + "id = " + to_string(s.id) + " age = " + to_string(s.age);
    //        ui->lineEdit->setText(QString::fromStdString(info));
    //        // 用于验证 slot 与 ui 下是否同属一个线程(id)
    //        // 结果表明此处的 slot 与子线程的 run() 同属一个线程(id)
    //        qDebug() << "slot thread id = " << QThread::currentThreadId();
    //    });
    
        // 以下实现在 ui 线程 (主线程) 中,可以直接修改 ui
        connect(ch, &ChildThread::sig_SendToUI, this, &Widget::showInfo);
    
        ch->start();
    }
    
    void Widget::showInfo(Score s) {
        qDebug() << "ui thread id2 = " << QThread::currentThreadId();
    
        string info = s.name + "id = " + to_string(s.id) + " age = " + to_string(s.age);
        ui->lineEdit->setText(QString::fromStdString(info));
    }
  • childthread.h

    cpp 复制代码
    #ifndef CHILDTHREAD_H
    #define CHILDTHREAD_H
    
    #include <QThread>
    #include <string>
    
    using namespace std;
    
    struct Score {
        string name;
        int id;
        int age;
    };
    
    class ChildThread : public QThread {
        Q_OBJECT
    
    public:
        ChildThread();
    
    protected:
        void run() override;
    
    signals:
        void sig_SendToUI(Score score);
    };
    
    #endif // CHILDTHREAD_H
  • childthread.cpp

    cpp 复制代码
    #include "childthread.h"
    #include <QDebug>
    
    ChildThread::ChildThread() {
        // 非基础类型参数需要注册
        qRegisterMetaType<Score>("Score");
        qRegisterMetaType<string>("string");
    }
    
    void ChildThread::run() {
        qDebug() << "run thread id = " << QThread::currentThreadId();
    
        Score s;
        s.name = "jack";
        s.id = 1001;
        s.age = 26;
    
        emit sig_SendToUI(s);  // 发送信号
    }

4.3 处理信号重名问题

  • 例如 QComboBox 的信号

    cpp 复制代码
    Q_SIGNALS:
        void currentIndexChanged(int index);
        void currentIndexChanged(const QString &);
  • 解决方案

    cpp 复制代码
    // 错误写法
    connect(ui->comboBox, &QComboBox::currentIndexChanged, this, &Widget::onIndex);
    
    // 解决方案一
    connect(ui->comboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onIndex(int)));
    
    // 解决方案二
    connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Widget::onIndex);

5. connect 函数详解

cpp 复制代码
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
                                              Func1 signal,
                                              const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,
                                              Func2 slot,
                                              Qt::ConnectionType type = Qt::AutoConnection)
cpp 复制代码
enum ConnectionType {
    AutoConnection,
    DirectConnection,
    QueuedConnection,
    BlockingQueuedConnection,
    UniqueConnection =  0x80,
    SingleShotConnection = 0x100,
};
  • AutoConnection

    • 默认连接方式,如果接收方在发出信号的线程中,使用 Qt::DirectConnection
    • 否则使用 Qt::QueuedConnection,在发出信号时确定连接类型
    • Qt 中默认使用 AutoConnection,所以平时写信号槽时都是 4 个参数
  • DirectConnection

    • 当发出信号时,插槽立即被调用,槽在发送信号的线程中执行
  • QueuedConnection

    • 当控制返回到接收方线程的事件循环时调用槽,槽在接收方的线程中执行
  • BlockingQueuedConnection

    • 与 Qt::QueuedConnection 相同,只是发送信号的线程会阻塞,直到槽返回
    • 如果接收方存在于发送信号的线程中,则不能使用此连接,否则应用程序将产生死锁
  • UniqueConnection

    • 这是一个可以使用按位 OR 与上述任何一种连接类型组合的标志,当 Qt::UniqueConnection 被设置时,如果连接已经存在,QObject::connect() 将失败 (例如,如果相同的信号已经连接到相同的对象对的插槽)

6. Qt 信号槽与 MOC

  • moc 全称是 Meta-Object Compiler,也就是 "元对象编译器"

    • Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件
    • 如果发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件,这个源文件中包含了 Q_OBJECT 宏的实现代码,这个新的文件名是原文件名前面加上 moc_ 构成,这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此,这个新的文件不是 "替换" 掉旧的文件,而是与原文件一起参与编译
    • 另外,还可看出:moc 的执行是在预处理器之前,因为预处理器执行之后,Q_OBJECT 宏就不存在了

    可以这么理解,moc 把 Qt 中一些不是 C++ 的关键字做了解析,让 C++ 编译器认识,例如:slots, signals,emit 等,moc 会把这些重新编译解析

7. Qt 内存管理机制

  • C++ 派生类

    • 构造顺序:先执行基类的构造函数,再执行派生类的构造函数
    • 析构顺序:先执行派生类的析构函数,再执行基类的析构函数
  • Qt 半内存管理机制

    • QObject 及其派生类的对象,如果其 parent 非 0,那么其 parent 析构时会析构该对象
    • QWidget 及其派生类的对象,可以设置 Qt::WA_DeleteOnClose 标志位,当 close 时会调用 QWidgetPrivate::close_helper,进而调用 deleteLater 析构该对象

8. 解决 Qt 中文乱码问题

  • 粘贴别人的代码时,首先在记事本里复制一遍,再粘贴到 QtCreator

  • 使用 u8

    • ui.pushButton->setText (u8"你好")
  • 不使用 QtCreator开发,直接使用 vs2019

  • 其他设置

    • QtCreator --- 选项 --- 文本编辑器 --- UTF8 BOM 总是删除
    • #pragma execution_character_set("utf-8")
相关推荐
逐雨~2 小时前
9.8C++作业
开发语言·c++
我爱挣钱我也要早睡!3 小时前
Java 复习笔记
java·开发语言·笔记
知识分享小能手5 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知7 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
疾风铸境8 小时前
qt+halcon开发相机拍照软件步骤
数码相机·qt·halcon·拍照
阿巴Jun8 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao8 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾8 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
抠脚学代码8 小时前
Ubuntu Qt x64平台搭建 arm64 编译套件
数据库·qt·ubuntu
利刃大大9 小时前
【高并发内存池】五、页缓存的设计
c++·缓存·项目·内存池