QT基础学习笔记

文章目录

  • 1 概述
    • 1.1 优点
    • 1.2 QT成功使用案例
    • 1.3 安装教程
      • 1.3.1 在线安装流程
      • 1.3.2 离线安装流程
  • 2 创建工程
    • 2.1 快捷键
      • 2.1.1 常用快捷键
      • 2.1.2 修改快捷键
    • 2.2 proj文件
  • 3 对象树
  • 4 信号和槽
    • 4.1 自定义信号和槽
      • 4.1.1 信号连接信号
      • 4.1.2 一个信号连接多个槽函数
      • 4.1.3 多个信号连接同一个槽函数
      • 4.1.3 信号和槽函数的参数列表必须一一对应,但是信号的参数个数可以多于槽
      • 4.1.4 断开信号
    • 4.2 QT4版本的信号和槽
    • 4.3 lambda表达式
      • 4.3.1 mutable关键字
      • 4.3.2 返回值
      • 4.3.3 利用lambda表达式实现槽函数功能
    • 4.4 小结
  • 5 控件
    • 5.1 窗口
      • 5.1.1 菜单栏
      • 5.1.2 工具栏
      • 5.1.3 状态栏
    • 5.2 PushButton
    • 5.3 对话框
      • 5.3.1 模态对话框
      • 5.3.2 非模态对话框
      • 5.3.3 消息对话框
        • 5.3.3.1 消息对话框是模态对话框
        • 5.3.3.2 使用案例
      • 5.3.4 颜色对话框
      • 5.3.4 文件对话框
      • 5.3.4 字体对话框
    • 5.4 按钮组
      • 5.4.1 Radio button
      • 5.4.2 Check box
    • 5.5 ListWidget
    • 5.6 TableWidget
    • 5.7 ComboBox
    • 5.8 封装自定义控件
      • 5.8.1 添加一个新的Widget类
      • 5.8.2 设计自定义的控件
      • 5.8.3 界面引用自定义的控件
      • 5.8.4 实现自定义控件功能
    • 5.9 TableView
      • 5.9.1 model
      • 5.9.2 view
      • client
  • 6 事件
    • 6.1 鼠标Event
    • 6.2 定时器
      • 6.2.1 Event方式
      • 6.2.1 QTimer对象
    • 6.3 事件分发器
    • 6.4 事件过滤器
      • 6.4.1 使用步骤
      • 6.4.2 代码示例
  • 7 资源文件
    • 7.1 添加使用资源文件
  • 8 国际化
    • 8.1 操作步骤
  • 9 本文例程
  • 10 参考资料

1 概述

1.1 优点

  • 跨平台,几乎支持所有的平台
  • 接口设计良好,使用简单
  • 一定程度上简化了内存回收机制
  • 开发效率高,能够快速的构建应用程序。
  • 社区氛围良好,市场份额上升
  • 支持嵌入式开发

1.2 QT成功使用案例

  • Linux桌面环境KDE
  • WPS office软件
  • Skype网络电话
  • Google Earth
  • VLC多媒体播放器
  • VirtualBox

1.3 安装教程

从5.15之后了都是在线安装了。

1.3.1 在线安装流程

在线安装流程:
https://blog.csdn.net/Python_0011/article/details/131699443

1.3.2 离线安装流程

【选择组件】
QT基础学习笔记_第1张图片
QT基础学习笔记_第2张图片
QT基础学习笔记_第3张图片

2 创建工程

2.1 快捷键

2.1.1 常用快捷键

Go back: Alt + Left 返回,光标上一次到的那个位置,如从一个文本到了另一个中。
Go Forward: Alt + Right前进

2.1.2 修改快捷键

有时Qt Creator快捷键与系统中的快捷键冲突了,可以自定义或者修改原来的快捷键,步骤如下: 工具-》选项-》环境-》键盘-》,此处比如切换书签的快捷键,Ctrl+M显示红色,就说明冲突了,我们选中这一行,点击Record重新记录,再点击Apply和OK即可

