本篇在博客园地址https://www.cnblogs.com/bbqzsl/p/18252961
本篇内容包括:
win32窗口嵌入Qt UI。反斗玩转signal-slot。最后 通达信 x 微信 x Qt 做手术。
Qt Alien Widget是一种广义的DirectUI。
在UI技术中,DirectUI和Alien Widget的概念有所重叠,但具体实现和应用场景有所不同。为了更好地理解它们之间的关系,让我们先定义清楚这几个概念。
DirectUI
DirectUI 是一种界面绘制技术,主要特点是通过直接绘制UI元素,而不是依赖操作系统提供的原生控件。这种方式有以下几个特点:
- 跨平台一致性:不依赖操作系统的控件,因此在不同操作系统上可以实现一致的外观和行为。
- 高性能:直接绘制可以优化绘制路径,减少不必要的开销,从而提高性能。
- 高自由度:开发者可以完全控制UI元素的绘制和交互,灵活性更高。
Native Widget
Native Widget 是由操作系统提供和管理的控件,这些控件直接使用操作系统的窗口和绘图资源。
- 系统控件:直接由操作系统管理和绘制,提供与操作系统一致的外观和行为。
- 平台相关:在不同的操作系统上,控件的行为和外观可能不同。
Alien Widget
Alien Widget 是指那些不依赖于操作系统提供的控件,而是由应用程序自身实现和管理的控件。在Qt中,默认控件(如QPushButton
, QLineEdit
等)都是Alien Widget。
- 跨平台一致性:在不同操作系统上可以实现一致的外观和行为。
- 独立性:不依赖操作系统的控件。
- 灵活性和定制性:开发者可以完全控制控件的外观和行为。
Qt的Alien Widget和DirectUI的关系
Qt的Alien Widget可以看作是DirectUI的一种实现,因为它们都强调不依赖操作系统的原生控件,通过直接绘制实现跨平台一致性和高自由度。
主要特点:
- 绘制机制:Qt的Alien Widget是通过Qt的绘图系统(如QPainter)来进行绘制的,这类似于DirectUI技术中直接绘制的概念。
- 跨平台一致性:由于Qt的Alien Widget不依赖于操作系统的控件,它们在不同操作系统上提供了一致的外观和行为。
- 高自由度和定制性:开发者可以使用Qt提供的丰富API来自定义控件的外观和行为。
结论
可以认为Qt的Alien Widget是DirectUI技术的一种实现形式。两者都不依赖于操作系统的原生控件,通过直接绘制来实现跨平台一致性和高自由度。Qt的Alien Widget利用Qt框架的强大功能,实现了与DirectUI类似的目标。因此,从这个角度来看,Qt的Alien Widget确实可以被视为一种DirectUI技术。
Qt界面不必一定依赖QApplication::exec才能运行
Native Widget 和 QWindow 在 Qt 的图形用户界面编程中有不同的用途和特性。以下是对它们的详细对比和解释:
Native Widget
- 定义:Native Widget 是由操作系统提供和管理的控件。这些控件直接使用操作系统的窗口和绘图资源,不依赖于 Qt 的绘图系统。
- 特性 :
- 系统控件:直接由操作系统管理和绘制,提供与操作系统一致的外观和行为。
- 不依赖 Qt 事件循环 :可以在不依赖于
QApplication::exec()
的情况下运行,因为它们由操作系统管理事件循环。 - 平台相关:在不同的操作系统上,控件的行为和外观可能不同。
- 使用场景:需要与系统深度集成,或使用特定平台功能时使用。例如嵌入系统控件到 Qt 应用程序中。
QWindow
- 定义:QWindow 是 Qt 的一个抽象类,表示一个独立的窗口。它提供了一个轻量级的窗口对象,可以用于显示内容,但不提供高级的控件功能。
- 特性 :
- Qt 窗口对象:QWindow 是 Qt 提供的窗口类,可以与 Qt 的绘图系统和事件系统无缝集成。
- 依赖 Qt 事件循环 :QWindow 依赖于
QApplication::exec()
或QGuiApplication::exec()
来运行事件循环。 - 跨平台一致性:Qt 提供了一致的 API 和行为,不论运行在什么平台上,Qt 窗口的行为和外观都是一致的。
- 使用场景:用于创建自定义窗口、渲染 OpenGL/Vulkan 内容或其他需要低级别窗口访问的场景。
比较和联系
- 事件循环 :Native Widget 不依赖 Qt 的事件循环,可以独立于
QApplication::exec()
运行。而 QWindow 需要 Qt 的事件循环才能正常工作。 - 管理和绘制:Native Widget 由操作系统管理和绘制,提供原生外观和行为。QWindow 由 Qt 管理,可以使用 Qt 的绘图系统来绘制内容。
- 灵活性和集成:Native Widget 适用于需要直接访问操作系统功能和外观一致性的场景。QWindow 提供更高的灵活性和跨平台一致性,适用于需要使用 Qt 提供的高级功能和自定义绘图的场景。
结论
Native Widget 和 QWindow 是 Qt 中不同层次的组件,分别适用于不同的使用场景。Native Widget 更适合需要深度集成操作系统功能的应用,而 QWindow 则更适合需要使用 Qt 的绘图和事件系统的应用。两者的选择应根据具体的需求和应用场景来决定。
以上是AI提供的分析。
由上述可知,Alien Widget可以作为一种dui方案使用,为保证跨平台一致性,请使用QApplication::exec()。
main()
{
QApplication app;
QMainWindow mainWin;
mainWin.show();
app.exec();
}
QApplication跟QMainWindow为固定定式。这个定式能够保证跨平台 一致性。
但是,在windows平台的软件,就有混合使用UI框架的需求。
所以针对windows平台还有一个特殊解决方案:https://github.com/qtproject/qt-solutions/tree/master/qtwinmigrate。
里面包含了三种迁移方案,分别是QMfcApp,QWinHost,QWinWidget。
QMfcApp是方便QApplication跟QMainWindow定式的使用,将QApplication跟CWinApp整合在一起,CWinApp::Run时绑定QApplication::exec。
QWinHost能够将其它框架(不限于MFC)的OS native window嵌入到QWidget。
QWinWidget能够QWidget嵌入到OS native window,以一个native widget hwnd挂到父hwnd。
native widget是由platforms/plugin/qwindows.dll支持的。代码路径在qtbase/src/plugins/platforms/windows,其中qwindowswindow.cpp实现了native window的WndProc,qWindowsWndProc。并将消息转换QtWindows::WindowsEventType由QWindowsContext::windowsProc进行处理。
通达信用的是qt5.5。
QPlatformWindow, qtbase/src/gui/kernel/qplatformwindow.h
QWindowsWindow, qtbase/src/plugins/platforms/windows/qwindowswindow.h
QWindow, qtbase/src/gui/kernel/qwindow.h
QMainWindow, qtbase/src/widgets/widgets/qmainwindow.h
QWidgetWindow, qtbase/src/widgets/kernel/qwidgetwindow_qpa_p.h
|-------------|---------------|------------------|
| QWidget | QSurface | QPlatformSurface |
| QMainWindow | QWindow | QPlatformWindow |
| | QWidgetWindow | QWindowsWindow |
| | "yes, i do." | "you own me?" |
尽管这几个都叫Window,单从名字好容易迷惑,它们不尽是QWindow。QWindow与QPlatformWindow是窗口的两个面,QWindow是跨平台的封装,QPlatformWindow则是底层支持。QWidgetWindow是一个私有类,将QWidget与QWindow能够联系在一起,QWidget从而能够依附在一个底层窗口分派事件。
QApplication::exec只是一个幌,隐藏了许多真相。QWidget分派事件需要的窗口过程是由QPlatformWindow提供支持的,但是Qt事件分派器却依赖QApplication,QApplication是一个QObject,QObject设计成绑定于一个线程上运行。所以使得QWidget依赖于QApplication所绑定的线程。因此一个进程范围内只允许有一个Qt的UI线程,而这个线程跟QApplication绑定在一起。
真相是,只有在QApplication绑定的线程上,QPlatformWindow才能在native窗口的消息循环中正常分派Qt事件。所以运行Qt UI的必要条件是QPlatformWindow,QApplication,并且在同一线程,这个线程上有消息循环。QApplication::exec是充分条件但不是必要的。
QPlatformWindow对应一个系统窗口,在跨平台层对应一个QWindow,其中它的继承类QWidgetWindow可以承载QWidget。QMainWindow是一个QWidget并且自带QWidgetWindow。所以QWindow对应着一个系统窗口。自然地QMainWindow也对应着一个系统窗口,同时成为所有子QWidget的窗口设备根基。迁移解决方案的QWinWidget就是用来替代QMainWindow的位置,为QWidget提供窗口设备根基。
QPlatformWindow由平台相关的qwindows.dll进行支持,在windows平台就是QWindowsWindow。必须依赖QApplication进行Object event filter。而QApplication只能绑定在一个线程上。同一进程同一时间,只能有一个线程上的QPlatformWindow以及Widgets可以正常运行。如果在QApplication之外的线程创建的QPlatformWindow以及Widgets是不能正常分派事件的,当它们需要响应事件的时候,QApplication就会警告"Object event filter cannot be in a different thread."而不能响应。
下图演示,在两个线程分别创建QWinWidget,只有跟随QApplication同一个线程的QWidget能够正常响应事件。
为了演示,我将通达信君的tdxzdview100.dll插入到微信MM的体内,在微信MM体内分别诞生QWinWidget,QWindow,还有QMainWindow。微信MM体内是个没有被MFC污染的洁净环境。演示过程展示了只要有消息循环,就可以运行qt框架的UI。
接下来就是逆向Qt。
Qt以metaObject行了类型反射之实。
定义
反射(Reflection)是一种程序能够在运行时自我检查和操作其结构和行为的机制。通过反射,程序可以动态地获取类型信息,并操作对象、方法、属性等。
涵盖的操作
反射技术涵盖了以下几类操作:
-
类型检查和获取元数据:
- 获取类型信息:通过反射,可以在运行时获取对象的类型信息,如类名、方法、属性、字段等。
- 检查类型成员:检查类型的成员(如方法、属性、事件、构造函数等)的名称、参数、返回类型等信息。
-
动态创建对象:
- 实例化类型:通过反射,可以动态地创建类型的实例,而不需要在编译时指定类型。
-
调用方法:
- 动态调用方法:可以在运行时调用对象的方法,而不需要在编译时知道方法的名称。
-
访问和修改属性和字段:
- 动态访问属性和字段:在运行时读取或修改对象的属性和字段。
-
检索和操作特性(注解):
- 获取和操作特性(注解):通过反射,可以获取和操作应用在类型、方法、属性等上的特性(如C#中的属性,Java中的注解)。
Meta Object可以看作一个小型数据库,提供类型信息。
metacast负责反射类型转换。
metacall是所有反射操作的接口函数。原型类似于ioctl, fcntl。接受包括方法调用,属性访问等。
对于signal-slot的场合,本篇的metacall专指(QMetaObject::Call == InvokeMetaMethod)。
声明过signal跟slot的方法,本质上就是能够通过反射进行调用的方法,我们需要将QObject具体类的成员方法声明为signal或slot,然后还要用moc生成相关的metaobject代码,才能注册到反射。
signal-slot本质就变成了metacall的传递。connect就是对象的metacall连接成拓扑关系,使得metacall可以按拓扑关系进行传递。
signal-slot字面上局限了想象。connect must from signal to slot。然而connect实质是accept from signal to method which can be slot or signal as well.
我们换一下表达式,signal [, signal, ...] - slot,这样就开阔了。
反射不仅提供了类型信息,也可以修改类型信息。简直就是送上门让人逆向。
下图演示通过metaobject查询类型信息,对象方法
下图演示通过跟踪QObject::connect,收集所有signal-slot。这种方法在各Qt版本中比较通用。比较好忽略版本的差异。
下图演示,通过类型反射直接调用对象的slot方法。
下图演示几种修改signal-slot连接关系的方法。通过dynamic meta,或者修改connection,将qt按钮连接到微信按钮,将qt编辑框连接到微信按钮,通过反射来调用按钮的clicked(),调用编辑框的textChanged(QString)。
对于逆向,不需要编译器,不需要头文件,不需要因为slot而去找代码moc编译QObject。
反射的一个特性就是动态修改,那就直接修改。
插入我的slot到signal。signal-slot定式限制了想象,好像只能有一条出路,我必须要有一个slot,方法类型需要对上signal。我必须要写一个QObject达成一个slot,用moc去生成代码。
但是,正如我在上面转换的定式,signal [,signal,... ] -slot,这样一来,signal的下一跳不一定必要是slot,而可以是signal。
拿QPushButton举例,signal clicked() 连接到一个影子QPushButton的signal clicked(),就可以除去一大票工作。
思路一, 通过dynamic meta。
要是我将QPushButton的metacall修改了signal clicked()的处理,必然导致全部QPushButton不能正常工作。意外地,Qt贴心地为我提供了Dynamic MetaObject,解决了我的烦恼。那么我需要moc编译另一个MetaObject吗?不,我以经将moc丢进垃圾桶了。我们可以直接将QPushButton的MetaObject连根复制一份。正所谓CopyOnWrite。我们只要修改副本的qt_static_metacall,将signal clicked()处理修改即可。最后将副本放到影子QPushButton的Dynamic MetaObject。手术完成。缺点是要将其它signal的处理也在同一个qt_static_metacall中修改,不灵活。另外,像clicked()是在父类QAbstractButton中声明为signal的,当其作为connect的receiver端时,并不是用QPushButton的MetaObject::qt_static_metacall分派,而是用QAbstractButton::qt_static_metacall进行分派。十分不方便。这个可以通过QMetaObjectPrivate::connect方法获知继承树中哪个类的MetaObject被选用。
思路二,通过Connection。
Qt5又一贴心设计,QObject::connect返回的不再是冰冷的bool,取而代之是热气腾腾的Connection。不同版本有所差异。但基本三要素没有变,sender, recver, metacall。That's good. Connection还直接指明了metacall函数指针。思路一的工作都可以直接删减了。一个Connection的method_index是固定,所以自已写一个metacall修改起来就简单得多,单一处理,灵活性大大提高。
思路三,自已跟自已。
经过前两次思路进化后,最后我又注意到,影子QPushButton还必要吗?nai, hitsuyu ga nai. 自已跟自已connect,直接省事。谨记,要将修改操作原子地在sender同一线程上进行,不然sender抢先signal一下,也就小狗追尾巴直到天荒地老了。如果需要代码在自已希望的线程上分派,那么还得依靠一个影子QPushButton绑定一个线程。
思路四,Qt5.5也可以connect到Functor。
虽然,Qt5.7才支持connect到Functor。但是在编程时,应用上面的思路,完全可以实现达到目标。
看啦,就这么简单好玩,祝大家也玩得开心。
本篇到这里,下一篇再见。
我还有逆向通达信系列。
我还有一个K线技术工具项目KTL,可以用C++14进行公式,QT,数据分析等开发。