Qt 元对象系统 & 信号槽机制# 头文件扫描 → moc 代码生成 → 生成内容解析 → 运行时信号槽调度
一、整体架构:为什么需要 moc?# C++ 本身 不支持运行时反射 。Qt 为了实现信号槽、属性系统、动态调用等能力,需要在 C++ 之上添加一层"元信息"。这层信息由 moc(Meta-Object Compiler) 在编译前自动生成。
Tip
截至目前,C++26已经确认支持反射系统了,据说Qt也已经在积极拥抱新版C++了,看来未来有望摆脱moc,迎接纯血C++!
flowchart LR
A["Counter.h (含 Q_OBJECT)"] -->|moc 扫描| B["moc_Counter.cpp (生成的元对象代码)"]
B -->|编译链接| C["可执行文件"]
A -->|正常编译| C
核心链路 :源码 → moc 预处理 → 生成 moc_*.cpp → 与原始代码一起编译链接
二、头文件扫描规则# 2.1 When?# 构建系统 触发条件 qmake .pro 中 HEADERS += xxx.h,qmake 分析文件内容自动生成 moc 规则CMake set(CMAKE_AUTOMOC ON) 后,CMake 自动扫描所有 target 源文件premake-qt files { "xxx.h" } 中列出的头文件,premake-qt 插件检测 Q_OBJECT 宏后生成 moc 自定义构建步骤手动 moc Counter.h -o moc_Counter.cpp
2.2 What?# moc 按行扫描源文件,寻找以下关键标记:
标记 作用 必须条件 Q_OBJECT启用完整的元对象系统(信号、槽、属性、反射) 类必须直接或间接继承 QObject Q_GADGET轻量版,仅启用 Q_ENUM/Q_FLAG/Q_PROPERTY/Q_INVOKABLE,不支持信号槽 不需要继承 QObject Q_NAMESPACE为命名空间启用 Q_ENUM_NS 等 放在 namespace 块中
Important
建议将Q_OBJECT 宏放在类声明的 private 区域 (类体的最顶部)。因为他的展开代码中包含private。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define Q_OBJECT \
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
QT_OBJECT_GADGET_COMMON \
QT_DEFINE_TAG_STRUCT(QPrivateSignal); \
QT_WARNING_POP \
QT_ANNOTATE_CLASS(qt_qobject, "")
根据扫描结果,moc会对包含相关宏的文件进行动态生成,产生moc_<BaseName>.cpp文件,并将其加入编译单元(这个动作由相关的构建工具来完成)。而不包含相关宏的文件则被忽略。
三、moc 生成内容详解# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Counter : public QObject
{
Q_OBJECT
Q_PROPERTY ( int value READ value WRITE setValue NOTIFY valueChanged )
// 附加自定义的键值对元信息,可在运行时通过 QMetaObject 查询
Q_CLASSINFO ( "Author" , "Demo" )
Q_CLASSINFO ( "Version" , "1.0" )
public :
enum CountDirection {
Up = 1 ,
Down = - 1
};
Q_ENUM ( CountDirection ) // 将枚举注册到元对象系统, 使其可以通过字符串名进行查询/转换
explicit Counter ( QObject * parent = nullptr )
: QObject ( parent ), m_value ( 0 ), m_direction ( Up ) {}
int value () const { return m_value ; }
// 标记为 Q_INVOKABLE 后,可以通过 QMetaObject::invokeMethod() 调用
Q_INVOKABLE void reset ()
{
qDebug () << "[Counter::reset] 通过 Q_INVOKABLE 重置计数器" ;
setValue ( 0 );
}
Q_INVOKABLE QString describe () const
{
return QString ( "Counter(value=%1, direction=%2)" )
. arg ( m_value )
. arg ( m_direction == Up ? "Up" : "Down" );
}
void setDirection ( CountDirection dir ) { m_direction = dir ; }
public slots :
void setValue ( int newValue )
{
if ( m_value == newValue )
return ;
int oldValue = m_value ;
m_value = newValue ;
emit valueChanged ( m_value );
}
void increment ()
{
setValue ( m_value + static_cast < int > ( m_direction ));
}
signals :
void valueChanged ( int newValue );
void overflowDetected ( const QString & message );
private :
int m_value ;
CountDirection m_direction ;
};
以上 Counter 类使用了 Q_OBJECT、Q_PROPERTY、Q_CLASSINFO、Q_ENUM、Q_INVOKABLE、signals、slots 等各类元对象宏。moc 扫描到 Q_OBJECT 后,会为其生成 moc_Counter.cpp。下面我们逐段拆解这份生成文件的内容。
3.1 字符串存储(StringRefStorage)# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// moc_Counter.cpp (Qt 6.10.1 生成)
template <> constexpr inline auto Counter :: qt_create_metaobjectdata < qt_meta_tag_ZN7CounterE_t > ()
{
namespace QMC = QtMocConstants ;
QtMocHelpers :: StringRefStorage qt_stringData {
"Counter" , // 0
"Author" , // 1
"Demo" , // 2
"Version" , // 3
"1.0" , // 4
"valueChanged" , // 5
"" , // 6 (空标签, tag)
"newValue" , // 7
"overflowDetected" , // 8
"message" , // 9
"setValue" , // 10
"increment" , // 11
"reset" , // 12
"describe" , // 13
"value" , // 14
"CountDirection" , // 15
"Up" , // 16
"Down" // 17
};
所有 类名、方法名、参数名、属性名、枚举键名 全部打表存储在一个紧凑的字符串数组中,运行时通过索引查找。比如 Q_CLASSINFO("Author", "Demo") 对应的就是索引 {1, 2}。
这里的 StringRefStorage 类型是一个自定义的结构体类型,用来保存这个字符串数组和一些其他信息,通过可变长模板参数来实现:
1
2
3
4
5
6
7
8
9
10
11
12
template < typename ... Strings > struct StringRefStorage
{
static constexpr int StringCount = sizeof ...( Strings );
static constexpr size_t StringSize = stringSizeHelper ();
static_assert ( StringSize <= MaxStringSize , "Meta Object data is too big" );
const char * inputs [ StringCount ];
constexpr StringRefStorage ( const Strings & ... strings ) noexcept
: inputs { strings ... }
{ }
// ......
}
Tip
注释提到这里的数组最长只能有4G(64位)。但是可能还没等到触发断言编译器就崩溃了。
3.2 方法表(qt_methods)# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
QtMocHelpers :: UintData qt_methods {
// Signal 'valueChanged'
QtMocHelpers :: SignalData < void ( int ) > ( 5 , 6 , QMC :: AccessPublic , QMetaType :: Void , {{
{ QMetaType :: Int , 7 },
}}),
// Signal 'overflowDetected'
QtMocHelpers :: SignalData < void ( const QString & ) > ( 8 , 6 , QMC :: AccessPublic , QMetaType :: Void , {{
{ QMetaType :: QString , 9 },
}}),
// Slot 'setValue'
QtMocHelpers :: SlotData < void ( int ) > ( 10 , 6 , QMC :: AccessPublic , QMetaType :: Void , {{
{ QMetaType :: Int , 7 },
}}),
// Slot 'increment'
QtMocHelpers :: SlotData < void () > ( 11 , 6 , QMC :: AccessPublic , QMetaType :: Void ),
// Method 'reset'
QtMocHelpers :: MethodData < void () > ( 12 , 6 , QMC :: AccessPublic , QMetaType :: Void ),
// Method 'describe'
QtMocHelpers :: MethodData < QString () const > ( 13 , 6 , QMC :: AccessPublic , QMetaType :: QString ),
};
每个方法条目都记录了:
名称索引 (指向字符串表)标签索引 (为方法添加标签)访问级别 (Public / Protected / Private)返回类型 参数列表 (每个参数的 QMetaType + 名称索引)注意区别:SignalData、SlotData、MethodData 对应 signals、slots、Q_INVOKABLE 三种来源。运行时通过 QMetaMethod::methodType() 可以区分它们。
Note
方法在表中的 顺序就是方法 ID 。valueChanged = 0, overflowDetected = 1, setValue = 2, increment = 3, reset = 4, describe = 5。信号始终排在最前面 ,这个顺序在信号发射时至关重要。
3.3 属性表、枚举表与类信息# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 属性表
QtMocHelpers :: UintData qt_properties {
// property 'value'
QtMocHelpers :: PropertyData < int > ( 14 , QMetaType :: Int ,
QMC :: DefaultPropertyFlags | QMC :: Writable | QMC :: StdCppSet , 0 ),
// 末尾的 0 就是 NOTIFY 信号的索引 → 即 valueChanged (方法ID=0)
};
// 枚举表
QtMocHelpers :: UintData qt_enums {
// enum 'CountDirection'
QtMocHelpers :: EnumData < enum CountDirection > ( 15 , 15 , QMC :: EnumFlags {}). add ({
{ 16 , CountDirection :: Up }, // "Up" = 1
{ 17 , CountDirection :: Down }, // "Down" = -1
}),
};
// Q_CLASSINFO
QtMocHelpers :: ClassInfos qt_classinfo ({
{ 1 , 2 }, // "Author" = "Demo"
{ 3 , 4 }, // "Version" = "1.0"
});
这三张表让运行时可以:
通过 QObject::property("value") 反射式读写属性 通过 QMetaEnum::valueToKey(1) 把枚举值 1 转为字符串 "Up" 通过 QMetaObject::classInfo(i) 查询附加的键值对元信息 1
2
3
4
5
6
7
8
9
Q_CONSTINIT const QMetaObject Counter :: staticMetaObject = { {
QMetaObject :: SuperData :: link < QObject :: staticMetaObject > (), // 父类元对象(继承链)
qt_staticMetaObjectStaticContent < qt_meta_tag_ZN7CounterE_t > . stringdata , // 字符串表
qt_staticMetaObjectStaticContent < qt_meta_tag_ZN7CounterE_t > . data , // 元数据
qt_static_metacall , // 调度函数指针
nullptr ,
qt_staticMetaObjectRelocatingContent < qt_meta_tag_ZN7CounterE_t > . metaTypes , // 类型信息
nullptr
} };
这就是 Q_OBJECT 宏中声明的那个 static const QMetaObject staticMetaObject 的定义。它是整个元对象系统的入口 ,运行时所有的反射操作都从这里开始。
QMetaObject 的结构在 Qt 源码 qobjectdefs.h 中定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct QMetaObject
{
struct { // d
const QMetaObject * superdata ; // 父类的 QMetaObject
const uint * stringdata ; // 字符串表
const uint * data ; // 元数据整型数组
typedef void ( * StaticMetacallFunction )( QObject * , QMetaObject :: Call , int , void ** );
StaticMetacallFunction static_metacall ; // 调度函数
const SuperData * relatedMetaObjects ;
const QtPrivate :: QMetaTypeInterface * const * metaTypes ;
void * extradata ;
} d ;
};
这是整个 moc 生成代码中最关键的函数 ,它像一个总调度器,根据 QMetaObject::Call 的类型和方法 ID,路由到具体的 C++ 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void Counter :: qt_static_metacall ( QObject * _o , QMetaObject :: Call _c , int _id , void ** _a )
{
auto * _t = static_cast < Counter *> ( _o );
// 调用方法(信号/槽/Q_INVOKABLE)
if ( _c == QMetaObject :: InvokeMetaMethod ) {
switch ( _id ) {
case 0 : _t -> valueChanged (( * reinterpret_cast < std :: add_pointer_t < int >> ( _a [ 1 ]))); break ;
case 1 : _t -> overflowDetected (( * reinterpret_cast < std :: add_pointer_t < QString >> ( _a [ 1 ]))); break ;
case 2 : _t -> setValue (( * reinterpret_cast < std :: add_pointer_t < int >> ( _a [ 1 ]))); break ;
case 3 : _t -> increment (); break ;
case 4 : _t -> reset (); break ;
case 5 : { QString _r = _t -> describe ();
if ( _a [ 0 ]) * reinterpret_cast < QString *> ( _a [ 0 ]) = std :: move ( _r ); } break ;
}
}
// 信号的函数指针 → 索引映射(connect 时使用)
if ( _c == QMetaObject :: IndexOfMethod ) {
if ( QtMocHelpers :: indexOfMethod < void ( Counter ::* )( int ) > ( _a , & Counter :: valueChanged , 0 ))
return ;
if ( QtMocHelpers :: indexOfMethod < void ( Counter ::* )( const QString & ) > ( _a , & Counter :: overflowDetected , 1 ))
return ;
}
// 读属性
if ( _c == QMetaObject :: ReadProperty ) {
void * _v = _a [ 0 ];
switch ( _id ) {
case 0 : * reinterpret_cast < int *> ( _v ) = _t -> value (); break ; // 调用 READ 函数
}
}
// 写属性
if ( _c == QMetaObject :: WriteProperty ) {
void * _v = _a [ 0 ];
switch ( _id ) {
case 0 : _t -> setValue ( * reinterpret_cast < int *> ( _v )); break ; // 调用 WRITE 函数
}
}
}
四个分支各自负责:
QMetaObject::Call作用 触发场景 InvokeMetaMethodswitch-case 调用对应的信号/槽/Q_INVOKABLE 函数 emit、invokeMethod()、信号槽连接触发IndexOfMethod将函数指针 映射为方法 ID QObject::connect 新式写法(函数指针连接)ReadProperty调用属性的 READ 函数 QObject::property("value")WriteProperty调用属性的 WRITE 函数 QObject::setProperty("value", 42)
参数传递使用 void **_a 数组:_a[0] 是返回值,_a[1] 起是各个参数,通过 reinterpret_cast 还原类型。这就是 Qt 信号槽的 类型擦除 机制。
3.6 信号的函数体(moc 生成)# 信号只需要在头文件里 声明 ,不需要也不能自己实现 (signals: 区域下的函数没有函数体)。moc 会自动生成信号的实现:
1
2
3
4
5
6
7
8
9
10
11
// SIGNAL 0
void Counter :: valueChanged ( int _t1 )
{
QMetaObject :: activate < void > ( this , & staticMetaObject , 0 , nullptr , _t1 );
}
// SIGNAL 1
void Counter :: overflowDetected ( const QString & _t1 )
{
QMetaObject :: activate < void > ( this , & staticMetaObject , 1 , nullptr , _t1 );
}
当你写 emit valueChanged(42) 时:
emit 是一个空宏(#define emit),所以 emit valueChanged(42) 就是 valueChanged(42)调用上面这个 moc 生成的函数体 该函数调用 QMetaObject::activate(),传入 this、staticMetaObject、信号索引 0、以及参数 activate() 内部会遍历这个信号上所有已注册的连接(connection list),根据连接类型决定是 直接调用 还是 投递到事件队列 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int Counter :: qt_metacall ( QMetaObject :: Call _c , int _id , void ** _a )
{
_id = QObject :: qt_metacall ( _c , _id , _a ); // 先让父类处理(如 QObject 自己的属性 "objectName")
if ( _id < 0 )
return _id ;
if ( _c == QMetaObject :: InvokeMetaMethod ) {
if ( _id < 6 ) // Counter 有 6 个方法
qt_static_metacall ( this , _c , _id , _a );
_id -= 6 ;
}
// ... ReadProperty / WriteProperty 同理,Counter 有 1 个属性
return _id ;
}
这个函数是 Q_OBJECT 宏中声明的 virtual int qt_metacall(...) 的实现。它的作用是将 全局方法 ID 按继承链分段:父类处理父类的,本类处理本类的。_id -= 6 就是把本类的 6 个方法"消费"掉,剩下的 ID 留给子类。
1
2
3
4
const QMetaObject * Counter :: metaObject () const
{
return QObject :: d_ptr -> metaObject ? QObject :: d_ptr -> dynamicMetaObject () : & staticMetaObject ;
}
metaObject() 通常直接返回 &staticMetaObject。只有当对象有 动态属性 (通过 setProperty 设置了未在 Q_PROPERTY 中声明的属性)时,才会返回 dynamicMetaObject()。
1
2
3
4
5
6
7
void * Counter :: qt_metacast ( const char * _clname )
{
if ( ! _clname ) return nullptr ;
if ( ! strcmp ( _clname , qt_staticMetaObjectStaticContent < ... > . strings ))
return static_cast < void *> ( this );
return QObject :: qt_metacast ( _clname ); // 沿继承链向上查找
}
qt_metacast 是 Qt 自己的类型转换机制(类似 dynamic_cast,但不依赖 C++ RTTI),qobject_cast<Counter*>(obj) 内部就是调用它。
四、信号槽连接的底层机制# 4.1 QObject::connect 做了什么?# Qt 有两种 connect 风格,它们的底层路径不同:
旧式(字符串匹配) :
1
connect ( & counter , SIGNAL ( valueChanged ( int )), & notifier , SLOT ( onValueChanged ( int )));
SIGNAL() 和 SLOT() 是宏,展开后在方法名前加一个标识字符:
1
2
3
4
5
6
7
8
9
10
11
12
13
# define QMETHOD_CODE 0 // member type codes
# define QSLOT_CODE 1 // SLOT 为 1
# define QSIGNAL_CODE 2 // SIGNAL 为 2
# define QT_PREFIX_CODE(code, a) QT_STRINGIFY(code) #a
# define QT_STRINGIFY_METHOD(a) QT_PREFIX_CODE(QMETHOD_CODE, a)
# define QT_STRINGIFY_SLOT(a) QT_PREFIX_CODE(QSLOT_CODE, a)
# define QT_STRINGIFY_SIGNAL(a) QT_PREFIX_CODE(QSIGNAL_CODE, a)
# define SLOT(a) QT_STRINGIFY_SLOT(a)
# define SIGNAL(a) QT_STRINGIFY_SIGNAL(a)
// qobjectdefs.h
#define SLOT(a) "1"#a // "1onValueChanged(int)"
#define SIGNAL(a) "2"#a // "2valueChanged(int)"
connect 内部用这个字符串到 QMetaObject 的方法表中逐一比较签名 ,找到方法 ID 后建立连接。
新式(函数指针) :
1
connect ( & counter , & Counter :: valueChanged , & notifier , & Notifier :: onValueChanged );
connect 内部将函数指针传给 qt_static_metacall(_, IndexOfMethod, _, _),让 moc 生成的代码直接 把函数指针映射为方法 ID ,编译期就能做类型检查。
两种方式最终都会创建一个 Connection 对象,追加到发送者的连接列表中:
1
2
3
4
5
6
7
8
struct Connection {
QObject * sender ;
QObject * receiver ;
int signal_index ; // 信号在 sender 的方法表中的 ID
int method_index ; // 槽在 receiver 的方法表中的 ID(或 -1 表示 lambda)
uint connectionType ; // Direct / Queued / Auto ...
// ...
};
4.2 emit signal() 做了什么?# 以 emit counter.valueChanged(42) 为例,完整调用链为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
emit counter . valueChanged ( 42 )
│
├─ emit 是空宏,等价于 counter . valueChanged ( 42 )
│
▼
Counter :: valueChanged ( int _t1 ) ← moc 生成的信号函数体
│
├─ 将参数打包到 void * [] 数组
│
▼
QMetaObject :: activate ( this , & staticMetaObject , 0 /* signalIdx */ , nullptr , _t1 )
│
├─ 查找 signalIdx = 0 对应的连接列表
├─ 遍历所有 Connection :
│ ├─ DirectConnection :
│ │ 直接调用 receiver -> qt_static_metacall ( _ , InvokeMetaMethod , slotId , args )
│ │ └─ switch ( slotId ) → 调用实际的 C ++ 槽函数
│ ├─ QueuedConnection :
│ │ 将参数深拷贝,封装为 QMetaCallEvent
│ │ └─ 投递到 receiver 所在线程的事件队列
│ │ └─ 事件循环取出后再调用 qt_static_metacall
│ └─ AutoConnection :
│ 判断 sender 和 receiver 是否在同一线程
│ ├─ 同线程 → Direct
│ └─ 跨线程 → Queued
│
▼
返回
4.3 连接类型对比# 类型 行为 线程安全 使用场景 AutoConnection同线程→Direct;跨线程→Queued ✅ 默认且最常用 DirectConnection在发射者线程中立即同步 调用槽 ❌(槽需自行保证线程安全) 同线程、需要立即响应 QueuedConnection参数拷贝后投递到接收者线程的事件队列 ✅ 跨线程通信 BlockingQueuedConnection同 Queued,但发射者阻塞直到槽完成 ⚠ 死锁风险 需要等待跨线程结果 UniqueConnection防止重复连接(可与上述 OR 组合) — 防止重复 connect
Important
QueuedConnection 要求参数类型必须已通过 qRegisterMetaType<T>() 注册(或是 Qt 内建类型如 int、QString),因为参数需要被拷贝并序列化到事件中。
对于挂载在同一个信号的多个槽函数,根据FIFO的规则进行顺序调用。
五、Q_PROPERTY 属性系统# 1
Q_PROPERTY ( int value READ value WRITE setValue NOTIFY valueChanged )
这行声明告诉 moc 为 Counter 注册一个名为 "value" 的属性。moc 为它生成了 ReadProperty 和 WriteProperty 的 switch 分支(见 3.5 节)。
运行时通过 QObject 的通用接口访问:
1
2
3
4
5
6
7
// 反射式读写(不需要知道具体类型)
QVariant v = counter . property ( "value" ); // 内部: qt_static_metacall(_, ReadProperty, 0, _)
counter . setProperty ( "value" , 100 ); // 内部: qt_static_metacall(_, WriteProperty, 0, _)
// 动态属性(未在 Q_PROPERTY 中声明)
counter . setProperty ( "customTag" , "hello" ); // 存储在 QObject 内部的 QVariantMap 中
counter . property ( "customTag" ); // 照样可以读取
动态属性不会出现在 QMetaObject 的属性表中,也不会触发 moc 生成的 ReadProperty/WriteProperty 分支。
常用的属性声明关键字:
关键字 必需 说明 READ✅ 读取函数(const 成员函数) WRITE❌ 写入函数 NOTIFY❌ 属性变化时发射的信号(QML 绑定必需) MEMBER❌ 直接绑定到成员变量(Qt5.1+),可替代 READ + WRITE CONSTANT❌ 属性不可变,不可与 NOTIFY / WRITE 共存 BINDABLE❌ Qt6 绑定属性(QBindable<T>)
六、Q_ENUM 与枚举反射# 1
2
enum CountDirection { Up = 1 , Down = - 1 };
Q_ENUM ( CountDirection )
注册后,moc 在枚举表中写入了 {16, Up}, {17, Down} 这样的 key→value 映射。运行时可以:
1
2
3
4
QMetaEnum me = QMetaEnum :: fromType < Counter :: CountDirection > ();
me . valueToKey ( 1 ); // → "Up"
me . keyToValue ( "Down" ); // → -1
qDebug () << Counter :: Up ; // 输出 "Counter::Up" 而非 "1"
七、Q_INVOKABLE 与动态调用# 1
2
Q_INVOKABLE void reset ();
Q_INVOKABLE QString describe () const ;
标记为 Q_INVOKABLE 的函数在方法表中类型为 MethodData(既不是 Signal 也不是 Slot),但同样通过 qt_static_metacall 的 InvokeMetaMethod 分支调用。运行时可以通过字符串名调用:
1
2
3
4
5
6
7
8
// 无返回值
QMetaObject :: invokeMethod ( & counter , "reset" );
// 有返回值
QString result ;
QMetaObject :: invokeMethod ( & counter , "describe" ,
Qt :: DirectConnection ,
Q_RETURN_ARG ( QString , result ));
这也是 QML 调用 C++ 方法 的底层机制——QML 引擎通过 QMetaObject 查找方法名,然后走 invokeMethod 路径。