2.2 proj文件

QT基础学习笔记_第4张图片

3 对象树

QT基础学习笔记_第5张图片

4 信号和槽

4.1 自定义信号和槽

  • 定义信号
    singals后面的函数都可以作为信号,信号只有声明,没有实现。
class Teacher : public QObject
{
    Q_OBJECT
public:
    explicit Teacher(QObject *parent = nullptr);

signals:
    void hungry();

    void hungry(QString foodName);
};
  • 定义槽
    public slots后面的函数都可以作为slot,slot需要有实现。
class Student : public QObject
{
    Q_OBJECT
public:
    explicit Student(QObject *parent = nullptr);

signals:

public slots:
    void treat();

    void treat(QString foodName);
};
void Student::treat()
{
    qDebug() << "请老师吃饭";
}

void Student::treat(QString foodName) {
    qDebug() << "请老师吃饭, 老师要吃" << foodName.toUtf8().data();
}
  • 建立连接

调用connect函数建立连接。

void (Teacher::*hungryMethod)(QString) = &Teacher::hungry;
void (Student::*treatMethod)(QString) = &Student::treat;

connect(teacher, hungryMethod, student, treatMethod);
  • 发出信号
void MainWindow::classOver() {
    emit this->teacher->hungry("Noodles");
}

【自定义信号或者槽出现重载的情况的解决】

使用函数指针的方式来明确指向的函数的地址。

void (Teacher::*hungryMethod)(QString) = &Teacher::hungry;
void (Student::*treatMethod)(QString) = &Student::treat;

【一个小知识点:QString转为char*】

void Student::treat(QString foodName) {
    qDebug() << "请老师吃饭, 老师要吃" << foodName.toUtf8().data();
}

4.1.1 信号连接信号

 //信号连接信号
void (Teacher::*hungryMethodPure)(void) = &Teacher::hungry;
void (Student::*treatMethodPure)(void) = &Student::treat;
connect(teacher, hungryMethodPure, student, treatMethodPure);
connect(classOverButton, &QPushButton::clicked, teacher, hungryMethodPure);

4.1.2 一个信号连接多个槽函数

4.1.3 多个信号连接同一个槽函数

4.1.3 信号和槽函数的参数列表必须一一对应,但是信号的参数个数可以多于槽

不匹配的情况下会报如下错误:
在这里插入图片描述

4.1.4 断开信号

disconnect(classOverButton, &QPushButton::clicked, teacher, hungryMethodPure);

4.2 QT4版本的信号和槽

在这里插入图片描述
【优点】
参数直观
【缺点】
编译器不会检测参数类型

4.3 lambda表达式

4.3.1 mutable关键字

    //mutable关键字表示可以修改值传递的变量,但是修改的是拷贝,并不是变量本身
    QPushButton* myBtn = new QPushButton(this);
    QPushButton* myBtn2 = new QPushButton(this);
    myBtn2->move(100, 100);
    int m = 10;
    connect(myBtn, &QPushButton::clicked, this, [m]()mutable {m = 110; qDebug() << m;});
    connect(myBtn2, &QPushButton::clicked, this, [=](){qDebug() << m;});
    qDebug() << m;

4.3.2 返回值

void testReturnInLambda() {
    int ret = []()->int{return 1000;}();
    qDebug() << "ret = " << ret;
}

4.3.3 利用lambda表达式实现槽函数功能

/**
 * @brief testOnButtonClick,利用lambda表达式实现槽函数功能
 *
 * @param mainWindow
 */
void testOnButtonClick(MainWindow* mainWindow) {
    QPushButton* button = new QPushButton("Clock window", mainWindow);
    button->move(100, 100);
    mainWindow->connect(button, &QPushButton::clicked, mainWindow, [=](){
        mainWindow->getStudent()->treat("Coco cola");
        mainWindow->close();
    });
}

4.4 小结

QT基础学习笔记_第6张图片

