在 VS Code 中学习Qt

Qt

This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.

写在前面

寒假在家看完了导师给的文献 (真的水),重新拾起了自己在上学期上机时玩的 Qt,用的语言是 C++,因为 qt creator 在 Arch 下的界面极其的丑,缩放还有问题,所以转到了 VS Code 上写代码。中间遇到了不少问题,就在这里记录一下解决方法,还有 Qt 的学习所得。

又开了一个新坑

本文所有配置是在 Arch Linux 上进行的,Qt 版本为 5.15.2现在变成 6.4.0了

VS Code 配置

插件

  • C/C++ Extension Pack
  • Qt tools

环境变量

通过 pacman 安装的 qt 各种头文件已经在系统路径中了,无需做更改

.ui 文件转换为头文件

在使用 qt creator 开发时,编辑器会读取 qt 的 ui 文件然后正确解析变量之间的关系。然而 VS Code 不会这么做,所以会出现 VS Code 无法正确理解存在 ui 文件中的变量及其关系。解决方案是使用 qt 提供的 uic 命令将 ui 文件转换成头文件。

1
uic qt.ui -o ui_qt.h

这样 VS Code 中红红的报错就消失了。在 ui 文件发生更改后需要再执行命令更新头文件。

注意

以下部分代码功能不完备,经我自己实践编写后已经另外重写了图片查看器代码

传送门

使用 Qt 写一个简单的图片查看器

初始化 Qt 项目

qt creator 中按照 文件 → 新建文件或项目 打开对话框,选择 Qt Widgets Application,名称和路径自定,构建系统 (Build system) 选择 CMakeClass name 同样自定,这里使用 ImageViewer。由于是学习目的,Translation 可以先跳过。之后的 Kits版本控制 也可以先不选。

关于 Qt 的一些概念

在使用 Qt 之前,需要先介绍一些概念,理解了这些概念之后,代码就容易看懂了。

关于 ui 文件

ui 文件可以直接在 qt creator 中进行编辑,这个文件对应了你所设计的程序的图形界面的布局。ui 文件使用的语言是 XML,但是 qt creator 会隐式的将其当作 .h 头文件来处理,这一点在创建的 imageviewer.cpp 中就可以看出来。

上图的 ui_imageviewer.h 就是 ui 文件代表的头文件,前面也提到过,VSCode 不会将 ui 看作是头文件,因此需要用 uic 命令转换 ui 文件。

关于 QAction

在很多找到的教程里,都有提到 QAction 这个控件。其实,如果你是直接在 qt creator 编辑控件的话,没有必要再自己定义这个控件了。所有添加到 ui 里的控件都会定义在 ui 头文件里面。在后面的代码中你会看到这一点。

关于槽

槽函数,是 Qt 中一类特殊的函数。我的理解是,当你写好了一个槽函数,并将其绑定到一个控件上,在控件收到对应的信号后 (例如鼠标单击触发的 triggered 信号),便会调用对应的函数。槽函数的声明是写在对应类的 private slots 中。

在头文件中声明需要的函数

imageviewer.h 中,你可以看到预先写好的 ImageViewer 类,所有函数的声明将写在这个类中。

对于槽函数:

  • 需要 open 来执行打开图片的功能
  • 需要 zoomInzoomOut 来执行缩放图片的功能
  • 需要 normalSize 来执行将图片设置回原来大小的功能
  • 需要 fitToWindow 来执行将图片设置为与窗口大小相同的功能

所以,在 private slots 中可以写为

1
2
3
4
5
6
private slots:
void open();
void zoomIn();
void zoomOut();
void normalSize();
void fitToWindow();

另外,我们还需要如下私有函数及变量:

  • updateActions() 来设置某个选项是否可触发,例如在没有打开图片的时候将 zoomInzoomOutnormalSizefitToWindow 功能设置为不可触发。
  • scaleImage() 来调整图片大小
  • adjustScrollbar() 来调整滚动条
  • ui 指针来获取窗口的指针,以进行后续操作
  • scaleFactor 来记录图片的缩放比

以及如下公共函数:

  • ImageViewer() 析构函数与 ~ImageViewer() 解构函数,这个新建项目的时候会自动写好
  • loadFile 来读取图片

最终整个文件如下

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
34
35
36
37
38
39
#ifndef IMAGEVIEWER_H
#define IMAGEVIEWER_H

#include <QMainWindow>
#include <QScrollBar>
#include <QLabel>
#include <QScrollArea>

QT_BEGIN_NAMESPACE
namespace Ui
{
class ImageViewer;
}
QT_END_NAMESPACE

class ImageViewer : public QMainWindow
{
Q_OBJECT

public:
ImageViewer(QWidget *parent = nullptr);
bool loadFile(const QString &);
~ImageViewer();

private slots:
void open();
void zoomIn();
void zoomOut();
void normalSize();
void fitToWindow();

private:
Ui::ImageViewer *ui;
void updateActions();
void scaleImage(double factor);
void adjustScrollbar(QScrollBar *scrollBar, double factor);
double scaleFactor = 1;
};
#endif // IMAGEVIEWER_H

设计界面

