目录
1.创建工程时基类的选择
2.第一个QT程序
3.创建一个按钮
4.对象树简单理解
5.信号和槽
5.1自定义信号槽
5.2信号连接信号
5.3信号函数和槽函数的注意事项
5.4配合lambda表达式
在创建工程时会被要求选择一个基类:
这里有三个基类可供选择,分别是"QMainWindow"、"QWidget"、"QDialog"。它们的关系是:
选择"QWidget"作为基类,并且不勾选"Generate form"选项,得到的工程文件如下:
.pro文件是以qmake构建的工程文件,它描述了当前工程的一些信息。该文件内的内容和解释如下图:
这些都是默认生成的。如果要使用网络通信模块,那么还应该加载"network"模块。
main.cpp是该项目的入口,它的内容和解释如下图:
如果开发过服务器,那么这几句代码很容易理解。"a.exec()"就是一个事件监听循环。
widget.h是项目自动生成的派生类,它继承自"QWidget"。它的内容和解释如下图:
widget.cpp是上述类的成员函数实现,它的内容如下图:
运行该程序,得到如下结果:
可见,输出结果就是一个空白窗口,并且不是一闪而过,这是因为main.cpp中做了事件监听循环处理。
在QT当中,按钮也封装成了单独的类,它的相关说明如下图:
可以看到,QPushButton类继承自QAbstractButton类,而QAbstractButton又继承自QWidget类,如下图所示:
这说明按钮可以在单独的窗口当中打开,代码如下:
#include "widget.h"
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton;// 创建一个按钮对象
btn->show();// 单独打开一个窗口显示
w.show();
return a.exec();
}
最终的运行结果是这样的:
那么想要让按钮依附于已经存在的窗口,只需要指定"父亲"即可,代码如下:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton("我的按钮",&w);// 构造函数当中可以指定按钮的文字和"父亲"
//btn->setParent(&w);// 另一种方法指定"父亲"
//btn->setText("我的按钮");// 另一种方法指定按钮的文字
w.show();
return a.exec();
}
最终效果如下:
还可以更改按钮的大小和位置,代码如下:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton("我的按钮",&w);
btn->resize(200,300);// 设置按钮的大小
btn->move(200,300);// 移动到(200,300)位置
w.show();
return a.exec();
}
运行效果如下:
关于QPushButton类还有很多用法,篇幅有限就不一一列举了,可以在平时的练习和项目当中发现更多有意思的东西。
观察上面的代码可以发现,它们不是一份合格的C++代码,因为它们看起来有内存泄漏:
那么给它加上delete会发生意想不到的事:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget *wid = new Widget;
QPushButton *btn = new QPushButton("我的按钮",wid);
wid->show();
delete wid;
delete btn;
return 0;
}
这份代码看似完美,实则会发生崩溃:
这是因为对象树的存在。
在上面的代码当中,出现了指定"父亲"的情况,这种操作实际上就是加入对象树。
那么对象树和内存泄漏有什么关系?
在QT当中的对象树有一个特性,对象树当中的任意一个对象要析构的时候,清理自身资源之前要先清理所有的"儿子"。
这就是为什么大多数QT类当中需要传入一个parent指针的原因,就是要让该类对象加入对象树,然后根据对象树当中的任意对象析构时的特性,可以做到避免内存泄漏。
举一个例子来说明以上结论是正确的,这里自定一个MyPushButton类:
#include "widget.h"
#include
#include
#include
#include
class MyPushButton : QPushButton// 继承自QPushButton类
{
public:
MyPushButton(const QString &text, QWidget *parent = nullptr)
:QPushButton(text,parent)
{}
~MyPushButton()
{
qDebug() << "~MyPushButton()";
}
private:
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
MyPushButton *btn = new MyPushButton("自定义按钮",&w);
w.show();
return a.exec();
}
Widget::~Widget()
{
delete ui;
qDebug() << "Widget::~Widget()";
}
其运行结果如下:
那么在这个例子当中就有了一颗简单的对象树:
"信号槽"实际上是两个东西,一个是信号,一个是槽。那么我自己更愿意称它们为"信号和信号处理"。
既然谈到信号和信号处理,那么就必定涉及四个部分:
其中"收到信号后要做什么动作"在QT当中称为槽。
流程如下图所示:
connect函数是QObject当中的成员函数,而QObject是最顶层的基类,意思是说例如QWidget或者QMainWindow这样的类它们的祖宗类都是QObject,所以它们都可以直接使用类内的connect函数。
下面以代码演示一下"按钮点击之后窗口关闭"的效果:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton("我的按钮",&w);
Widget::connect(btn,&QPushButton::clicked,&w,&Widget::close);// 注册信号和槽
w.show();
return a.exec();
}
connect方法实际上类似于Linux的系统调用signal,它们都是在注册一堆信号和信号处理方法,信号产生并且接收到时就会执行预设的动作。那么在QT当中有一个取消注册信号槽的方法:disconnect。
在QT当中允许自定义信号和槽。
有一需求:设计出员工类和老板类,员工负责发送"月底到了"的信号,老板负责响应员工发送的信号,老板的动作就是发工资。
下面给出实现的代码:
// boss.hpp
#ifndef BOSS_H
#define BOSS_H
#include
#include
class Boss : public QObject
{
Q_OBJECT
public:
explicit Boss(QObject *parent = nullptr)
:QObject(parent)
{}
public slots:
void GetPaid()//
{
qDebug() << "老板发工资了!";
}
};
#endif // BOSS_H
// staff.hpp
#ifndef STAFF_H
#define STAFF_H
#include
class Staff : public QObject// 员工类
{
Q_OBJECT
public:
explicit Staff(QObject *parent = nullptr)
:QObject(parent)
{}
signals:
void IsEndOfMonth();// 到月底了
};
#endif // STAFF_H
// main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Staff *st = new Staff;
Boss *bs = new Boss;
QObject::connect(st,&Staff::IsEndOfMonth,bs,&Boss::GetPaid);// 注册一个信号和槽
emit st->IsEndOfMonth();// 员工发送"月底到了"的信号
return a.exec();
}
程序运行后的结果为:
上面代码设计到4个新鲜的东西,这里一一介绍一下:
connect方法不仅可以指定信号连接槽,还可以指定信号连接信号。
接下来演示一个实例代码:点击按钮触发一个信号,该信号连接到Staff类的"月底到了"的信号,然后Staff类的信号连接到Boss类的"发工资"信号:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
Staff *st = new Staff(&w);
Boss *bs = new Boss(&w);
QPushButton *btn = new QPushButton("我的按钮",&w);
QObject::connect(btn,&QPushButton::clicked,st,&Staff::IsEndOfMonth);// 信号连接信号
QObject::connect(st,&Staff::IsEndOfMonth,bs,&Boss::GetPaid);
w.show();
return a.exec();
}
上面的案例当中信号函数和槽函数都是没有参数的,事实上在QT当中,信号函数和槽函数允许重载。需要注意的是,槽函数的参数必须与信号函数一一对应,但是可以少于信号函数的参数。例如下面这样:
// 信号函数
void Signal1();
void Signal2(int x);
void Signal3(int x,int y);
// 槽函数
void Slot1();
void Slot2(int x);
在如上代码代码当红,Signal1触发会调用Slot1槽函数,Signal2、Signal3触发会调用Slot2槽函数。
还需要注意,在上面的所有与信号和槽相关的代码时都是"不准确"的,因为在实际的项目当中一个类的信号或槽都有多个重载,那么在使用connect()函数时,如何指定信号和槽?答案是使用函数指针。例如下面的用法:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
Staff *st = new Staff(&w);
Boss *bs = new Boss(&w);
QPushButton *btn = new QPushButton("我的按钮",&w);
/*指向无参的信号和槽*/
// void (Staff:: *staffSignal)() = &Staff::IsEndOfMonth;
// void (Boss:: *bossSlot)() = &Boss::GetPaid;
/*指向int类型参数的信号和槽*/
void (Staff:: *staffSignal)(int) = &Staff::IsEndOfMonth;
void (Boss:: *bossSlot)(int) = &Boss::GetPaid;
QObject::connect(btn,&QPushButton::clicked,st,staffSignal);// 信号连接信号
QObject::connect(st,staffSignal,bs,bossSlot);
w.show();
return a.exec();
}
那么输出结果或许猜想到了,是不正确的,因为我们并没有做传参的处理:
为了解决上面的问题,可以使用lambda表达式来解决它。
注意,槽函数的本质只是一个函数,而lambda表达式本质是一个匿名函数对象,所以可以直接搭配使用。
最终代码如下:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
Staff *st = new Staff(&w);
Boss *bs = new Boss(&w);
QPushButton *btn = new QPushButton("我的按钮",&w);
/*指向int类型参数的信号和槽*/
void (Staff:: *staffSignal)(int) = &Staff::IsEndOfMonth;
void (Boss:: *bossSlot)(int) = &Boss::GetPaid;
QObject::connect(st,staffSignal,bs,bossSlot);
auto func = [=]()
{
emit st->IsEndOfMonth(100);
};
QObject::connect(btn,&QPushButton::clicked,func);// 使用lambda表达式时可以不指定接收信号对象
w.show();
return a.exec();
}