5 控件

5.1 窗口

QT基础学习笔记_第7张图片
QT基础学习笔记_第8张图片

5.1.1 菜单栏

最多只有一个

5.1.2 工具栏

可以有多个

5.1.3 状态栏

5.2 PushButton

5.3 对话框

QT基础学习笔记_第9张图片

5.3.1 模态对话框

不可以操作其他窗口了。

void showModeDialog(QWidget* parent) {
    QDialog dialog(parent);
    dialog.resize(200, 100);
    dialog.exec();
}

5.3.2 非模态对话框

还可以操作其他窗口

void showNoneModeDialog(QWidget* parent) {
    QDialog* dialog = new QDialog(parent);
    dialog->resize(200, 100);
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    dialog->show();
}

5.3.3 消息对话框

5.3.3.1 消息对话框是模态对话框

QT基础学习笔记_第10张图片

5.3.3.2 使用案例
void showMessageBox(QWidget* parent, int dialogType) {
    switch (dialogType) {
    case 1:
    {
        QMessageBox::critical(parent, "critial", "critial happened");
        break;
    }
    case 2:
    {
        QMessageBox::information(parent, "information", "information happened");
        break;
    }
    case 3:
    {
        QMessageBox::StandardButton result = QMessageBox::question(parent, "ques", "question happened", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
        if (result == QMessageBox::Yes) {
            qDebug() << "Yes";
        } else {
            qDebug() << "No";
        }
        break;
    }
    case 4:
    {
        QMessageBox::warning(parent, "warning", "warning happened");
        break;
    }
    default:
        QMessageBox::critical(parent, "critial", "critial happened");
        break;
    }
}

5.3.4 颜色对话框

void showColorDialog(QWidget* parent) {
    QColor color = QColorDialog::getColor(QColor(255, 0, 0));
    QString str = QString("[%1, %2, %3]").arg(color.red()).arg(color.green()).arg(color.blue());
    qDebug() << str;
}

5.3.4 文件对话框

void showFileDialog(QWidget* parent) {
    QString filePath = QFileDialog::getOpenFileName(parent, "Sk Open File", "C:\\Users\\imt2047\\Desktop", "(*.png)");
    qDebug() << filePath;
}

5.3.4 字体对话框

void showFontDialog(QWidget* parent) {
    bool flag;
    QFont font = QFontDialog::getFont(&flag, QFont("Times New Roman", 14));
    QString res = QString("Family:%1, Size:%2, Bold:%3, Italic:%4").arg(font.family()).arg(font.pointSize()).arg(font.bold())
            .arg(font.italic());
    qDebug() << res;
}

5.4 按钮组

5.4.1 Radio button

设置默认选中

ui->rBtnMan->setChecked(true);

5.4.2 Check box

在这里插入图片描述

5.5 ListWidget

void MainWindow::initListWidget() {
//    QListWidgetItem* item = new QListWidgetItem("Hello hello, jin gou bei, jin goubei");
//    item->setTextAlignment(Qt::AlignHCenter);
//    ui->listWidget->addItem(item);

    QStringList list;
    list << "Hello hello, jin gou bei, jin goubei" << "Hello hello, jin gou bei, jin goubei"
         << "Hello hello, jin gou bei, jin goubei" << "Hello hello, jin gou bei, jin goubei";
    ui->listWidget->addItems(list);
}

5.6 TableWidget

void MainWindow::initTableWidget() {
    ui->tableWidget->setColumnCount(3);
    ui->tableWidget->setRowCount(5);
    ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"Name" << "Sex" << "Age");

    QStringList nameList;
    nameList << "yase" << "zhoayun" << "zhangfei" << "guanyu" << "huamulan";

    QList<QString> sexList;
    sexList<< "Male" << "Male" << "Male" << "Male" << "Female";

    for (int i = 0; i < 5; i++) {
        int columnIndex = 0;
        ui->tableWidget->setItem(i, columnIndex++, new QTableWidgetItem(nameList[i]));
        ui->tableWidget->setItem(i, columnIndex++, new QTableWidgetItem(sexList[i]));
        ui->tableWidget->setItem(i, columnIndex++, new QTableWidgetItem(QString::number(i)));
    }
}

