一、概述
QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象、易于扩展、真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT 库的基础之上。QT 支持下列平台:MS/WINDOWS-95、98、NT 和 2000;UNIX/X11-Linux、Sun Solaris、HP-UX、Digital Unix、IBM AIX、SGI IRIX;EMBEDDED- 支持 framebuffer 的 Linux 平台。伴随着 KDE 的快速发展和普及,QT 很可能成为 Linux 窗口平台上进行软件开发时的 GUI 首选。
信号和槽机制(Signals and Slots)是 QT 的核心机制,要精通 QT 编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。信号和槽是 QT 自行定义的一种通信机制,它独立于标准的 C/C++ 语言,因此要正确的处理信号和槽,必须借助一个称为 moc(Meta Object Compiler)的 QT 工具,该工具是一个 C++ 预处理程序,它为高层次的事件处理自动生成所需要的附加代码。
二、引出
Older toolkits使用回调(callbacks)来达到这样的目的。回调是一个指向函数的指针,所以如果你希望一个处理函数通知你某些事件发生了,你可以传递一个指向其他函数的指针(回调)给它。这个处理函数将在适当的时候调用回调函数。回调函数有两个明显的缺点,第一,它们并不是类型安全,我们永远都不能确定调用者是否将通过正确的参数来调用“回调函数”;第二,回调函数与处理函数是紧耦合(strongly coupled)的,因为调用者必须知道应该在什么时候调用哪个回调函数。
Qt使用了信号和槽来代替回调函数。当一个特定的事件发生时,信号会被发送出去。Qt的窗体部件(widget)拥有众多预先定义好的信号,当然,我们也可以创建窗体部件(widget)的子类来为它们添加我们需要的自定义信号。槽,则是对一个特定的信号进行的反馈。Qt的窗体部件(widget)同样拥有众多预先定义好的槽,但是通常的做法是,创建窗体部件(widget)的子类并添加自定义槽,以便对感兴趣的信号进行处理。
三、特点
1)QT信号槽机制的引用精简了程序员的代码量
2)QT的信号可以对应多个槽(但他们的调用顺序随机),也可以多个槽映射一个信号 ,信号还可以映射信号
3)QT的信号槽的建立和解除绑定十分自由
4)信号槽同真正的回调函数比起来时间的耗损还是很大的,所有在嵌入式实时系统中应当慎用
5)信号槽的参数限定很多例如不能携带模板类参数,不能出现宏定义等等
信号和槽机制是类型安全的(type-safe):一个信号的参数必须和接收槽的参数匹配。(槽的参数可以比它接收的信号的参数短)由于这种参数匹配机制,编译器以帮助我们检查类型不匹配的签名。
信号与槽是松耦合(loosely coupled)的:一个发出信号的类既不知道也不关心哪一个槽接收到这个信号。Qt的信号和槽机制保证了如果你将一个信号连接到一个槽上,槽会在正确的时间以号的参数被调用。信号与槽可以携带任意个、任意类型的参数。他们是完全的类型安全。
所有从QObject或者它的一个子类(比如:QWidget)继承的类都可以使用号与槽。对象中以这种方式通信:一个对象的状态发生了改变并发送信号,关心这个改变的另一对像接收到这个信号。发送信号的对象并不知道也不感兴趣什么对象接收它所发出的信号,这是真正的信息封装,保证了对象能被当作软件组件来使用。
槽能被用来接收信号,除此之外它们也是普通的成员函数。槽不知道是否有信号与它连接起来,正如对象不知道它发出信号是否会被接收一样。这样的机制确保了可以使用Qt创建一个个完全独立的组件。
你可以把你感兴趣的多个信号与一个个槽进行连接,也可以把一个信号与多个槽进行连接。甚至可以直接把一个信号连接到另一个信号(当第一个信号发送出去的时候,第二个信号紧接着被发送)。
就这样,信号与插槽建立了强大的组件编程机制。
四、实例
通过调用 QObject 对象的 connect 函数来将某个对象的信号与另外一个对象的槽函数相关联,这样当发射者发射信号时,接收者的槽函数将被调用。该函数的定义如下:
static bool connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType =
#ifdef qdoc
Qt::AutoConnection
#else
#ifdef QT3_SUPPORT
Qt::AutoCompatConnection
#else
Qt::AutoConnection
#endif
#endif
);
QLabel *label = new QLabel;
QScrollBar *scroll = new QScrollBar;
QObject::connect( scroll, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)) );
class MyWidget : public QWidget
{
Q_OBJECT //最好都加上
public:
MyWidget();
...
signals:
void aSignal();
...
private:
...
QPushButton *aButton;
};
MyWidget::MyWidget()
{
aButton = new QPushButton( this );
connect( aButton, SIGNAL(clicked()), SIGNAL(aSignal()) );
}
所有包含信号或槽的类在他们声明的顶端都必须写上Q_OBJECT,即使不使用信号槽机制,最好也在类中加上该宏定义进行申明,其次必须直接或间接继承自Qobject。
槽的实现:
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
};
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
emit行从对象中以新的value值作为参数,发送信号valueChanged()。
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48
bool QObject::disconnect ( const QObject * sender, const char * signal,
const Object * receiver, const char * member ) [static]
- 断开与某个对象相关联的任何对象。这似乎有点不可理解,事实上,当我们在某个对象中定义了一个或者多个信号,这些信号与另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就可以利用这个方法,非常之简洁。
disconnect( myObject, 0, 0, 0 )
//或者
myObject->disconnect()
- 断开与某个特定信号的任何关联。
disconnect( myObject, SIGNAL(mySignal()), 0, 0 )
//或者
myObject->disconnect( SIGNAL(mySignal()) )
- 断开两个对象之间的关联。
disconnect( myObject, 0, myReceiver, 0 )
//或者
myObject->disconnect( myReceiver )
在 disconnect 函数中 0 可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者 sender 不能为 0,其它三个参数的值可以等于 0。
五、编译过程
六、信号建立和发射及槽接收过程
信号在对象的内部状态改变的时候以某种方式发送出来,以通知该对象感兴趣的客户端或拥有者。只有在类中定义了信号才能在该类及其子类中发送信号。
当信号被发送时,一般情况下,与之相连的槽会立即被调用,这个过程与一般的函数调用是一样的。这一切的发生,信号与槽机制完全与任何的GUI事件循环无关。emit语句后面代码在所有的槽代码执行完毕之后将会继续执行。这个情况在使用“队列连接”(Queued Connections)时,会有些不同,这种情况下,emit关键字后面的代码会立即执行,而槽会在之后被执行。
如果一个信号同时与好几个槽连接,那么在信号产生时,这些槽将按与信号连接的先后顺序,逐个调用。
信号由moc程序自动产生,并且不能在“.cpp”文件中实现。信号不能有返回值(只能使用void)。
关于信号函数的注释:经验显示,如果信号与槽的参数不使用一些特殊的类型,那么它们的重用性将更好。如果QScrollBar::valueChanged()信号使用了一个特殊的参数,如:QScrollBar::Range,那么它仅可能被连接到为QScrollBar设计的特殊槽上。这是软件复用思想中最深恶痛绝的。这么做的话,该信号就不能与其他不同的窗口输入部件(Input Widget)中的槽连接了。
槽与信号连接之后,当信号发送时就会被调用。槽其实就是普通的C++函数,并且可以像普通函数一样调用.它们唯一的特点就是可以与信号相连。
因为槽是普通的成员函数,所以当它们被直接调用时,它们也遵循C++函数调用规则。然而,它们也可以通过信号-槽之间的连接,由其它组件调用而不管槽的访问级别(如:private级别的槽)。换句话说,从任意的类实例中发送出来的信号都能调用一个不相关类中的private级别的槽。
槽也可以用virutal定义,实际上,后面我们会发现这样子做很有用。
与回调函数(callback)相比,信号与槽会稍微慢一些(译者注:大概慢一个数量级左右,但以现在的硬件来说,我们根本感觉不出来。),这是因为信号-槽机制更灵活。当然,实际应用程序上,它们的不同点是忽略不计的。一般来讲,引发一个连接了多个槽的信号,会比直接调用信号的接收者,慢10倍左右(这里不与虚函数调用比较)。这是因为,信号-槽机制需要定位连接对象,以确保安全地遍历所有的连接(例如,检查在信号发送过程中,所有要接收信号的对象未被销毁),以及参数的正反序列化,这些过程都必须占用花费。举个例子说明下,同时调用十个非虚函数听起来好像很多,但它的花费将比任意的new或delete操作少得多。试想下,你执行创建或销毁字符串,向量或列表的操作,它们将引发new或delete操作,与这些操作相比,信号-槽所需要的花费仅仅占整个函数调用花费的一小部分。
在槽中进行系统调用的花费与上面讲述的类似。在一台i586-500机器上,你每秒可以进行2,000,000次的1对1信号-槽调用,或者1,200,000次的1对2信号-槽调用。信号-槽机制的简单性与灵活性,与它们所占花费比起来,那真是物超所值。当然客户在使用你的程序时,完全不会察觉到这种效率上的微小变化。
注意,有些第三方库可能定义了一些变量,像signals或slots,这些库在Qt程序中使用时,这可能会引发编译器的警告或错误。要解决这个问题,使用#undef预处理器关闭这些预处理符号。
七、信号函数和槽函数的参数
信号与槽的签名包含参数,且参数可能含有不同的默认值。让我们看下QObject::destroyed():
void destroyed(QObject* = 0);
当QObject被销毁时,它会发送QObject::destroyed()信号。当我们想对被删除的QObject对象进行一些处理时,例如,做些清理工作,就可以捕捉该信号。一种接收该信号的槽的函数签名可能像下面这样:
void objectDestroyed(QObject* obj = 0);
为了将信号与-槽相连接,我们会用到QObject::connect(),SIGNAL()和SLOT()宏。在SIGNAL()与SLOT()宏中是否要包含参数,有以下规则:如果信号的参数有默认值,那么传递给SIGNAL()宏的参数个数不能少于SLOT()宏的参数个数。
下面这些代码都会正常工作:
connect(sender, SIGNAL(destroyed(QObject*)), this,
SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this,
SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
但下面的代码就不能正常工作:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
由于槽需要得到一个QObject作为参数,而信号却不带这个参数,会导致这个信号-槽连接报一个运行时错误,这种错误往往很难找出来。
八、信号-槽的高级用法
有时候,你可能想要获取信号发送者的信息,这样子我们就可以判断是谁发出的信号。Qt提供了QObject::sender()函数,它返回一个指向信号发送者的指针。
有时候,会出现一个槽连接多个信号,而这个槽可能需要对不同的信号执行不同的处理方式,QSignalMapper类就是为这种情况设计的。
假设你有三个不同的按钮,它们分别用于打开不同的文件,如:传真文件,账号文件与报表文件。
为了打开正确的文件,你可以使用QSignalMapper::setMapping()将所有按钮的clicked()信号映射到QSignalMapper对象,然后你就可以将QPushButton::clicked()信号与QSignalMapper::map()槽相连接,如下代码所示:
signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
connect(taxFileButton, SIGNAL(clicked()), signalMapper, SLOT (map()));
connect(accountFileButton, SIGNAL(clicked()), signalMapper, SLOT (map()));
connect(reportFileButton, SIGNAL(clicked()), signalMapper, SLOT (map()));
然后将mapped()信号与readFile()槽相连,这个槽依据不同的按钮点击事件,打开不同的文件。
connect(signalMapper, SIGNAL(mapped(QString)), this, SLOT(readFile(QString)));
注:下面的代码也可以编译运行,但由于签名规范化(signature normalization,查了GOOGLE,似乎找不到这词的中文翻译,个人认为应该是函数签名的转换,译者注),这些代码的运行效率会低些
//由于函数签名运行时的规范化,这样子会慢些
connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(readFile(const QString &)));
参考来源:https://blog.csdn.net/kingle123/article/details/72871731