在真正的写函数之前,要把 UI 界面先设计好。其实很简单,用 qt creator 拖拉几个控件就好了。

双击这个地方添加菜单和二级菜单

File 和 View 子菜单如下

并且在下方设置一下 fit to window 的 action 使其可以被选中

然后在左边的控件栏里搜索 Scroll AreaLabel 添加两个控件,Label 用来展示图片,Scroll Area 可以在其子控件过大时显示滚动条来以供通过滚动来显示子控件。将 Label 控件拖到 Scroll Area 控件内部,qt creator 会自动将其添加为 Scroll Area 的子控件。

定义函数

ImageViewer

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
// 这里的 ':' 与 ',' 是初始化父类与参数
// ui(new Ui::ImageViewer) 其实就等于 Ui::ImageViewer *ui = new Ui::ImageViewer
ImageViewer::ImageViewer(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::ImageViewer)
{
// 这个函数实现窗口的生成与各种属性的设置、信号与槽的关联。
// 上面这一句是我抄来的,具体什么作用我也不理解,不过这一句是必须的,大概是实现了什么初始化的功能吧
ui->setupUi(this);
// 设置 label 部件的背景色为 文本输入窗口部件 的背景色,这个颜色是内置的。
// label 即对应刚刚拖动控件时添加的 label, 这两个的名字一定是相对应的,
// 因为 qt 根据设置的名字来生成 ui 头文件
// 设置背景色为 Base
ui->label->setBackgroundRole(QPalette::Base);
// 设置控件在布局里面的大小变化的属性
ui->label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
// 设置是否按比例填充满整个label框
ui->label->setScaledContents(true);
ui->scrollArea->setBackgroundRole(QPalette::Dark);
// 使得Widget可见或不可见
ui->scrollArea->setVisible(false);
// 将 scrollArea 设置为主窗口的中心窗口部件,但是理论上讲
// 使用 qt creator 设计 GUI 时,编辑器会自动创建中心窗口部件
setCentralWidget(ui->scrollArea);

// combine the slot
// 将 action 与槽函数绑定,这样对应的 action 触发时才能执行相应的功能
connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(open()));
connect(ui->actionzoom_in, SIGNAL(triggered()), this, SLOT(zoomIn()));
connect(ui->actionzoom_out, SIGNAL(triggered()), this, SLOT(zoomOut()));
connect(ui->actionnormal_syize, SIGNAL(triggered()), this, SLOT(normalSize()));
connect(ui->actionfit_to_window, SIGNAL(triggered()), this, SLOT(fitToWindow()));
}

updateActions

1
2
3
4
5
6
7
8
9
/*
* 根据 fit to window 是否被选中来调整 zoomIn, zoomOut, normalSize 的可用性
*/
void ImageViewer::updateActions()
{
zoomInAct->setEnabled(!fitToWindowAct->isChecked());
zoomOutAct->setEnabled(!fitToWindowAct->isChecked());
normalSizeAct->setEnabled(!fitToWindowAct->isChecked());
}

loadFile

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
/*
* 用来读取图片文件并显示在 label 上
*/
bool ImageViewer::loadFile(const QString &filename)
{
QImageReader reader(filename);
reader.setAutoTransform(true);
const QImage newImage = reader.read();
if (newImage.isNull())
{
QMessageBox::information(this, QGuiApplication::applicationDisplayName(),
tr("Cannot load %1: %2")
.arg(QDir::toNativeSeparators(filename),
reader.errorString()));
return false;
}
// 显示图片
ui->label->setPixmap(QPixmap::fromImage(newImage));;
scaleFactor = 1.0;
ui->scrollArea->setVisible(true);
// 将 fit to window 设置为可用状态,默认为不可用
ui->actionfit_to_window->setEnabled(true);
updateActions();
// 如果 fit to window 被勾选,则自动调整 label 的大小
if (!fitToWindowAct->isChecked())
ui->label->adjustSize();
return true;
}

adjustScrollbar

1
2
3
4
5
6
7
8
9
/*
* 调整滚动条的大小
*/
void ImageViewer::adjustScrollbar(QScrollBar *scrollBar, double factor)
{
scrollBar->setValue(
int(factor * scrollBar->value() + ((factor - 1) * scrollBar->pageStep() / 2))
);
}

scaleImage

1
2
3
4
5
6
7
8
9
10
11
12
/*
* 调整图片大小
*/
void ImageViewer::scaleImage(double factor)
{
scaleFactor *= factor;
ui->label->resize(scaleFactor * ui->label->pixmap(Qt::ReturnByValue).size());
adjustScrollbar(ui->scrollArea->horizontalScrollBar(), factor);
adjustScrollbar(ui->scrollArea->verticalScrollBar(), factor);
zoomInAct->setEnabled(scaleFactor < 3.0);
zoomOutAct->setEnabled(scaleFactor > 0.333);
}

简单的图片查看器

开发环境为 Qt6,UI 设计部分仅简略描述

UI 部分

文件菜单

image-20221008202559333

编辑菜单

image-20221008202645818

以及其对应的控件命名

控件名称 Action 名称
打开 action_open
关闭图片 action_close_image
退出 action_exit
放大 action_big
缩小 action_small