5.7 ComboBox

void MainWindow::initComboBox() {
    ui->comboBox->addItem("Benz");
    ui->comboBox->addItem("BMW");
    ui->comboBox->addItem("125 Moto");

    connect(ui->selectCarPushButton, &QPushButton::clicked, this, [=](){
        // ui->comboBox->setCurrentIndex(2);
        ui->comboBox->setCurrentText("BMW");
    });
}

5.8 封装自定义控件

5.8.1 添加一个新的Widget类

QT基础学习笔记_第11张图片

5.8.2 设计自定义的控件

拖拖拽拽
在这里插入图片描述

5.8.3 界面引用自定义的控件

QT基础学习笔记_第12张图片
QT基础学习笔记_第13张图片
QT基础学习笔记_第14张图片

5.8.4 实现自定义控件功能

spinbox和slider联动。

void SmallWidget::setValue(int value) {
    ui->spinBox->setValue(value);
}


int SmallWidget::getValue() {
    return ui->spinBox->value();
}

void SmallWidget::initBaseFunction() {
    // SpinBox数值变化,Slider跟随滑动
    void (QSpinBox::* valueChangedSignal)(int) = &QSpinBox::valueChanged;
    connect(ui->spinBox, valueChangedSignal, ui->horizontalSlider, &QSlider::setValue);

    // Slider滑动,SpinBox跟随数值变化
    connect(ui->horizontalSlider, &QSlider::valueChanged, ui->spinBox, &QSpinBox::setValue);
}

5.9 TableView

5.9.1 model

import sys
from datetime import datetime

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # Get the raw value
            value = self._data[index.row()][index.column()]

            # Perform per-type checks and render accordingly.
            if isinstance(value, datetime):
                # Render time to YYY-MM-DD.
                return value.strftime("%Y-%m-%d")

            if isinstance(value, float):
                # Render float to 2 dp
                return "%.2f" % value

            if isinstance(value, str):
                # Render strings with quotes
                return '"%s"' % value

            # Default (anything not captured above: e.g. int)
            return value

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

5.9.2 view

import sys
from datetime import datetime

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from tablemodel import TableModel

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()

        data = [
            [4, 9, 2],
            [1, -1, 'hello'],
            [3.023, 5, -5],
            [3, 3, datetime(2017, 10, 1)],
            [7.555, 8, 9],
        ]

        self.model = TableModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)

client

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from mainwindow import MainWindow

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

6 事件

6.1 鼠标Event

#ifndef MYLABEL_H
#define MYLABEL_H

#include 
#include 
#include 

class MyLabel : public QLabel
{
    Q_OBJECT
public:
    explicit MyLabel(QWidget* parent = 0);

    /**
     * @brief enterEvent: 覆写基类方法
     * @param event
     */
    void enterEvent(QEvent *event);
    void leaveEvent(QEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
    void mouseDoubleClickEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
};

#endif // MYLABEL_H
#include "mylabel.h"

MyLabel::MyLabel(QWidget* parent):QLabel(parent)
{
    // 开启鼠标追踪
    setMouseTracking(true);
}

void MyLabel::enterEvent(QEvent *event) {
    qDebug() << "mouse in in la.";
}

void MyLabel::leaveEvent(QEvent *event) {
    qDebug() << "mouse out out la.";
}

void MyLabel::mousePressEvent(QMouseEvent *event) {
    // 如果鼠标左键和右键按下,提示信息
    QString string = QString("Mouse click happened");
    if (event->button() == Qt::LeftButton) {
        string = QString("Mouse left button click, x = %1, y = %2").arg(event->x()).arg(event->y());
    } else if (event->button() == Qt::RightButton) {
        string = QString("Mouse right button click, x = %1, y = %2").arg(event->x()).arg(event->y());
    } else {

    }
    qDebug() << string;
}

void MyLabel::mouseReleaseEvent(QMouseEvent *event) {
    QString string = QString("Mouse release, x = %1, y = %2").arg(event->x()).arg(event->y());
    qDebug() << string;
}

void MyLabel::mouseDoubleClickEvent(QMouseEvent *event) {
    QString string = QString("Mouse double click, x = %1, y = %2").arg(event->x()).arg(event->y());
    qDebug() << string;
}

void MyLabel::mouseMoveEvent(QMouseEvent *event){
    if (event->buttons() & Qt::LeftButton) {
        QString string = QString("Mouse moving, x = %1, y = %2").arg(event->x()).arg(event->y());
        qDebug() << string;
    }
}

6.2 定时器

6.2.1 Event方式

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include 
#include 

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

