0%

qt_getting_started

Qt5入门

日期:2020/07/24

学习参考:Bilibili

这段时间帮导师做一个项目的时候,正好用到有关Qt5的一些技能。趁此机会稍微入门了一下,感觉跟C++搭配以后Qt会经常用到,就写了一个很简单的框架,以后有机会会进一步完善。Qt-model-github

学Java的时候学过不少前端的知识,用法其实大同小异,不过Qt使用的时候真的很有一种在写Java的感觉。这里介绍和归纳一些简单入门知识点,方便日后速查。

1. Window Widgets

构成Qt基本框架的有几个基本的外层容器控件(应用&窗体)。我比较喜欢的外层嵌套方法为:QApplication > QMainWindow/QWidget >QViewer

pic1.jpg

在Qt中,所有的类都继承自QObject,大部分的子控件都是QWidget的子类。通常,我们作主窗口时,使用的是QMainWindow 而不是QWidget,原因在于QMainWindow能够显示QMenuBarQToolbar

这是一个简单的基本窗口:

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
// main.cpp
// ...
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
MyMainWindow w;
w.show();C
return app.exec();
}

// ...

// MainWindow
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
// ...
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr) {
setWindowTitle("[[Window Title]");
setWindowIcon(QIcon(("../icons/title_icon.ico")));
setGeometry(300, 300, 960, 640);

// initialize viewer
_viewer = new MyViewer;
this->setCentralWidget(_viewer);
};
~MainWindow() {
delete _viewer;
};
MyViewer* _viewer;
private slots:
void slotOpen();
};
#endif // MAINWINDOW_H

// ...
// MyViewer
#ifndef MYVIEWER_H
#define MYVIEWER_H

class MyViewer : public QWidget {
Q_OBJECT
public:
explicit MyViewer(QWidget *parent = nullptr) {
// add layout and widget
// ...
};

void testLayout() {
// 可用于显示viewer的占用布局
QPalette pal;
pal.setColor(QPalette::Background, Qt::red);
setAutoFillBackground(true);
setPalette(pal);
}
};
#endif // MYVIEWER_H

也可以根据具体的使用场景,使用QApplicationQMainWindowQViewer的继承子类,实现更多定制化的功能(如事件的响应)。

在MainWindow的构造函数中,可以添加QMenuBar、QToolBar和QStatusBar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// set menubar
QMenuBar* _menubar = menuBar();
QMenu* _menu_1 = _menubar->addMenu("File");
QAction* _action_1 = _menu_1->addAction("Open", this, SLOT(close()), QKeySequence::Open);
_action_1->setToolTip("Click to close window");
_menu_1->addSeparator();
// ...

// set toolbar
QToolBar* _toolbar = addToolBar("My Toolbar");
_toolbar->addAction(_open_action);

// set statusbar
QStatusBar* _statusbar = statusBar();
QLabel* _label;
_statusbar->addWidget(_label = new QLabel());
_label->setText("<font style='bold'>Ready</font>");


2. Widgets

下面列举出的是Qt用到的一些主要控件:

QPushButton
1
2
3
4
5
6
7
8
9
QPushButton button;
button.setText("Button Name");
button.setParent(this);
// or use QPushButton button = new QPushButton("Button Name", this);
QObject(&button, SIGNAL(clicked()), &obj, SLOT(slotFunction()));
// obj must realize slot function
// or you can use lambda expression
button->setStyleSheet("QPushButton { font:bold 16px; color: red}"); // 该函数支持css语法
button->setDefault(this); // 设置当前默认处理的控件,可配合键盘响应事件使用

QLabel
1
2
3
4
5
6
QLabel label;
label.setText("<h1>Qt</h1>"); // Qt在设置text的时候支持html语法是真的棒
label.setPixmap(QPixmap("../pix.jpg"));
connect(label, &QLabel::linkActivated, [](QString str) {
qDebug() << str;
});

QLineEdit & QCompleter
1
2
3
4
5
6
7
8
9
QLineEdit edit;
edit.setParent(this);
edit.setEchoMode(QLineEdit::PasswordEchoOnEdit);
qDebug() << edit.text();
edit.setPlaceholderText("Please input text: ");

QCompleter co({ "abc", "123", "998" });
co.setFilterMode(Qt::MatchContains);
edit.setCompleter(&co);

QTextEdit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
QTextEdit edit;
edit.setText("abc");
// 硬核
// 可以通过输入html代码的方式在TextEdit中显示表格
edit.setText("<table border=1><tr><th>head1</th><th>head2</th></tr>"
"<tr><td>data1</td><td>data2</td></tr>"
"<tr><td>value1</td><td>value2</td></tr>"
"</table><br>"
"<img src='../aaa.jpg'></img>");
connect(edit, &QTextEdit::textChanged, [&]() {
QTextEdit* _edit = (QTextEdit*)this->sender(); // 获取事件的发起者
qDebug() << _edit->toPlainText();
});
edit.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); // 设置滚动条的显示方案

