引例
#include
#include
int main(int argc, char* argv[])
{
//创建QApplication实例,这是必不可少的
QApplication app(argc, argv);
//创建QLabel实例,对构造函数赋值Hello, world文本,这是为了能在QLabel上显示这一条文本
QLabel label("Hello, world");
//调用show()方法将其显示出来
label.show();
//调用app.exec(),从而开启事件循环,试想,若不开启事件循环,那么main函数会立即退出,QLabel对象就不能一直显示了
return app.exec();
}
补充:
#include
#include
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
//不建议这样做
QLabel* label = new QLabel("Hello, world");
label->show();
return app.exec();
}
所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,用自己的一个函数(成为槽(slot))来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)
例子:
#include
#include
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
//按钮在QT中被称为QPushButton
QPushButton firstButton("Quit");
QObject::connect(&firstButton, &QPushButton::clicked, &QApplication::quit);
firstButton.show();
return app.exec();
}
详解QObject::connect():
该函数让我们连接系统提供的信号和槽
该函数最常用的一般形式:
connect(sender, signal, receiver, slot);
sender:发出信号的对象
signal:发送对象发出的信号
receiver:接收信号的对象
slot:接收对象在接收信号后会自动调用的函数
sender对象发出信号signal后,会自动调用receiver对象的slot函数
在Qt中,有五个重载:
//将signal和slot作为字符串处理
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
//可以将每个函数视为QMetaMethod的子类,该写法可使用QMetaMethod来进行类型对比
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
//可见缺少了receiver,这是将this指针作为receiver
QMetaObject::Connection connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const;
//PointerToMemberFunction,指向成员函数的指针
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
//Functor类型,可接受static函数、全局函数和Lambda表达式
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
将一个信号连接到Lambda表达式:
经典的观察者模式在讲解举例的时候通常会举报纸和订阅者的例子。有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。在这个例子中,观察者是Subscriber,被观察者是Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如subscriber.registerTo(newspaper))。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(newspaper.notifyAllSubscribers())。
使用QT信号槽机制来实现上述机制:
//main.cpp
#include
#include "newspaper.h"
#include "reader.h"
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
Newspaper newspaper("Newspaper A");
Reader reader;
QObject::connect(&newspaper, &Newspaper::newPaper,
&reader, &Reader::receiveNewspaper);
newspaper.send();
return app.exec();
}
//Reader.h
#include
#include
class Reader : public QObject
{
Q_OBJECT
public:
Reader() {
}
//槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
void receiveNewspaper(const QString& name)
{
qDebug() << "Receives Newspaper: " << name;
}
};
//Newspaper.h
#include
//只有继承自QObject的类,才有信号槽的能力
class Newspaper : public QObject
{
//凡是Object类,都应在第一行添加Q_Object宏
//特别注意,这个宏需放到头文件中
Q_OBJECT
public:
Newspaper(const QString& name) :
m_name(name)
{
}
void send()
{
//emit是QT对c++的扩展,是一个关键字,其实也是一个宏
//emit的含义:发出newPaper()信号,为了让接收者知道是谁发出的信号,于是将报纸名字作为参数传给这个信号
//接收者连接该信号时,通过槽函数获取实际值,完成数据从发出者到接收者的一个转移
emit newPaper(m_name);
}
//该模块列出信号,信号是一个个函数名,返回值为void,参数是该类想要让外界知道的数据,不需要添加任何实现
signals:
void newPaper(const QString& name);
private:
QString m_name;
};
Qt5模块分为两部分:
Essentials Modules:所有平台均可使用
Add-on Modules:建立在基础模块的基础上,可选择性引入
详见:Qt模块
QMainWindow是Qt预定义好的主窗口。
主窗口:普通应用程序最顶层的窗口,例如,在使用浏览器时,主窗口就是这个浏览器窗口
vs2019下:
#include
#include
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QMainWindow win;
win.show();
return app.exec();
}
QAction类:代表窗口的动作
该类抽象出公共的动作,在菜单栏和工具栏中使用
包含:图标、菜单文字、快捷键、状态栏文字、浮动帮助等信息
代码:
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget* parent = 0);
~MainWindow();
private:
void open();
QAction* openAction;
};
#endif
//mainwindow.cpp
#include
#include
#include
#include
#include
#include "mainwindow.h"
//该行说明 MainWindow 的构造函数之前需要调用父类 QMainWindow 的带有参数的构造函数
MainWindow::MainWindow(QWidget* parent) :
QMainWindow(parent)
{
//设置窗口主标题
//tr()函数:用于Qt国际化的函数
setWindowTitle(tr("Main Window"));
//在堆上创建openAction对象,并对其构造函数传入一个图片,一个文本和this指针
//QIcon:传入值是一个字符串,对应Qt资源文件中的一个路径,意味着从资源文件中查找资源
//Qt内置支持的图片格式是png,其它格式的图片入jpg,gif则需要插件支持
//第二个参数:文本值前有一个&,意味这将成为一个快捷键,注意截图中File有一个下划线
openAction = new QAction(QIcon(":/images/image1"), tr("&Open..."), this);
//使用setShortcuts()函数,用于说明QAction的快捷键
//QKeySequence定义了很多内置的快捷键,比如下面的Open,这样添加快捷键,会根据平台不同来自动定义相应快捷键
openAction->setShortcuts(QKeySequence::Open);
//setStatusTi()实现当用户鼠标划过Action时,会在下方状态栏产生的提示
openAction->setStatusTip(tr("Open an existing file"));
//将QAction的triggered()信号与MainWindow类的open()连接起来
connect(openAction, &QAction::triggered, this, &MainWindow::open);
//menuBar(),toolBar()和statusBar()是三个QMainWindow的内置函数,用于创建窗口的相应部分
//向菜单栏添加一个FIle菜单,并将openAction添加到这个菜单
QMenu* file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
//新增加一个工具栏,将openAction添加到这个菜单
QToolBar* toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
statusBar();
}
MainWindow::~MainWindow()
{
}
void MainWindow::open()
{
QMessageBox::information(this, tr("Information"), tr("Open"));
}
//main.cpp
#include
#include "mainwindow.h"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
MainWindow win;
win.show();
return app.exec();
}
参考:
资源管理
Qt”扩展“了c++:
C++原有对象模型在特定情况下捉襟见肘,不能满足GUI界面要求,为此,在C++编译器编译Qt源程序之前,Qt首先使用moc(Meta Object Compiler)对Qt源代码做一次预处理,此次处理放生在C++预处理器工作之前。
信号函数就是在通过moc处理后才能通过标准C++的编译的。
moc为标准C++增加了一些特性:
信号槽机制,用于解决对象之间的通讯,这个我们已经了解过了,可以认为是 Qt 最明显的特性之一;
可查询,并且可设计的对象属性;
强大的事件机制以及事件过滤器;
基于上下文的字符串翻译机制(国际化),也就是 tr() 函数,我们简单地介绍过;
复杂的定时器实现,用于在事件驱动的 GUI 中嵌入能够精确控制的任务集成;
层次化的可查询的对象树,提供一种自然的方式管理对象关系。
智能指针(QPointer),在对象析构之后自动设为 0,防止野指针;
能够跨越库边界的动态转换机制。
通过继承QObject类,可以获得这些特性。
更多详见:对象模型
组件定位:所谓GUI界面,是一堆组件的叠加,我们应该把组件放在正确的地方, 有两种定位方式可以实现:绝对定位和布局定位。
绝对定位:给出组件坐标和长宽
问题:用户改变窗口大小时,组件不会有相适应的响应
布局定位:由专门的管理器管理,适应调整大小和位置
引入两个新组件:
QSpinBox:只能输入数字,带有上下箭头的步进按钮
QSlider:带滑块的滑杆
布局管理器:
QHBoxLayout:按照水平方向从左到右布局;
QVBoxLayout:按照竖直方向从上到下布局;
QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;
QFormLayout:按照表格布局,每一行前面是一段文本,文本后面跟随一个组件(通常是输入框),类似 HTML 的 form;
QStackedLayout:层叠的布局,允许我们将几个组件按照 Z 轴方向堆叠,可以形成向导那种一页一页的效果。
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("Enter your age");
QSpinBox* spinBox = new QSpinBox(&window);
QSlider* slider = new QSlider(Qt::Horizontal, &window);
//创建实例后,用setRange管理其范围
spinBox->setRange(0, 130);
slider->setRange(0, 130);
QObject::connect(slider, &QSlider::valueChanged, spinBox, &QSpinBox::setValue);
//若直接用如下写法,编译器会报错:
//QObject::connect(spinBox, &QSpinBox::valueChanged, slider, &QSlider::setValue);
void (QSpinBox:: * spinBoxSignal)(int) = &QSpinBox::valueChanged;
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
spinBox->setValue(35);
QHBoxLayout* layout = new QHBoxLayout;
layout->addWidget(spinBox);
layout->addWidget(slider);
window.setLayout(layout);
window.show();
return app.exec();
}
实际开发中,QMainWindow通常只作为主窗口,对话框更多只使用QDialog类,该类缺少一些方便的函数,如:menuBar()和toolBar()。
QMenuBar:表示菜单的类
addMenu():可以为其添加菜单,可以只提供一个字符串作为参数
&:可以为菜单创建有一个快捷键
addAction():添加活动
QToolBar:表示工具栏的类
addToolBar():
对话框:一个顶层窗口,出现在程序最上层,用于实现短期任务或简洁的交互
用QDialog类实现对话框
QDialog(及其子类,以及所有Qt::Dialog类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。
//注意parent指针(本例中指的时this)的有无对QDialog实例的影响
void MainWindow::open()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Hello,dialog"));
dialog.exec();
}
Qt支持模态对话框和非模态对话框:
模态对话框:组赛同一应用程序中其它窗口的输入
非模态对话框:
Qt支持两种级别的模态:
应用程序级别和窗口级别
应用程序级别:当该模态对话框出现时,用户必须先与该对话框交互,而不能与应用程序中其它窗口交互。
窗口级别:该模块仅阻塞与该对话框关联的窗口,可以同应用中其它窗口交互
QDialog::exec():实现应用程序级别模态
QDialog::open():实现窗口级别模态
QDialog::show():实现非模态
特别注意:
show()不会阻塞线程,对话框显示,则函数立即返回,代码继续执行
为了解决此问题,应在堆上建立dialog:
void MainWindow::open()
{
QDialog *dialog = new QDialog;
dialog->setWindowTitle(tr("Hello, dialog!"));
dialog->show();
}
补充参考:对话框
数据传递:
对话框与主窗口之间的数据交互相当重要
对话框分为模态和非模态两种,两种与主窗口间的数据交互不同
对于使用了exec()显示的模态对话框,我们可以在exec()函数之后直接从对话框的对象获取到数据值。
void MainWindow::open()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Hello, dialog!"));
dialog.exec();
qDebug() << dialog.result();
}
qDebug()类似于std::cout
exec()开始了一个事件循环,代码被阻塞到这里。由于exec()函数没有返回,因此下面的result()函数也就不会被执行。直到对话框关闭,exec()函数返回,此时,我们就可以取得对话框的数据。
如果我们设置 dialog 的属性为WA_DeleteOnClose,那么当对话框关闭时,对象被销毁,我们就不能使用这种办法获取数据了。在这种情况下,我们可以考虑使用 parent 指针的方式构建对话框,避免设置WA_DeleteOnClose属性;或者是利用另外的方式。
QDialog::exec()是有返回值的,其返回值是QDialog::Accepted或者QDialog::Rejected
常规写法:
根据用户是点击了“确定”还是“取消”来判断
QDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
// do something
} else {
// do something else
}
非模态对话框,QDialog::show()函数会立即返回,如果我们也这么写,就不可能取得用户输入的数据。
因为show()函数不会阻塞主线程,show()立即返回,用户还没有来得及输入,就要执行后面的代码,当然是不会有正确结果的。
使用信号槽机制:
由于非模态对话框在关闭时可以调用QDialog::accept()或者QDialog::reject()或者更通用的QDialog::done()函数,所以我们可以在这里发出信号。另外,如果找不到合适的信号发出点,我们可以重写QDialog::closeEvent()函数,在这里发出信号。在需要接收数据的窗口(这里是主窗口)连接到这个信号即可。类似的代码片段如下所示:
void UserAgeDialog::accept()
{
emit userAgeChanged(newAge); // newAge is an int
QDialog::accept();
}
// in main window:
void MainWindow::showUserAgeDialog()
{
UserAgeDialog *dialog = new UserAgeDialog(this);
connect(dialog, &UserAgeDialog::userAgeChanged, this, &MainWindow::setUserAge);
dialog->show();
}
// ...
void MainWindow::setUserAge(int age)
{
userAge = age;
}
QFileDialog
文件对话框:
QString getOpenFileName() :选择打开一个文件
QStringList getOpenFileNames() :选择打开多个文件
QString getSaveFileName() :选择保存一个文件
QString getExistingDirectory() :选择一个己有的目录
QUrl getOpenFileUrl():选择打幵一个文件,可选择远程网络文件
QcolorDialog
颜色对话框:
QColor getColor():选择颜色
QFontDialog
字体对话框:
QFont getFont():选择字体
QinputDialog
输入对话框:
QString getText():输入单行文字
int getlnt() :输入整数
double getDouble():输入浮点数
QString getltem():从一个下拉列表框中选择输入
QString getMultiLineText(): 输入多行字符串
QMessageBox
消息框:
QMessageBox详解:
if (QMessageBox::Yes == QMessageBox::question(this,
tr("Question"),
tr("Are you OK?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes)) {
QMessageBox::information(this, tr("Hmmm..."), tr("I'm glad to hear that!"));
} else {
QMessageBox::information(this, tr("Hmmm..."), tr("I'm sorry!"));
}
QMessageBox类的 static 函数优点是方便使用,缺点也很明显:非常不灵活。我们只能使用简单的几种形式。为了能够定制QMessageBox细节,我们必须使用QMessageBox的属性设置 API。如果我们希望制作一个询问是否保存的对话框,我们可以使用如下的代码:
QMessageBox msgBox;
msgBox.setText(tr("The document has been modified."));
msgBox.setInformativeText(tr("Do you want to save your changes?"));
msgBox.setDetailedText(tr("Differences here..."));
msgBox.setStandardButtons(QMessageBox::Save
| QMessageBox::Discard
| QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Save);
int ret = msgBox.exec();
switch (ret) {
case QMessageBox::Save:
qDebug() << "Save document!";
break;
case QMessageBox::Discard:
qDebug() << "Discard changes!";
break;
case QMessageBox::Cancel:
qDebug() << "Close document!";
break;
}
使用了一个detailedText,也就是详细信息,当点击了详细信息按钮时,对话框可以自动显示更多信息。
深入 Qt5 信号槽新语法