这个系列的摸索是在研究Qt Tutorial英文教程的基础上,克服在 macOS 上遇到的若干问题写下的记录。前面两篇我们还是在研究比较基础的模块,还没有涉及具体的 GUI 部分。从这篇开始我们来研究 GUI 开发。

1 First program, more

在第一篇文章里面我们写了一个简单的窗口程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <QApplication>
#include <QWidget>

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

QWidget window;

window.resize(250, 150);
window.setWindowTitle("Simple example");
window.show();

return app.exec();
}

这里我们将其扩展一下。

1.1 鼠标图标的例子

这个例子将展示控件的组合方式

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
// cursors
#include <QApplication>
#include <QWidget>
#include <QFrame>
#include <QGridLayout>

class Cursors : public QWidget
{
public:
Cursors(QWidget *parent = 0);
};

Cursors::Cursors (QWidget *parent)
: QWidget (parent)
{
QFrame* frame1 = new QFrame(this);
frame1->setFrameStyle(QFrame::Box);
frame1->setCursor(Qt::SizeAllCursor);

QFrame *frame2 = new QFrame(this);
frame2->setFrameStyle(QFrame::Box);
frame2->setCursor(Qt::WaitCursor) ;

QFrame *frame3 = new QFrame(this);
frame3->setFrameStyle(QFrame::Box);
frame3->setCursor(Qt::PointingHandCursor);

QGridLayout *grid = new QGridLayout(this);
grid->addWidget(frame1, 0, 0);
grid->addWidget(frame2, 0, 1);
grid->addWidget(frame3, 0, 2);

setLayout(grid);
}

int main(int argc, char*argv[]) {
QApplication app(argc, argv);

Cursors window;
window.resize(350, 150);
window.setWindowTitle("Cursors");
window.show();

return app.exec();
}

运行界面如下:

将鼠标放到不同的框里面,鼠标的图标形态会发生变化。

1.2 按钮与数据交互

我们来写一个累加累减小工具。这次我们需要写三个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// plusminus.h
#pragma once

#include <QWidget>
#include <QApplication>
#include <QPushButton>
#include <QLabel>

class PlusMinus : public QWidget {

Q_OBJECT

public:
PlusMinus(QWidget *parent = 0);

private slots:
void OnPlus();
void OnMinus();

private:
QLabel *lbl;
};
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
// plusminus.cpp
#include "plusminus.h"
#include <QGridLayout>

PlusMinus::PlusMinus(QWidget *parent)
: QWidget(parent) {
QPushButton *plsBtn = new QPushButton("+", this);
QPushButton *minBtn = new QPushButton("-", this);
lbl = new QLabel("0", this);

QGridLayout *grid = new QGridLayout(this);
grid->addWidget(plsBtn, 0, 0);
grid->addWidget(minBtn, 0, 1);
grid->addWidget(lbl, 1, 1);

setLayout(grid);

connect(plsBtn, &QPushButton::clicked, this, &PlusMinus::OnPlus);
connect(minBtn, &QPushButton::clicked, this, &PlusMinus::OnMinus);
}

void PlusMinus::OnPlus() {
int val = lbl->text().toInt();
val++;
lbl->setText(QString::number(val));
}