QRadioButton
1
2
3
4
5
QRadioButton radio;
radio.setText("Radio");
connect(radio, &QRadioButton::clicked, [](bool v) {
qDebug() << v;
});

QCheckBox
1
2
3
QCheckBox check;
check.setParent(this);
check.isChecked();

QComboBox
1
2
3
4
5
6
QComboBox* combo = new QComboBox();
combo->addItem("Select Item 1");
combo->addItem("Select Item 2");
combo->setEditable(true);
connect(combo, SIGNAL(currentIndexChanged(QString)), this, SLOT(slotComboBoxIndexChanged(QString)));
combo->setCompleter(new QCompleter(combo->model()));

QGroupBox
1
2
QGroupBox* group = new QGroupBox("Some items");
group->setLayout(new QHBoxLayout());

QSlider & QSpinBox
1
2
3
4
5
6
7
8
9
10
11
QSlider* slider = new QSlider();
slider->setMaximum(100);
slider->setMinimum(0);
slider->setOrientation(Qt::Horizontal);

QSpinBox* spinBox = new QSpinBox();
spinBox->setMaximum(100);
spinBox->setMinimum(0);

// 绑定 qslider和qspinbox
connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int)));

QDateTimeEdit
1
2
3
QDateEdit* date = new QDateEdit();
QTimeEdit* time = new QTimeEdit();
QDateTimeEdit* dateTimeEdit = new QDateTimeEdit();

QLCDNumber
1
2
3
4
5
// LCD数据显示
QLCDNumber* lcd = new QLCDNumber(10);
lcd->display(123);
lcd->setMode(QLCDNumber::Hex);
lcd->setSegmentStyle(QLCDNumber::Outline); // 可以用在嵌入式领域


3. Layout

Qt中用到的几个Layout这要以QHBoxLayout、QVBoxLayout和QGridLayout为主。这里有一个比较重要的概念:【弹簧】,即Stretch,顾名思义,可以理解成通过在没有控件的地方放置弹簧使得布局尽可能地撑开,能够做到保持控件的大小不变而通过弹簧的拉伸改变空白区域的大小。(灵魂画手上线(●ˇ∀ˇ●))

pic2
QHBoxLayout & QVBoxLayout

这两个的用法基本都是相似的,注意不要忘了把Layout对象放到父类容器里去就行。

1
2
3
4
5
6
7
// QHBoxLayout和QVBoxLayout使用方法相同
QHBoxLayout layout;
layout.addStretch(1); // 设置弹簧
layout.addWidget(new QPushButton("button"), 1);
layout.addSpacing(50);
layout.addStretch(1);
_view->setLayout(&layout);

QGridLayout
1
2
3
4
5
6
7
8
9
10
11
12
13
// 一个简单的5*4的GridLayout
QGridLayout layout;
layout.setColumnStretch(0, 1);
layout.setColumnStretch(3, 1);
layout.setRowStretch(0, 1);
layout.setRowStretch(4, 1);
layout.addWidget(new QLabel("Username: "), 1, 1);
layout.addWidget(new QLineEdit(), 1, 2);
layout.addWidget(new QLabel("Password: "), 2, 1);
layout.addWidget(new QLineEdit(), 2, 2);
// 指定行&列数放置控件
// 后两个参数表示在行&列上各跨几格
layout.addWidget(new QPushButton("Login"), 3, 2, 1, 2);

比较实用的布局方式

自己做的Qt框架可能用来显示渲染的模型比较多,所以自己比较推荐的一种布局方式是最外层用QHBoxLayout在水平的布局上做一个大致的比例规划,一般左2右1(分别对应显示界面和操作面板)。在操作面板上可以使用QVBoxLayout放不同的操作模块。在每个操作模块内部,再使用QGridLayout来放置相应的控件,比较方便。实现出的样子如下图:

pic3

4. Event&EventFilter

Qt消息机制的大概处理流程为:先由QApplication得到具体处理该消息的窗口,再通过event()->event()来根据消息类型调用该消息具体的虚函数。我们可以重载event()函数来处理或截取消息,也可以通过重载具体的虚函数来实现对消息的响应。(如鼠标、键盘消息等)

重写event()截取消息
1
2
3
4
5
6
7
8
9
bool MyWidget::event(QEvent *event){
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "Mouse Pressed";

// 这里返回true则截断当前鼠标消息
//return true;
}
return QWidget::event(event);
}

