2024年12月29日-qt学习

使用qtcreator写了一个简单的qt程序。记录一下。

程序整体的结构

在我的程序中,一个窗口,对应一个类,对应一个命名空间,继承QWidget或者QMainWindow类。类中有信号(signals)和槽(slots),按照我的理解:槽是实际执行任务的方法,信号是能跨窗口触发槽的方法。

我的程序主要有mainWindow和starPromptWindow两个窗口,在mainwindow类中实例化了一个m_starPromptWindow,用于触发信号从而使用槽。

信号和槽

例如,如果我要点击starPromptWindow的一个按钮,要改变mainWindow的一个控件内容,那我应该在starPromptWindow中定义一个信号,还要有一个触发信号的方法,在mainWindow中应该有一个目标方法来改变控件内容。我应当在mainWindow中连接这个信号和目标方法,因为我在mainWindow中实例化了一个starPromptWindow对象m_starPromptWindow。

总之就是:

  • starPromptWindow:
    • 按钮
    • 信号
    • 发出信号的方法
      • 与按钮连接
  • mainWindow:
    • 实例化一个starPromptWindow
    • 连接信号与目标方法
    • 目标方法(槽)
      • 操作mainWindow中的控件

如果我反过来,要点击mainWindow中的按钮,控制starPromptWindow中的控件,那我同样应该使用信号。但是不能像这样直接调换地位:

  • starPromptWindow:
    • 实例化一个mainWindow
    • 连接信号与目标方法
    • 目标方法(槽)
      • 操作starPromptWindow中的控件
  • mainWindow:
    • 按钮
    • 信号
    • 发出信号的方法
      • 与按钮连接

这是错误的(大概)。按理来说应该这么做:

  • starPromptWindow:
    • 信号
    • 发出信号的方法
      • 与mainWindow中的按钮连接
      • 连接在mainWindow中设置
    • 一个private槽
      • 用于控制starPromptWindow中的控件
  • mainWindow:
    • 实例化一个starPromptWindow
    • 按钮
    • 连接按钮与starPromptWindow的信号

不过事实上也可以不通过信号,直接使用starPromptWindow中的public槽:

  • starPromptWindow:
    • 一个public槽
      • 用于控制starPromptWindow中的控件
  • mainWindow:
    • 实例化一个starPromptWindow
    • 连接到starPromptWindow的public槽

总而言之,通过信号和槽,就能间接地通过一个窗口的方法和另一个窗口的控件交互了。

窗口头文件

以mainWindow为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include "starPromptWindow.h"
#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

signals:
// ......

public slots:
// ......

private slots:
// ......

public:
// ......

private:
Ui::MainWindow *ui;
starPromptWindow *m_starPromptWindow; // StarPromptWindow 的实例
// ......
};

#endif

按照qt(或者说是qtcreator)标准写法写的类定义,声明成员。

cpp程序

以mainWindow为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "starPromptWindow.h"

MainWindow::MainWindow(QWidget *parent):
QMainWindow(parent),
ui(new Ui::MainWindow),
m_starPromptWindow(new starPromptWindow(this))
{
ui->setupUi(this);
// ......
}

MainWindow::~MainWindow()
{
delete ui;
delete m_starPromptWindow;
}

实现功能的部分。

其中,~MainWindow()函数是退出窗口时执行的动作,这个方法在类定义的时候声明了。

不过事实上,要更改窗口关闭时的行为,我是重写了closeEvent。例如,我不希望starPromptWindow退出的时候彻底退出,我希望只是隐藏,同时保存prompt,于是我重写了closeEvent:

1
2
3
4
5
void starPromptWindow::closeEvent(QCloseEvent *event) {
this->hide();
saveStarPrompt();
event->ignore(); // 忽略关闭事件,防止窗口被销毁
}

对应的,要在头文件类定义中说明你重写了这个方法:
1
2
protected:
void closeEvent(QCloseEvent *event) override;

主要功能:

绘图:

通过发送请求完成。请求的内容存储在ApiData.json中。

  1. 图片预览