代码部分

imageviewer.h

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
34
35
36
37
38
#ifndef IMAGEVIEWER_H
#define IMAGEVIEWER_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class ImageViewer; }
QT_END_NAMESPACE

class ImageViewer : public QMainWindow
{
Q_OBJECT

public:
ImageViewer(QWidget *parent = nullptr);
~ImageViewer();
// 调整显示图片的QLabel的大小的函数
void resizeLabel(int height, int width);

public slots:
// 用来绑定到控件上的槽函数
void showImage();
void scaleBig();
void scaleSmall();
void clearLabel();

protected:
// 重写类的事件函数以便监听窗口大小调整事件
void resizeEvent(QResizeEvent* event) override;

private:
Ui::ImageViewer *ui;
// 定义变量记录图片原始高和宽以及缩放比
int imageHeight = 10;
int imageWidth = 10;
float scale = 1;
};
#endif // IMAGEVIEWER_H

imageviewer.cpp

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "imageviewer.h"
#include "./ui_imageviewer.h"
#include <iostream>
#include <QFileDialog>

ImageViewer::ImageViewer(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::ImageViewer)
{
ui->setupUi(this);
// 绑定槽函数
QObject::connect(ui->action_open, SIGNAL(triggered()), this, SLOT(showImage()));
QObject::connect(ui->action_big, SIGNAL(triggered()), this, SLOT(scaleBig()));
QObject::connect(ui->action_small, SIGNAL(triggered()), this, SLOT(scaleSmall()));
QObject::connect(ui->action_close_image, SIGNAL(triggered()), this, SLOT(clearLabel()));
}

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

void ImageViewer::resizeEvent(QResizeEvent* event)
{
QMainWindow::resizeEvent(event);
// 窗口调整大小时,对显示图片的QLabel进行相应的调整
resizeLabel(this->imageHeight, this->imageWidth);
}

// 调整显示图片的QLabel的大小,保证图片不会发生形变,并让图片完整显示
void ImageViewer::resizeLabel(int imageHeight, int imageWidth)
{
QSize test = ui->image_widget->size();
int imageLabelSide, imageLabelXStart, imageLabelYStart, imageLabelOtherSide;
// 这里涉及到一些有关图片大小调整的概念,请参考后文给出的链接
int flag = ((double)test.width() / test.height()) > ((double)imageWidth / imageHeight) ? 0: 1;
if (flag)
{
imageLabelSide = (test.width()) * this->scale; // Side equals to label's width
imageLabelOtherSide = (imageLabelSide * imageHeight / imageWidth); // OtherSide is height
imageLabelYStart = (test.height() - imageLabelOtherSide) / 2;
imageLabelXStart = (test.width() - imageLabelSide) / 2;
ui->image_label->move(imageLabelXStart, imageLabelYStart);
ui->image_label->resize(imageLabelSide, imageLabelOtherSide);
} else {
imageLabelSide = test.height() * this->scale; // Side equals to label's height
imageLabelOtherSide = (imageLabelSide * imageWidth / imageHeight); // OtherSide is width
imageLabelXStart = (test.width() - imageLabelOtherSide) / 2;
imageLabelYStart = (test.height() - imageLabelSide) / 2;
ui->image_label->move(imageLabelXStart, imageLabelYStart);
ui->image_label->resize(imageLabelOtherSide, imageLabelSide);
}
}

void ImageViewer::showImage()
{
// 使用QFileDialog获得需要显示的图片的路径
QString qFileName = QFileDialog::getOpenFileName(this, "Open File", "/home/syize", "File (*)");
ui->filename_label->setText(qFileName);
// 读取图片,并让QLabel显示图片,然后设置图片自适应QLabel
QPixmap image = QPixmap(qFileName);
ui->image_label->setPixmap(image);
ui->image_label->setScaledContents(1);
this->imageHeight = image.size().height();
this->imageWidth = image.size().width();
// 获取图片的宽高并记录,然后调整QLabel大小
resizeLabel(image.size().height(), image.size().width());
}

void ImageViewer::scaleBig()
{
this->scale += 0.2;
resizeLabel(this->imageHeight, this->imageWidth);
}

void ImageViewer::scaleSmall()
{
this->scale -= 0.2;
resizeLabel(this->imageHeight, this->imageWidth);
}

void ImageViewer::clearLabel()
{
ui->image_label->clear();
ui->filename_label->clear();
}

关于如何正确缩放图片,请看 这里

Qt5 和 Qt6 的一个需要注意的地方

使用uic命令将ui文件转换成头文件以后,VS Code报错了。

image-20221008204935739

查看了一下uic的版本是5.15.6,判断是qt5-base里面带的uic,因为我的Arch上同时存在qt5qt6两个版本。

image-20221008205244393

至于qt6uic在哪里就不得而知了。经过对include路径搜索后发现,想要解决这个错误,需要将QtWidgets修改为QtGui

image-20221008205450387 image-20221008205341430

还挺不方便的。

Author: Syize

Permalink: https://blog.syize.cn/2022/02/12/qt-with-vscode/

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Syizeのblog

Comments