    void timerEvent(QTimerEvent* event);

private:
    Ui::MainWindow *ui;

    int timerId1;
    int timerId2;

    void onTimer1Event(QTimerEvent* event);
    void onTimer2Event(QTimerEvent* event);
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"

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

    // 启动定时器
    timerId1 = startTimer(1000);
    timerId2 = startTimer(2000);
}

void MainWindow::timerEvent(QTimerEvent* event) {
    if (event ->timerId() == timerId1) {
        onTimer1Event(event);
    } else if (event->timerId() == timerId2) {
        onTimer2Event(event);
    }
}

void MainWindow::onTimer1Event(QTimerEvent* event) {
    static int num = 1;

    ui->label->setText(QString::number(num++));
}

void MainWindow::onTimer2Event(QTimerEvent* event) {
    static int num = 1;

    ui->label_2->setText(QString::number(num++));
}

6.2.1 QTimer对象

void startQTimer(MainWindow* parent) {
    QTimer* timer = new QTimer(parent);
    parent->connect(timer, &QTimer::timeout, parent, [=](){
        static int num = 1;
        parent->getUi()->label_3->setText(QString::number(num++));
    });
    timer->start(500);

    parent->connect(parent->getUi()->pauseButton, &QPushButton::clicked, parent, [=](){
        timer->stop();
    });

    parent->connect(parent->getUi()->resumeButton, &QPushButton::clicked, parent, [=](){
        timer->start(500);
    });
}

6.3 事件分发器

bool MyLabel::event(QEvent *e) {
    // 拦截处理鼠标按下事件
    if (e->type() == QEvent::MouseButtonPress) {
        QMouseEvent* event = static_cast<QMouseEvent*>(e);
        handleMousePressEvent(event, "event()");
        return true;
    }

    // 交给父类处理
    return QLabel::event(e);
}

6.4 事件过滤器

6.4.1 使用步骤

  1. 控件安装事件过滤器
  2. 覆写eventFilter函数。

6.4.2 代码示例

void MainWindow::initLabel() {
    //1. install event filter
    ui->label->installEventFilter(this);
}
/**
 * 2. override eventFilter
 *
 * @brief MainWindow::eventFilter
 * @param object
 * @param e
 * @return
 */
bool MainWindow::eventFilter(QObject* object, QEvent* e) {
    if (object == ui->label) {
        // 拦截处理鼠标按下事件
        if (e->type() == QEvent::MouseButtonPress) {
            QMouseEvent* event = static_cast<QMouseEvent*>(e);
            handleMousePressEvent(event, "eventFilter()");
            return true;
        }
    }

    return QMainWindow::eventFilter(object, e);
}

7 资源文件

7.1 添加使用资源文件

  • 将资源文件拷贝到目录中
    QT基础学习笔记_第15张图片

  • 创建资源文件(qrc文件)
    QT基础学习笔记_第16张图片

  • 编辑资源文件
    添加前缀,添加文件。
    QT基础学习笔记_第17张图片

  • 使用资源文件