closeEvent()
1
2
3
void MyWidget::closeEvent(QCloseEvent* e){
qDebug() << "Close window";
}

mouseEvent()
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
void MyWidget::mousePressEvent(QMouseEvent *event){
QPoint pt = event->pos();
qDebug() << pt;

if (event->button() == Qt::LeftButton){
qDebug() << "Press left button";
}

if (event->modifiers() == Qt::ShiftModifier){ // 通过逻辑判断还能够对键盘事件同步处理
qDebug() << "Shift press.";
return;
}

if (event->modifiers() == Qt::ControlModifier){
// handle with control
return;
}

}

void MyWidget::mouseReleaseEvent(QMouseEvent *event){

}

void MyWidget::mouseMoveEvent(QMouseEvent *event){
static int i=0;

// 在父窗体设置: this->setMouseTracking(true); 则鼠标不要按下,mouseMove就能调用
qDebug() << "mouse move" << i++; // 需要点击鼠标后才有反应
}

keyEvent()
1
2
3
4
5
6
7
8
9
void MyWidget::keyPressEvent(QKeyEvent *e){
e->modifiers();
// 获取输入按键信息
int key = e->key();
qDebug() << key;
qDebug() << (char)key;
}

void MyWidget::keyReleaseEvent(QKeyEvent *e){}

paintEvent()
1
2
3
4
5
// 窗体载入时则调用该事件
void MyWidget::paintEvent(QPaintEvent *e) {
QPainter p(this); // 可以画在窗口上或者图片上
p.drawLine(QPoint(0, 0), QPoint(100, 100));
}

EventFilter

控件可以通过给自己安装一个消息过滤器来过滤自己触发的消息。放入的过滤器对象需要实现一个eventFilter函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_button = new QPushButton("This button", this);
connect(_button, SIGNAL(clicked()), this, SLOT(close()));
_button->installEventerFileter(this);

// ...

// this 中定义的eventFilter
bool MyWidget::eventFilter(QObject *o, QEvent *e) {

if (o == (QObject*)_button && (
e->type() == QEvent::MouseButtonPress ||
e->type() == QEvent::MouseButtonDblClick)){
return true; // 返回true表示过滤消息
} // 通过该方法阻止了_button按钮_close()事件的触发

return QWidget::eventFilter(o, e);
}

notify()

QApplication对象或者它的子类对象能够重写notify函数来实现消息的传递,广播到所有的控件。

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
bool MyApplication::notify(QObject *o, QEvent *e) {

if (this->topLevelWidgets().count()>0){
QWidget* mainwnd = this->topLevelWidgets().at(0);
if (o==(QObject*)mainwnd && e->type() == QEvent::MouseButtonPress) {
// do...
qDebug() << "Mainwnd is clicked";
}
}

return QApplication::notify(o, e);
}

// 调用方法
app.postEvent(&main_window, new QEvent(QEvent::User)); // 使用postEvent主程序不会等待子控件的接受
app.sendEvent(&main_window, new QEvent(QEvent::User)); // 使用sendEvent主程序会等待子控件接受后再执行

// Widget响应该消息的方法
bool MyWidget::event(QEvent *e) {
if (e->type() == QEvent::User){ // 通过判断event类型来进行对应的过滤
qDebug() << "User event is coming";
}

return QWidget::event(e);
}


5. Signals and Slots

区别于Java,Qt内设置控件事件监听的方式是通过绑定【信号函数】和【槽函数】的方式。

常规的使用
1
2
3
4
5
6
7
_button = new QPushButton("This button", this);
connect(_button, SIGNAL(clicked()), this, SLOT(close()));

// 可以通过Lambda表达式的方式调用
connect(_button, &QPushButton::clicked, [](){
// ...
});

Signal对象和Slot对象
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
// 先通过实例化两个对象直接进行信号和槽的联动
MySignal sig;
MySlot slot;

QObject::connect(&sig, SIGNAL(sig()), &slot, SLOT(slot()));
emit sig.sig();

// 下面是信号类的定义
#ifndef MYSIGNAL_H
#define MYSIGNAL_H

#include <QObject>
class MySignal : public QObject {
Q_OBJECT
public:
explicit MySignal(QObject *parent = nullptr);
signals:
void sig(); // 不需要实现
};
#endif // MYSIGNAL_H

// 下面是槽类的定义
#ifndef MYSLOT_H
#define MYSLOT_H

#include <QObject>
class MySlot : public QObject {
Q_OBJECT
public:
explicit MySlot(QObject *parent = nullptr);
signals:
public slots:
void slot(){ qDebug() << "I'm a slot." }; // 需要实现
};

#endif // MYSLOT_H

加一段课上记得笔记:

