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

Izz*_*zzo 16 c++ qt

我目前正在研究一个Qt项目,并且对信号和插槽机制有点困惑.但我觉得我对QObject和用户界面表单之间的差异有一点了解.

用户界面表单(由.ui文件描述)被馈送到用户界面编译器(uic)并产生相关的头文件.此头文件不仅包含接口信息,而是包含应格式化QObject的实现细节.另一方面,QObject是基础类,其中构建了许多Qt框架.信号和插槽系统完全基于QObject.

在扩展QObject类(或从派生类)时,实际上是在定义一个可以产生信号和槽的对象.要将此对象格式化为您在Qt Designer中设计的用户界面,请创建ui类的实例(通过uic生成的ui标头).您在此类上调用setup方法并将其提供给您正在创建的新QObject.

这一切似乎都有道理.

然而,没有意义的是当你开始实际定义信号时.从Qt Designer开始,我可以选择右键单击并选择"信号".当我选择一个信号时(无论是鼠标点击按钮还是进度条中的更改),它最终会将其添加到我的QObjects信号部分.我的问题是:为什么以及如何?在Qt Creator中,我在Qt Designer(一个为uic生成XML的程序)中的操作到底是如何与我的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文件中定义的元素?

Jas*_*n C 20

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

简短的回答是:魔术.

真正的答案是,它实际上与您在Designer中的操作无关,至少不是直接的.它实际上甚至不是特定于UI的东西,只是设计师充分利用它.这是怎么回事:

首先,每当你编译一个使用Q_OBJECT宏的文件时,这会触发Qt的moc工具在编译期间运行("触发器"实际上并不是最准确的描述:更确切地说,当qmake运行生成makefile时,它会添加建立规则运行mocQ_OBJECT在标题检测,但这是题外话).详细信息有点超出了这个答案的范围,但长话短说它最终会生成moc_*.cpp你在编译后会看到的那些文件,其中包含一大堆已编译的元信息.其中重要的部分是它生成有关您声明的各种信号和插槽的运行时可访问信息.你会在下面看到这一点的重要性.

另一件重要的事情是所有人QObject都有一个名字.此名称可在运行时访问.您在设计器中为小部件指定的名称将设置为小部件的对象名称.这一点很重要的原因也将在下面说明.

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

好的,所以......

你会注意到源代码Qt为你的windows生成你总是ui->setupUi(this)在构造函数中有这个调用.该函数的实现是由uic编译期间Qt的工具生成的,例如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);
}
Run Code Online (Sandbox Code Playgroud)

该功能connectSlotsByName是所有这些小部件信号发生魔力的地方.

基本上该功能执行以下操作:

  1. 迭代所有声明的插槽名称,它可以执行,因为moc在元对象信息中,这些都被打包到运行时可访问的字符串中.
  2. 当它找到名称与模式匹配的插槽时on_<objectname>_<signalname>,它会递归搜索对象层次结构,查找名称为<objectname>的对象,例如您指定的小部件之一,或者您可能创建的任何其他命名对象.然后它将对象信号<signalname>连接到它找到的插槽(它基本上自动调用QObject::connect(...)).

就是这样.

所以这意味着,当你去那里的插槽时,设计师没有什么特别的事情发生.所有这一切都发生在设计器中,将带有相应参数的名为on_widgetName_signalName的插槽插入到标头中,并为其生成一个空函数体.(thuga对此进行了很好的解释).这足以QMetaObject::connectSlotsByName在运行时找到它并设置连接.

这里的含义非常方便:

  • 您可以删除小部件的信号处理程序而无需在设计器中执行任何特殊操作.您只需从源文件中删除插槽及其定义,它们都是完全自包含的.
  • 您可以手动添加小部件信号处理程序,而无需通过设计器界面,这有时可以节省您一些乏味.您所要做的就是在窗口中创建一个名为eg的插槽on_lineEdit1_returnPressed(),它可以工作.你只需要小心,因为hyde在评论中提醒我们:如果你在这里犯了一个错误,你将不会得到编译时错误,你只会得到一个运行时警告打印到stderr并且连接不会被创造; 因此,如果您在此处进行任何更改,请务必注意控制台输出.

这里的另一个含义是该功能实际上不限于UI组件.moc适合所有人QObject,并connectSlotsByName始终可用.因此,您创建的任何对象层次结构,如果您命名对象,然后声明具有适当名称的插槽,您可以调用connectSlotsByName以自动设置连接; 它恰好发生uic在最后召唤的棍棒setupUi,因为这是一个方便的地方.但魔术不是特定于UI的,并且不依赖于uic元信息.这是一种普遍可用的机制.它非常整洁.

暂且不说:我QMetaObject::connectSlotsByNameGitHub上写了一个简单的例子.


thu*_*uga 6

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

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

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

您可以通过查看源代码的这一部分来更详细地检查它的工作原理.

但最重要的是,你在单独的.h.cpp文件中声明和定义你的类方法,并且ui_xxx.h包含了这个Ui::xxx对象,并且已经在那些文件中声明了该对象.当然,这是Qt Creator在您添加新表单类时自动为您执行的操作,因此您不必担心它.