void MainWindow::initFileMenuItems() {
    QAction* newAction = new QAction("New");
    //使用QT资源: : + 前缀名 + 资源名
    newAction->setIcon(QIcon(":/image/robot_hand.svg"));
    this->fileMenuItems.append(newAction);
    this->fileMenuItems.append(new QAction("Open"));
}

8 国际化

8.1 操作步骤

  1. 生成ts文件
  • pro文件增加内容
TRANSLATIONS = ListTableWidgetTest_EN.ts \
                ListTableWidgetTest_CN.ts
  • 使用lupdate生成ts文件
    QT基础学习笔记_第18张图片
  • 查看生成的ts文件
    生成的文件就在工程目录中。ts文件就是xml文件。
    QT基础学习笔记_第19张图片
  1. 生成qm文件
    使用linguist生成。
    QT基础学习笔记_第20张图片
  2. 代码中加载qm文件,安装translator,retranslateUi
void MainWindow::on_selectLanguangeComboBox_activated(int index)
{
    switch(index) {
    // chinese
    case 0:
        translator->load(":/language/res/ListTableWidgetTest_CN.qm");
        isChinese = true;
        break;
    // english
    case 1:
        translator->load(":/language/res/ListTableWidgetTest_EN.qm");
        isChinese = false;
        break;
    default:
        break;
    }

     qApp->installTranslator(translator);
     ui->retranslateUi(this);
}
  1. 注意ComboBox
    retranslateUi刷新UI的时候,会把组合框默认选择第一个。如果同组合框切换语言的时候,要处理下这个选择框。
    解决办法就是记录全局变量,记录每一次选择的原因。 如果组合框因为刷新的原因发生了不一致,就根据全局变量重新恢复。
void MainWindow::changeEvent(QEvent *e) {
    switch (e->type()) {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        if (!isChinese && ui->comboBox->currentIndex() == 0) {
            ui->selectLanguangeComboBox->setCurrentIndex(1);
        }
        break;
    default:
        break;
    }
}

9 本文例程

https://download.csdn.net/download/kaikai_sk/88496589
https://download.csdn.net/download/kaikai_sk/88496592
https://download.csdn.net/download/kaikai_sk/88496597

10 参考资料

[1] https://www.bilibili.com/video/BV1g4411H78N/?p=11&spm_id_from=pageDriver&vd_source=f4dcb991bbc4da0932ef216329aefb60
[2] https://blog.csdn.net/weixin_51081223/article/details/121671615
[3] https://cloud.tencent.com/developer/article/2245901
[4] https://blog.csdn.net/ihmhm12345/article/details/127408975
[5] 创建PyQT项目: https://www.pythonguis.com/tutorials/first-steps-qt-creator/
[6] ui转换为py,并生成exe: https://www.cnblogs.com/linyfeng/p/11223707.html
[7] QT Linguist: https://blog.csdn.net/liang19890820/article/details/50274409
[8] QT 国际化:https://blog.csdn.net/liang19890820/article/details/50276673
[9] QT 国际化:https://www.bilibili.com/video/BV1yx411t7cX/?spm_id_from=333.337.search-card.all.click&vd_source=f4dcb991bbc4da0932ef216329aefb60
[10] Android事件分发机制:https://www.jianshu.com/p/38015afcdb58
[11] QT事件机制:https://blog.csdn.net/luolaihua2018/article/details/110797592
[12] QT事件机制:https://blog.csdn.net/tqs_1220/article/details/82563070
[13] QT事件机制:https://www.cnblogs.com/Braveliu/p/7417476.html
[14] 在Qt Creator中查看QT源码:https://blog.csdn.net/ihmhm12345/article/details/127408975
[15] PyQt TableView demo: https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/
[16] Qt信号和槽机制详解: https://c.biancheng.net/view/9414.html
[17] Qt一篇全面的信号和槽函数机制总结: https://zhuanlan.zhihu.com/p/603617075

你可能感兴趣的:(Cpp入坑之路,qt,学习,笔记)