通过生成并打开一个QDialog类型的、继承了mainWindow的imageDialog实现。在其中插入一个QLabel类型的imageLabel变量,通过setPixmap(QPixmap::fromImage(scaledImage))设置内容,然后layout->addWidget(imageLabel)将其插入QVBoxLayout型的layout,最后通过imageDialog->setLayout(layout)设置layout。

  • 值得注意的是,QDialog有一个性质名为模态,model,需要通过imageDialog->setModal(false)设置为flase,否则只能聚焦在最后一个打开的QDialog上。
  • 值得注意的是,如果不执行imageDialog->setAttribute(Qt::WA_DeleteOnClose),imageDialog占用的内存不会释放,导致占用节节高。
  1. 图片和参数保存

图片有现成的类型QImage,QImage有现成的方法save();文件有QFile,可以通过文本流QTextStream写入:

(QFile变量名为jsonFile)

1
2
3
4
5
if (jsonFile.open(QIODevice::WriteOnly)) {
QTextStream out(&jsonFile);
out << QJsonDocument(jsonObj).toJson();
jsonFile.close();
}

  1. “上一次输入的prompt”将被自动记录,并在下一次启动时填写

同上读写json文件。

没有使用数据库是因为不方便直接编辑。

  1. 收藏prompt,查看与管理收藏的prompt

使用了starPromptWindow窗口完成。读写文件同上。和主窗口的交互详见信号量部分。

次要功能:

  1. 网络错误时会弹出提示框

使用了我自己写的专门的类ErrorDialog(继承了QDialog)。qt有专门的错误提示框QErrorMessage,用起来很方便,不过我不太喜欢ui布局所以自己写了。

  1. clear按钮可以快速清屏右边的日志

直接将按钮连接到ui->outputTextEdit->clear函数

  1. alt + s 是绘制的快捷键

先创建快捷方式,然后连接。就像这样

1
2
QShortcut *shortcutDraw = new QShortcut(QKeySequence(Qt::ALT | Qt::Key_S), this);
connect(shortcutDraw, &QShortcut::activated, ui->sendRequestButton, &QPushButton::click);

  1. ctrl + s 是保存tag的快捷键(如果不手动保存的话,画完一张图后也会自动保存)

同理

1
2
QShortcut *shortcutSaveHistory = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_S), this);
connect(ui->ClearButton, &QPushButton::clicked, this, &MainWindow::onClearButtonClicked);

  1. 响应式布局

最麻烦的一集

使用qtcreator的可视化编辑器折腾了好半天,继承QWidget的情况下,怎么都会挤成一坨。后来改成了继承MainWindow类才好。

starPromptWindow的初始化函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void starPromptWindow::initStarPrompts(){
this->setWindowTitle("Star Prompts");
this->resize(600, 600);
QJsonObject json = readStarPrompts();
tabWidget = new QTabWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(tabWidget);

// 遍历 JSON 对象,为每个键创建 Tab 页面
for (QString key : json.keys()) {
QJsonObject item = json[key].toObject();
addTabForStarPrompts(item["prompt"].toString(), item["negativePrompt"].toString(), key, tabWidget);
}

// 会挤成一团
// setLayout(mainLayout);
// 会挤成一团
// this->setLayout(mainLayout);
ui->layoutWidget->setLayout(mainLayout);
this->setAttribute(Qt::WA_DeleteOnClose);
}

注意这里layout用的是QVBoxLayout,是具体的垂直布局,即使只有一个子控件,而不是基类QLayout(QLayout似乎并不能直接拿来用)。

setLayout(mainLayout) 和 this->setLayout(mainLayout) 都会使得控件挤成一团。不理解为什么

main函数

首先实例化一个QApplication,然后实例化主窗口。

starPromptWindow不需要在这里实例化因为在mainWindow中实例化了。

"mainwindow.h"
1
2
3
4
5
6
7
8
9
10
11
#include "starPromptWindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.setWindowIcon(QIcon("./icon.ico"));
w.show();
return a.exec();
}

总结

qt说难不难,主要是麻烦。窗口之间交互依赖信号和槽,在谁里实例化谁要搞明白。而且可读性不太强。之前写过pyqt,最大区别大概是connect用法不同?

qt是把A的动作A1和某窗口(一般是this)的目标方法连接,pyqt是A的成员A1的方法connect直接连接目标方法。何况cpp大量使用指针,导致读的时候需要留个心眼看是直接传递的值还是传递了指针。

写完之后一看,思路还挺清晰的。也可能是因为我的程序比较小,规模大了也可能会比较混沌也说不定


2024年12月29日-qt学习
http://petertan303.github.io/2024/12/29/2024年12月29日-qt学习/
作者
peter?
发布于
2024年12月29日
许可协议