注意:
1)信号的定义必须保留在signals:保留字下,并且不需要实现
2)槽的定义必须在slots:保留字下,需要实现
3)信号和槽通过QObject::connect函数连接
4)当信号被触发时,槽函数被调用

另外:
1)信号和槽,是Qt的拓展,所以实现信号和槽的类,必须是QObject的子类
2)实现信号和槽的累,必须以宏Q_OBJECT开始
3)连接信号和槽,要用到SIGNAL和SLOT宏,转换函数为字符串
4)一个信号可以和多个槽连接,槽函数调用的顺序是不确定的
5)多个信号可以同时连接一个槽
6)信号可以连接信号,形成信号传导
7)信号和槽的参数应该一样多,而且类型必须相同
8)信号和槽都可以重载
9)信号和槽都可以有默认参数
10)槽函数可以像普通函数一样被调用
11)在槽函数中,调用sender可以获得信号调用者

总结:
1)一个类:Object
2)三个宏:Q_OBJECT SIGNAL SLOT
3)三个保留字:signals, slots, emit



6. Dialog

在使用dialog的时候,常常会涉及到一个模态框的概念。模态框出现的时候,当前应用的窗体是以堆栈的形式呈现给用户的,也就是说我们能操作到的窗口只有当前最顶层的窗口。通常我们在main函数的app对象中使用widget.show()展现的并不是一个模态框。

在模块对话框中,exec()有自己的消息循环,并且把app的消息循环接管了。如果dialog是通过exec()来显示,那么可以通过accept()或者reject()来关闭窗口。如果dialog是通过show()来显示,那么可以通过close()来关闭窗口(同QWidget)。

1
2
3
4
5
6
7
8
// 该内容往往放在某个控件的槽函数中
QDialog* dlg = new QDialog;
QPushButton* button = new QPushButton(dlg); // 把button放入dlg中,并指定点击button为accept
connect(button, SIGNAL(clicked()), dlg, SLOT(accept()));

int res = dlg->exec();
if (res == QDialog::Accepted) qDebug() << "Accepted";
else if (res == QDialog::Rejected) qDebug() << "Rejected";

Qt中也内置了很多不同的Dialog,其具体的显示方式与计算机的操作系统有关。下面介绍几个比较常用的:

QFileDialog & QFileInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获得文件地址
QString file_name = QFileDialog::getOpenFileName(NULL, "select pic file", _strDir, "pic file (*.png *.jpg)");


// 获得文件所在文件夹的地址
QString file_name = QFileDialog::getExistingDirectory();
qDebug() << file_name;

// 判空
if(file_name.isEmpty()){
qDebug() << "select none";
return;
}

// 获取文件信息
QFileInfo fileInfo(file_name);
_strDir = fileInfo.filePath(); // 通过使用QFileInfo能够解决路径的跨平台问题。记住_strDir后下一次可以直接进入


// 保存文件
QString file_name = QFileDialog::getSaveFileName(NULL, "Select file for save", _strDir, "Png file (*.png)");
QString file_name = QFileDialog::getSaveFileName(NULL, "Select file for save", _strDir, "Pic file (*.png *.jpg)");

QColorDialog
1
2
3
QColorDialog color;
color.exec();
QColor c = color.selectedColor();

QFontDialog
1
2
3
QFontDialog font;
font.exec();
QFont f = font.selectedFont();

QMessageBox
1
2
3
4
5
6
7
8
9
10
11
QMessageBox::warning(this, "Error", "Error msgs...");
QMessageBox::information(this, "Error", "Error msgs...");
QMessageBox::critical(this, "Error", "Error msgs...");
// 通过'|'可以放入不同的选择按钮
int ret = QMessageBox::question(this, "???", "ready?", QMessageBox::Yes | QMessageBox::No | QMessageBox::YesAll);
// int ret = QMessageBox::question(this, "???", "ready?", QMessageBox::Yes, QMessageBox::No, QMessageBox::YesAll);
if (ret == QMessageBox::Yes){
qDebug() << "Yes";
}else if (ret == QMessageBox::No){
qDebug() << "No";
}

QInputDialog
1
2
QInputDialog input;
input.exec();


7. Qt Designer

Qt还提供了相应的Qt Designer来进行前端的编辑,有点像Android界面的编辑。可视化的交互界面确实十分方便,你也能够同时为控件添加相应的槽函数(可以参考Qt Designer里槽函数的一些命名规范)。Qt Designer也将前端的界面定义为*.ui格式的XML文件,上手十分快,这里就不过多介绍。 (虽然是比较好用,但自己对*.ui文件的关联绑定这一块确实还不太清楚,先老老实实地直接写在MainWindow里好了,也方便跨平台的调试)