QT的学习
第一个QT项目“hello,world!”
#include
/*
引入QApplication 类,main函数一般以创建application对象开始
GUI 程序是QApplication,非 GUI 程序是QCoreApplication
这个对象用于管理 Qt 程序的生命周期,开启事件循环
/
#include
/
引入QLabel 类,直接创建一个QLabel对象
构造函数赋值“Hello, world”
能够在QLabel上面显示这行文本。
最后调用QLabel的show()函数将其显示出来
*/
int main(int argc ,char *argv[])
{
QApplication app(arc,argv);
QLabel label("hello , world!");
label.show();
return app.exec();
}
注意:推荐在栈上创建组件。
因为要靠人工管理new和delete的出错概率要远大于在栈上的自动控制。
除此之外,在堆上和在栈上创建已经没有任何区别。
信号槽(观察者模式)
当某个事件发生之后,比如,按钮检测到自己被点击了一下,
它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。
如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,
意思是,用自己的一个函数(成为槽(slot))来处理这个信号。
也就是说,当信号发出时,被连接的槽函数会自动被回调。
这就类似观察者模式:
当发生了感兴趣的事件,某一个操作就会被自动触发。
(这里提一句,Qt 的信号槽使用了额外的处理来实现,
并不是 GoF 经典的观察者模式的实现方式。)
代码举例:
#include
#include
int main(int argc,char *argv[])
{
QApplication app(argc,argv);
QPushButton button("Quit");
QObject::connect(&button,&QPushButton::clicked,&QApplication::quit);
button.show();
return app.exec();
}
QObject::connect 有五个重载:
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
//将signal和slot作为字符串处理
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
//将每个函数看作MetaMethod的子类
QMetaObject::Connection connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const;
//缺少了receiver,其实使用的是this指针作为receiver
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
//PointerToMemberFunction指向成员函数的指针
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
//最后一个参数类型:可以接受static函数、全局函数以及Lambda表达式
最常用的一般形式:
// !!! Qt 5
connect(sender, signal,
receiver, slot);
connect()一般会使用前面四个参数,第一个是发出信号的对象,
第二个是发送对象发出的信号,第三个是接收信号的对象,
第四个是接收对象在接收到信号之后所需要调用的函数。
也就是说,当 sender 发出了 signal 信号之后,
会自动调用 receiver 的 slot 函数。
connect()函数,sender 和 receiver 没有什么区别,都是QObject指针;
主要是 signal 和 slot 形式的区别。
具体到我们的示例,我们的connect()函数显然是使用的第五个重载,
最后一个参数是QApplication的 static 函数quit()。
也就是说,当我们的 button 发出了clicked()信号时,
会调用QApplication的quit()函数,使程序退出。
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。
如果不一致,允许的情况是,槽函数的参数可以比信号的少,
即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。
这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),
但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
将一个对象的信号连接到Lambda表达式,代码举例:
//!!!QT 5
#include
#include
#include
int main(int argc,char *argv [])
{
QApplication app(argc,argv);
QPushButton button("Quit");
QObject::connect(&button,&QPushButton::clicked,[](bool){
qDebug()<<"You clicked me!";
});
button.show();
return app.exec();
}
注意:这里的 Lambda 表达式接收一个 bool 参数,
这是因为QPushButton的clicked()信号实际上是有一个参数的。
Lambda 表达式中的qDebug()类似于cout,将后面的字符串打印到标准输出。
如果要编译上面的代码,你需要在 pro 文件中添加这么一句:
QMAKE_CXXFLAGS += -std=c++0x
Qt 4 的信号槽同 Qt 5 类似。在 Qt 4 的 QObject 中,有三个不同的connect()重载:
bool connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
bool connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
bool connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const
除了返回值,Qt 4 的connect()函数与 Qt 5 最大的区别在于,
Qt 4 的 signal 和 slot 只有const char *这么一种形式。
QT4的代码
//!!!QT4
#include
#include
int main(int argc ,char *argv)
{
QApplication app(argc,argv);
QPushButton button("Quit");
QObject::connect(&button,SIGNAL(clicked()),&app,SLOT(quit()));
button.show;
return app.exec();
}
使用了SIGNAL和SLOT两个宏,将两个函数名转换成了字符串。
注意,即使quit()是QAPPlication的static函数,也必须传入一个对象指针。
这是QT4信号槽用法的局限之处。
注意到connect()函数的 signal 和 slot 都是接受字符串,
因此,不能将全局函数或者 Lambda 表达式传入connect()。
一旦出现连接不成功的情况,Qt 4 是没有编译错误的
(因为一切都是字符串,编译期是不检查字符串是否匹配),
而是在运行时给出错误。这无疑会增加程序的不稳定性。
自定义信号槽!!!
经典的观察者模式举例:报纸和订阅者
有一个报纸类Newspaper,有一个订阅者类Subscriber
Subscriber可以订阅Newspaper。这样,当Newspaper有了新内容时,
Subscriber可以立即得到通知。在这个例子中,观察者是Subscriber,
被观察者是Newspaper,在经典实现代码中,观察者会将自身注册到被
观察者的一个容器中(比如:subscriber.registerTo(newspaper))
被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者
(newspaper.notifyAllSubscribers()).
实现代码:
//!!!QT 5
#include
//newspaper.h
class Newspaper: public QObject
{
Q_OBJECT
public:
Newspaper(const QString & name):
m_name(name)
{
}
void send()
{
emit newsPaper(m_name);
}
signals:
void newspaper(const QString &name);
private:
QString m_name;
};
///reader.h
#include
#include
class Reader : public QOject
{
Q_OBJECT
public:
Reader(){}
void receiveNewspaper(const QString & name)
{
qDebug()<<"Receives Newspaper: "<<name;
}
};
//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);
}
当我们运行上面的程序时,会看到终端输出 Receives Newspaper: Newspaper A 这样的字样。
分析:
首先Newspaper这个类。继承了QObject类。!!!!只有继承了QOject类的类,才
才具有信号槽的能力。!!!凡是QObject类(不管是直接子类还是间接子类),
都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。
这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。
注意,这个宏将由 moc(我们会在后面章节中介绍 moc。这里你可以将其理解为一种预处理器,
是比 C++ 预处理器更早执行的预处理器。) 做特殊处理。不仅仅是宏展开这么简单。
moc 会读取标记了 Q_OBJECT 的头文件,生成以 moc_ 为前缀的文件,
比如 newspaper.h 将生成 moc_newspaper.cpp。你可以到构建目录查看这个文件,
看看到底增加了什么内容。注意,由于 moc 只处理头文件中的标记了Q_OBJECT的类声明,
不会处理 cpp 文件中的类似声明。因此,如果我们的Newspaper和Reader类位于 main.cpp 中,
是无法得到 moc 的处理的。解决方法是,我们手动调用 moc 工具处理 main.cpp,
并且将 main.cpp 中的#include "newspaper.h"改为#include "moc_newspaper.h"就可以了。
不过,这是相当繁琐的步骤,为了避免这样修改,我们还是将其放在头文件中。许多初学者会遇到莫名其妙的错误,
一加上Q_OBJECT就出错,很大一部分是因为没有注意到这个宏应该放在头文件中。
Newspaper类的 public 和 private 代码块都比较简单,只不过它新加了一个 signals。
signals 块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是 void
(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。
信号作为函数名,不需要在 cpp 函数中添加任何实现(我们曾经说过,Qt 程序能够使用普通的 make 进行编译。
没有实现的函数名怎么会通过编译?原因还是在 moc,moc 会帮我们实现信号函数所需要的函数体,
所以说,moc 并不是单纯的将 Q_OBJECT 展开,而是做了很多额外的操作)。
Newspaper类的send()函数比较简单,只有一个语句emit newspaper(m_name);emit是QT对C++的拓展,是一个关键字。
(其实也是一个宏)。emit的含义是发出newspaper()信号,感兴趣的接收者会关注这个信号,还需要知道是
哪份报纸发出的信号,所以,将实际的报纸名字m_name当做参数传给这个信号。当接收者连接这个信号时,可以
通过槽函数获取实际值。这样就完成了数据从发出者到接收者的一个转移。
Read类更简单,因为这个类需要接收信号,所以继承了QOject,并且添加了Q_OBJECT宏。后面是默认构造函数和
一个普通的成员函数。QT5 中,任何成员函数,static函数、全局函数和Lambda表达式都可以作为槽函数。与信号
函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,会受到public、private
等访问控制符的影响。
main()函数中,首先创建Newspaper和Reader两个对象,然后使用QObject::connect函数(连接),调用Newspaper 的
send()函数。这个函数只有一个语句:发出信号,由于我们的连接,当这个信号发出时,自动调用reader的槽函数,打印语句。
注意事项!!!!
1、发送者和接收者都需要是QObject的子类(槽函数是全局函数、Lambda表达式等无需接收者的时候除外)
2、使用signal标记信号函数,信号是一个函数声明,返回void,不需要实现函数代码;
3、槽函数是普通的成员函数,作为成员函数,会受到public、private、protected的影响;
4、使用emit在恰当的位置发送信号
5、使用QOject::connect()函数连接信号和槽
QT4!!!
//!!! Qt4
#include
// newspaper.h
class Newspaper : public QObject
{
Q_OBJECT
public:
Newspaper(const QString & name) :
m_name(name)
{
}
void send() const
{
emit newPaper(m_name);
}
signals:
void newPaper(const QString &name) const;
private:
QString m_name;
};
// reader.h
#include
#include
class Reader : public QObject
{
Q_OBJECT
public:
Reader() {}
public slots:
void receiveNewspaper(const QString & name) const
{
qDebug() << "Receives Newspaper: " << name;
}
};
// 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, SIGNAL(newPaper(QString)),
&reader, SLOT(receiveNewspaper(QString)));
newspaper.send();
return app.exec();
}
//分析:
Newspaper类没有区别;
Reader类,receiveNewspaper()函数放在了public slots块中。在QT4中,槽函数必须放在由修饰的代码块中,
并且要使用访问控制符进行访问控制。其原则同其他函数一样;默认是private的,如果要在外部访问,就应该是public slots;
如果只需要在子类访问,就应该是protected slots。
main()函数中,QOject::connect()函数,第二、第四个参数需要使用SIGNAL和SLOT 这两个宏转换成字符串。
注意:SIGNAL和SLOT的宏参数并不是取函数指针,而是除去返回值的函数声明,并且const这种参数修饰符是忽略不计的。
下面说明另外一点,我们提到了“槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响”,
public、private 这些修饰符是供编译器在编译期检查的,因此其影响在于编译期。对于 Qt4 的信号槽连接语法,
其连接是在运行时完成的,因此即便是 private 的槽函数也是可以作为槽进行连接的。但是,如果你使用了 Qt5 的新语法,
新语法提供了编译期检查(取函数指针),因此取 private 函数的指针是不能通过编译的。