Qt信号/槽实际上是如何耦合到.ui文件中的元素的?

How are Qt signals/slots actually coupled to the elements in a .ui file?

本文关键字:ui 文件 元素 耦合 信号 实际上 何耦合 Qt      更新时间:2023-10-16

我目前正在一个Qt项目上工作,我对信号和插槽机制有些困惑。然而,我觉得我对QObject和用户界面表单之间的差异有了一些很好的理解。

用户界面表单(由.ui文件描述)被输入用户界面编译器(uic)并生成一个相关的头文件。这个头文件不仅包含接口信息,还包含应该格式化的QObject的实现细节。另一方面,QObject是许多Qt框架建立在其上的基类。信号和槽位系统完全基于QObject。

当扩展QObject类(或从派生类)时,您实际上定义了一个可以在其中产生信号和槽的对象。要格式化这个对象,使其看起来像您刚刚在Qt Designer中设计的用户界面,您需要创建ui类的实例(通过ui生成的ui头文件)。调用该类上的setup方法并向它提供正在创建的新QObject。

这一切似乎都有意义。

然而,当你开始真正定义信号时,就没有意义了。从Qt设计器,我有右键单击并选择一个"信号"的选项。当我选择一个信号(无论是鼠标单击按钮还是进度条中的更改)时,它最终会将其添加到我的QObjects信号部分。我的问题是:为什么?如何?我在Qt Designer(一个生成XML的程序)中的行为是如何与Qt Creator中的QObject相耦合的?更具体地说,它如何知道将这个槽添加到哪个QObject中?

这个问题可能有些不清楚,所以我在这里举一个例子。

比如说,我想创建一个新的交互式显示。我们就叫它"MyScreen"吧。为了创建这个界面,我可能会有3个文件:'myscreen.h', 'myscreen.cpp'和'myscreen.ui'。'myscreen.h'负责声明QObjects的属性和方法,以及信号和插槽。'myscreen.cpp'负责定义方法、信号和插槽。"myscreen。ui'负责创建将用于格式化MyScreen实例的用户界面布局。

但是由于我之前所说的ui只是用于生成标题,为什么它要耦合到'myscreen.cpp'?也就是说,我可以右键单击.ui布局,创建一个新的信号类型,并且该信号类型将被添加到myscreen.h和myscreen.cpp中。这是怎么发生的?它是如何耦合的?Qt是否总是存在3个文件(xxx.h, xxx.cpp, xxx.ui) ?

希望这能给你们一些我所困惑的背景。似乎没有一个写得很好的文档(至少我找到了),可以彻底描述所有这些项目之间的潜在关系。

TLDR -来自。h和。cpp的信号/槽实际上如何链接到。ui文件中定义的元素?

我的问题是:为什么?我在Qt Designer(一个生成XML的程序)中的行为是如何与Qt Creator中的QObject相耦合的?更具体地说,它如何知道将这个槽添加到哪个QObject中?

简短的回答是:魔法。

真正的答案是,它实际上与你在Designer中的操作没有任何关系,至少没有直接关系。它甚至不是特定于ui的东西,只是设计者很好地利用了它。结果如下:

首先,任何时候你编译一个使用Q_OBJECT宏的文件,这会触发Qt的moc工具在编译过程中运行("触发器"并不是最准确的描述:更准确地说,当qmake运行生成makefiles时,当Q_OBJECT在头文件中被检测到时,它会添加构建规则来运行moc,但这不是重点)。细节有点超出了这个答案的范围,但长话短说,它最终会生成那些moc_*.cpp文件,你会在编译后看到,其中包含一大堆编译后的元信息。其中重要的部分是它生成关于您已声明的各种信号和插槽的运行时可访问信息的位置。你会在下面看到这有多重要。

另一个重要的事情是所有的QObject都有一个名字。此名称可在运行时访问。在设计器中给小部件的名称设置为小部件的对象名称。这一点很重要的原因也将在下面变得清楚。

最后一个重要的事情是QObject具有层次结构;当您创建QObject时,您可以设置它的父节点。表单上的所有小部件最终都有表单本身(例如您的QMainWindow派生类)作为它们的父类。

好的,所以…

