本篇在部落格園地址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。但是在程式設計時,應用上面的思路,完全可以實現達到目標。
看啦,就這麼簡單好玩,祝大家也玩得開心。
本篇到這裡,下一篇再見。
逆向WeChat(四,mars)
逆向WeChat(三, EventCenter)
逆向WeChat (二, WeUIEngine)
我還有逆向通達信系列。
我還有一個K線技術工具專案KTL,可以用C++14進行公式,QT,資料分析等開發。