void PlusMinus::OnMinus() {
int val = lbl->text().toInt();
val--;
lbl->setText(QString::number(val));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.cpp
#include "plusminus.h"


int main(int argc, char *argv[]) {

QApplication app(argc, argv);

PlusMinus window;

window.resize(300, 190);
window.setWindowTitle("Plus minus");
window.show();

return app.exec();
}

在原版英文教程中没有给出编译方法即没有给出 pro 文件的内容,这里可能会有一些坑要踩。我们首先来看一下源文件中有什么不太一样的地方,然后给出编译的方法。

我们来看plusminus.h文件。首先我们要注意到Q_OBJECT这个宏。这个宏放在这里是我们使用 Qt 提供的信号与回调(槽)我也是刚开始学习,这里描述措辞后续可能需要更改的必要条件。宏的内容,实际上是定义了一些函数和属性,并且qmake编译系统在扫描到这个文件时,会自动生成实现这些函数的文件。另一需要注意的点是,在OnPlusOnMinus声明的前面有private slots字段。这里的slots也是一个特殊的宏,起作用是将其后的函数标注为可供 Qt 事件响应系统的回调函数。

要编译这几个文件,在处理 pro 文件时需要注意这么几点:

  1. 添加Headers += plusminus.h。如果没有这句话,编译系统就无法为Q_OBJECT标注的类创建需要的源文件,导致链接时出现Undefined symbols for architecture XXX类型的错误。
  2. 这里编译过程的中间临时文件比较多,建议在 pro 文件里面添加
1
2
OBJECTS_DIR=tmp
MOC_DIR=tmp

完整的 pro 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
######################################################################
# Automatically generated by qmake (3.1) Wed Sep 18 17:05:09 2019
######################################################################

TEMPLATE = app
TARGET = learnQt
INCLUDEPATH += .

# You can make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# Please consult the documentation of the deprecated API in order to know
# how to port your code away from it.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0

OBJECTS_DIR=tmp
MOC_DIR=tmp

# Input
SOURCES += main.cpp plusminus.cpp
HEADERS += plusminus.h

QT += widgets

程序运行的界面如下:

2 Menus and toobars

这里来介绍菜单和工具栏的使用。尽管在不同的操作系统下他们长的不一样,但是 Qt 提供了一致的接口。

2.1 简单的例子

下面的例子给出了最简单的 Menu 功能演示。我们分成三个文件:

1
2
3
4
5
6
7
8
9
10
11
// simplemenu.h
#pragma once

#include <QMainWindow>
#include <QApplication>

class SimpleMenu : public QMainWindow {

public:
SimpleMenu(QWidget *parent = 0);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// simplemenu.cpp
#include "simplemenu.h"
#include <QMenu>
#include <QMenuBar>

SimpleMenu::SimpleMenu(QWidget *parent)
: QMainWindow(parent) {

QAction *quit = new QAction("&Quit", this);

QMenu *file;
file = menuBar()->addMenu("&File");
file->addAction(quit);

connect(quit, &QAction::triggered, qApp, QApplication::quit);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.cpp
#include "simplemenu.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

SimpleMenu window;

window.resize(250, 150);
window.setWindowTitle("Simple menu");
window.show();

return app.exec();
}

这个例子在 Mac 上运行没法看到"File"这个菜单栏选项。为什么呢?这是因为在 MAC 中,名称为Quit的 Action 会被自动整合到名称为应用名的首个菜单项目里面去。要看到独立的File菜单选项,需要把Quit改成其他名字就好了。

图片出处:https://forum.qt.io/topic/98908/menubar-in-macos-not-working/9


Qt 的官方网文档在这里说明了原因:

Qt for macOS also provides a menu bar merging feature to make QMenuBar conform more closely to accepted macOS menu bar layout. The merging functionality is based on string matching the title of a QMenu entry. These strings are translated (using QObject::tr()) in the "QMenuBar" context. If an entry is moved its slots will still fire as if it was in the original place. The table below outlines the strings looked for and where the entry is placed if matched:

2.2 Toolbar

这部分最近应该用不到,先不实验,把教程内容搬过来

1
2
3
4
5
6
7
8
9
10
11
12
#pragma once

#include <QMainWindow>
#include <QApplication>

class Toolbar : public QMainWindow {

Q_OBJECT

public:
Toolbar(QWidget *parent = 0);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "toolbar.h"
#include <QToolBar>
#include <QIcon>
#include <QAction>

Toolbar::Toolbar(QWidget *parent)
: QMainWindow(parent) {

QPixmap newpix("new.png");
QPixmap openpix("open.png");
QPixmap quitpix("quit.png");

QToolBar *toolbar = addToolBar("main toolbar");
toolbar->addAction(QIcon(newpix), "New File");
toolbar->addAction(QIcon(openpix), "Open File");
toolbar->addSeparator();
QAction *quit = toolbar->addAction(QIcon(quitpix),
"Quit Application");

connect(quit, &QAction::triggered, qApp, &QApplication::quit);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.cpp
#include "toolbar.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

Toolbar window;

window.resize(300, 200);
window.setWindowTitle("QToolBar");
window.show();

return app.exec();
}

3 布局

这个部分开始我们来将布局方面的知识,这也是我最为关注的部分。布局有两种方式:

  • absolute positioning
  • layout managers

3.1 绝对定位

绝对定位要求显式指定各个 Widget 的位置和大小。关于绝对定位我们需要注意如下几点:

  • Widget 的位置和大小不会因为窗口 resize 而变化
  • 使用绝对定位在不同平台上看起来可能不会不一样(通常会很糟糕)
  • 改变字体会导致布局出错
  • 如果我们试图修改布局,那那么需要进行大量的重构,这是非常费时的

综合上面的原因,除了一些必须的场景以外,我们一般都是使用布局管理器。

下面是一个使用绝对布局的例子:

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
// absolute.cpp
#include <QApplication>
#include <QDesktopWidget>
#include <QTextEdit>

class Absolute : public QWidget {

public:
Absolute(QWidget *parent = 0);
};

Absolute::Absolute(QWidget *parent)
: QWidget(parent) {

QTextEdit *ledit = new QTextEdit(this);
ledit->setGeometry(5, 5, 200, 150);
}

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

Absolute window;

window.setWindowTitle("Absolute");
window.show();

return app.exec();
}

这里setGeometry()函数用来设置绝对位置坐标和大小。

下面我们开始介绍典型的布局管理器。

3.2 Box 布局

QVBoxLayout这个类将 Widget 垂直放置。Widget 通过addWidget函数添加。

1
2
3
4
5
6
7
8
9
10
// verticalbox.h
#pragma once

#include <QWidget>

class VerticalBox : public QWidget {

public:
VerticalBox(QWidget *parent = 0);
};
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
// verticalbox.cpp
#include "verticalbox.h"
#include <QVBoxLayout>
#include <QPushButton>

VerticalBox::VerticalBox(QWidget *parent)
: QWidget(parent) {

QVBoxLayout *vbox = new QVBoxLayout(this);
vbox->setSpacing(1);

QPushButton *settings = new QPushButton("Settings", this);
settings->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
QPushButton *accounts = new QPushButton("Accounts", this);
accounts->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
QPushButton *loans = new QPushButton("Loans", this);
loans->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
QPushButton *cash = new QPushButton("Cash", this);
cash->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
QPushButton *debts = new QPushButton("Debts", this);
debts->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

vbox->addWidget(settings);
vbox->addWidget(accounts);
vbox->addWidget(loans);
vbox->addWidget(cash);
vbox->addWidget(debts);

setLayout(vbox);
}

这里我们创建了五个垂直堆叠的按钮,并且让五个按钮再长、宽两个方向尽可能扩展(Expanding)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.cpp
#include "verticalbox.h"
#include <QApplication>

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

VerticalBox window;

window.resize(240, 230);
window.setWindowTitle("VerticalBox");
window.show();

return app.exec();
}

运行之后长这个样子:

类似于QVboxLayout, QHBoxLayout提供了水平排列的布局。垂直布局和水平布局可以联合起来使用。在下面这个例子中我们在窗口右下角添加两个水平分布的按钮。这就是通过混合使用垂直于水平分布实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// buttons.h
#pragma once

#include <QWidget>
#include <QPushButton>

class Buttons : public QWidget {

public:
Buttons(QWidget *parent = 0);

private:
QPushButton *okBtn;
QPushButton *applyBtn;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "buttons.h"
#include <QVBoxLayout>
#include <QHBoxLayout>

Buttons::Buttons(QWidget *parent)
: QWidget(parent) {

QVBoxLayout *vbox = new QVBoxLayout(this);
QHBoxLayout *hbox = new QHBoxLayout();

okBtn = new QPushButton("OK", this);
applyBtn = new QPushButton("Apply", this);

hbox->addWidget(okBtn, 1, Qt::AlignRight);
hbox->addWidget(applyBtn, 0);

vbox->addStretch(1);
vbox->addLayout(hbox);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <QApplication>
#include "buttons.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

Buttons window;

window.resize(290, 170);
window.setWindowTitle("Buttons");
window.show();

return app.exec();
}

下面是一个更加复杂的布局嵌套的例子:

1
2
3
4
5
6
7
8
9
10
// nesting.h
#pragma once

#include <QWidget>

class Layouts : public QWidget {

public:
Layouts(QWidget *parent = 0);
};
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
// nesting.cpp
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include "nesting.h"

Layouts::Layouts(QWidget *parent)
: QWidget (parent)
{
QVBoxLayout *vbox = new QVBoxLayout();
QHBoxLayout *hbox = new QHBoxLayout(this);

QListWidget *lw = new QListWidget(this);
lw->addItem("The Omen");
lw->addItem("The Exorcist");
lw->addItem("Notes on a scandal");
lw->addItem("Fargo");
lw->addItem("Capote");

QPushButton *add = new QPushButton("Add", this);
QPushButton *rename = new QPushButton("Rename", this);
QPushButton *remove = new QPushButton("Remove", this);
QPushButton *removeall = new QPushButton("Remove All", this);

vbox->setSpacing(3);
vbox->addStretch(1);
vbox->addWidget(add);
vbox->addWidget(rename);
vbox->addWidget(remove);
vbox->addWidget(removeall);
vbox->addStretch(1);

hbox->addWidget(lw);
hbox->addSpacing(15);
hbox->addLayout(vbox);

setLayout(hbox);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.cpp
#include <QApplication>
#include "nesting.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

Layouts window;

window.setWindowTitle("Layouts");
window.show();

return app.exec();
}

上述程序运行得到的界面如下图:

3.3 Form 布局

QFormLayout可以处理典型的表格输入的布局。其子 Widgets 被分为两列,分别是 Label 和输入控件(例如QLineEdit或者QSpinBox)。 Form 布局的使用见下面的例子:

1
2
3
4
5
6
7
8
9
10
// form.h
#pragma once

#include <QWidget>

class FormEx : public QWidget {

public:
FormEx(QWidget *parent = 0);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// form.cpp
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include "form.h"

FormEx::FormEx(QWidget *parent)
: QWidget(parent) {

QLineEdit *nameEdit = new QLineEdit(this);
QLineEdit *addrEdit = new QLineEdit(this);
QLineEdit *occpEdit = new QLineEdit(this);

QFormLayout *formLayout = new QFormLayout;
// 调整Label的对齐规则为水平右对齐,垂直居中
formLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
formLayout->addRow("Name:", nameEdit);
formLayout->addRow("Email:", addrEdit);
formLayout->addRow("Age:", occpEdit);

setLayout(formLayout);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.cpp
#include <QApplication>
#include "form.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

FormEx window;

window.setWindowTitle("Form example");
window.show();

return app.exec();
}

程序运行得到的界面如下:

3.4 网格布局

网格布局使用QGridLayout这个类,我们已经在前面的例子中见过了。这是一个强大的布局工具。英文教程中给了两个例子,其中 Review 那个例子要更加复杂一些,我们使用那个来说明:

1
2
3
4
5
6
7
8
9
10
// review.h
#pragma once

#include <QWidget>

class Review : public QWidget {

public:
Review(QWidget *parent = 0);
};
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
// review.cpp
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QTextEdit>
#include "review.h"

Review::Review(QWidget *parent)
: QWidget(parent) {

QGridLayout *grid = new QGridLayout(this);
grid->setVerticalSpacing(15);
grid->setHorizontalSpacing(10);

QLabel *title = new QLabel("Title:", this);
grid->addWidget(title, 0, 0, 1, 1);
title->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

QLineEdit *edt1 = new QLineEdit(this);
grid->addWidget(edt1, 0, 1, 1, 1);

QLabel *author = new QLabel("Author:", this);
grid->addWidget(author, 1, 0, 1, 1);
author->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

QLineEdit *edt2 = new QLineEdit(this);
grid->addWidget(edt2, 1, 1, 1, 1);

QLabel *review = new QLabel("Review:", this);
grid->addWidget(review, 2, 0, 1, 1);
review->setAlignment(Qt::AlignRight | Qt::AlignTop);

QTextEdit *te = new QTextEdit(this);
grid->addWidget(te, 2, 1, 3, 1);

setLayout(grid);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <QApplication>
#include "review.h"

int main(int argc, char *argv[]) {

QApplication app(argc, argv);

Review window;

window.setWindowTitle("Review");
window.show();

return app.exec();
}

这里使用QGridLayout::addWidget参数的时候,除了行列号以外,剩下两个数字分别表示行列的 Span。函数定义:

程序运行得到的界面是