你会注意到在Qt为你的windows生成的源代码中,你总是在构造函数中有ui->setupUi(this)调用。该函数的实现是由Qt的uic工具在编译期间生成的,例如ui_mainwindow.h。该函数是存储在.ui文件中的设计器中设置的所有属性的实际设置位置,包括小部件的对象名称。现在,生成的setupUI()实现的最后一行是关键:

void setupUi(QMainWindow *MainWindow) {
    ...
    // Object properties, including names, are set here. This is 
    // generated from the .ui file. For example:
    pushButton = new QPushButton(centralWidget);
    pushButton->setObjectName(QString::fromUtf8("pushButton"));
    pushButton->setGeometry(QRect(110, 70, 75, 23));
    ...
    // This is the important part for the signal/slot binding:
    QMetaObject::connectSlotsByName(MainWindow);
}

这个函数,connectSlotsByName,是所有这些小部件信号的神奇之处。

基本上这个函数做了以下事情:

  1. 遍历所有声明的槽名,它可以这样做,因为这都是由moc在元对象信息中打包成运行时可访问的字符串。
  2. 当它找到一个槽的名字匹配模式on_<objectname>_<signalname>,它递归地搜索对象层次结构的对象的名字是,例如一个您的命名小部件,或任何其他命名对象您可能已经创建。然后它连接对象信号,到它找到的插槽(它基本上自动调用QObject::connect(...))。

就是这样。

这意味着,在designer中没有什么特别的事情发生。所发生的一切是设计器插入一个槽名为on_widgetName_signalName,与适当的参数,到你的头,并为它生成一个空的函数体。 (thuga对此做了很好的解释)。这足以让QMetaObject::connectSlotsByName在运行时找到它并建立连接。

这里的含义非常方便:

    你可以删除一个小部件的信号处理程序,而不需要在设计器中做任何特殊的操作。您只需从源文件中删除槽及其定义,它就完全独立了。
  • 您可以手动添加小部件信号处理程序,而不必通过设计器界面,这有时可以节省您的一些繁琐。你所要做的就是在你的窗口中创建一个插槽,例如on_lineEdit1_returnPressed(),瞧,它工作了。你只需要小心,正如hyde在注释中提醒我们的那样:如果你在这里犯了错误,你不会得到编译时错误,你只会得到一个运行时警告,打印为stderr,并且不会创建连接;所以如果你在这里做了任何更改,注意控制台输出是很重要的。

这里的另一个含义是,该功能实际上并不局限于UI组件。moc在所有QObject上运行,connectSlotsByName总是可用的。所以你创建的任何对象层次结构,如果你给对象命名,然后用适当的名字声明槽,你可以调用connectSlotsByName来自动建立连接;碰巧uicsetupUi的末尾,因为这是一个方便的地方。但是魔法不是特定于ui的,也不依赖于uic,只依赖于元信息。这是一种普遍可行的机制。它很整洁。

作为题外话:我在GitHub上粘贴了一个QMetaObject::connectSlotsByName的快速示例。

当您从Go to slot...上下文菜单项中添加插槽时,会发生什么?它会遍历您的项目,并查找包含ui_myclass.h的任何文件。当它找到这个文件时,它会确保在该文件或直接包含的文件中声明Ui::MyClass对象。这些将是您的myclass.cppmyclass.h文件。如果它找到了这些文件,那么它将在这些文件中添加slot声明和定义。

您可以通过从myclass.h文件中删除Ui::MyClass对象(通常命名为ui )声明来测试这一点,然后通过在设计器中使用Go to slot...函数添加插槽。您应该得到一些错误消息,说明无法找到Ui::MyClass对象声明。您还可以尝试从myclass.cpp文件中删除ui_myclass.h包含。如果你现在尝试从设计器添加插槽,你会得到一个错误消息,指出它无法找到ui_myclass.h包含在任何地方,或类似的东西。

如果您在myclass.h文件中定义了所有内容并删除了myclass.cpp,它应该会给您一个错误消息,说明它无法添加插槽定义。

您可以通过查看这部分源代码来详细检查它是如何工作的。

但最终真正重要的是,你在单独的.h.cpp文件中声明和定义了你的类方法,ui_xxx.h被包括在内,Ui::xxx对象已经在这些文件中声明了。当然,这是Qt Creator在您添加新表单类时自动为您做的,因此您不必担心它。