為什么是WTL
項目是toB的控制端軟件,WTL程序包小嘶伟,僅有一個EXE文件除嘹,對不同版本操作系統(tǒng)兼容性好写半,B端客戶還用XP的不在少數(shù),發(fā)布與使用的便捷性強尉咕。
為什么寫這篇文章
WTL是基于模板對窗口封裝叠蝇,接近系統(tǒng)底層,靈活度高年缎,只是由于資料匱乏開發(fā)上手比較困難悔捶,關(guān)于WTL的淵源我就不介紹了,重要的是单芜,如果你在網(wǎng)絡(luò)上搜索WTL的開資料蜕该,只能找到各種各樣的廣告以及WTL for MFC Programmers這篇文章的翻譯版本,如果你沒有MFC基礎(chǔ)洲鸠,看起來將有一定的困難堂淡,而像我這樣沒有C++基礎(chǔ),則更加頭疼扒腕。作為IOS我有還行的C語言基礎(chǔ)绢淀,Objective-C也使用了許多與C++類似的語法因此我大概用了一周時間學(xué)習(xí)C++和一周時間熟悉WTL最基本的框架使用以及幾天時間了解公司項目框架構(gòu)成后,上手并完成了幾個界面模塊的擰螺絲工作瘾腰。這篇文章當(dāng)然不足以讓你成為WTL項目負(fù)責(zé)人皆的,但是應(yīng)該能幫助你順利的上手。
這篇文章都要講什么
利用WTL進行Win窗口界面程序開發(fā)主要是UI部分內(nèi)容蹋盆,主要有以下幾個部分
- 環(huán)境配置
- 創(chuàng)建第一個窗口
- 自定義繪圖
- 基礎(chǔ)控件:CButton CEdit CScrollerBar
- 動態(tài)及使用資源文件頁面布局
- 制作自定義控件
- WTL擴展增強-DDX
你需要提前準(zhǔn)備什么祭务?
需要先學(xué)好C++嗎内狗?需要先看一看MFC嗎?都不需要义锥,但是你至少需要:
1.有C語言基礎(chǔ)柳沙。
2.理解面向?qū)ο蟮某绦蜷_發(fā),不論是C++還是JAVA拌倍,OC或者SWIFT等其他面向?qū)ο蟮拈_發(fā)語言赂鲤,理解OOP即可。
3.如果你完全沒有C++基礎(chǔ)柱恤,也可以照著我的代碼一步步做数初,關(guān)于C++面向?qū)ο蟮奶匦岳缍嗬^承,模板編程等梗顺,我也會在到的地方做出簡單直觀的解釋泡孩,C++開發(fā)的其他資料比較詳實如果有理解不了的內(nèi)容,百度一下寺谤。
4.如果你有一些客戶端開發(fā)基礎(chǔ)IOS/安卓仑鸥,會有一些幫助。
5.如果你是MFC開發(fā)人員可以直接看WTL for MFC Programmers变屁。
更重要的資料
作為一篇以引入為目的的教程眼俊,我不會過多的介紹Windows系統(tǒng)的功能以及所有各種復(fù)雜的控件
如果你需要相關(guān)的資料 微軟的官方文檔是最佳參考資料
https://docs.microsoft.com/en-us/cpp/mfc/reference/mfc-classes?view=vs-2019
WTL基于ATL,ATL中的類大部分與MFC通用粟关,因此官方的MFC 類文檔是參考和學(xué)習(xí)價值極高的 某度甚至檢索到一大堆廣告 都不會把你引向官方文檔
這里是WTL的下載地址
https://sourceforge.net/projects/wtl/
WTL項目中也帶有一些例程疮胖,可以參考。
正式開始教程
項目搭建
1.下載安裝VS闷板。我使用的是Visual Studio2015 vs的版本對WTL影響不大澎灸,默認(rèn)配置即可。
2.下載WTL遮晚,并取出include文件夾性昭,這就是項目需要的WTL的全部文件
3.在VS中創(chuàng)建一個C++空項目
4.在項目中添加對WTL的引用
5.創(chuàng)建Main.cpp文件和stdafx.h文件。并分別寫入以下內(nèi)容
//stdfax.h:
#define STRICT
#define WIN32_LEAN_AND_MEAN
//#define _WTL_USE_CSTRING
#include <atlbase.h>
#include <atlstr.h>
#include <atlapp.h>
extern CAppModule _Module;
#define _WTL_NO_CSTRING
#include <atlwin.h>
#include <atlmisc.h>
#include <atlcrack.h>
#include <atlframe.h>
#include <atlctrls.h>
#include <atldlgs.h>
#include <atlwin.h>
// main.cpp:
#include "stdafx.h"
CAppModule _Module;
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
_Module.Init(NULL, hInstance);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
_Module.Term();
return msg.wParam;
}
這些基本是固定寫法鹏漆。stdfax.h作為公共頭文件,引入需要的WTL文件创泄,由于include是將對應(yīng)文件的代碼復(fù)制到當(dāng)前文件中因此要注意如果你不知道怎么回事艺玲,就不要改變引用順序 ,包括其中插入的宏命令位置鞠抑。
Main.CPP是程序入口饭聚。CAppModule _Module;是保存主線程ID和消息循環(huán)的實例。
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
這個循環(huán)是主消息循環(huán)搁拙,控制著應(yīng)用的生命周期秒梳。Win程序的運行依賴于消息機制法绵。邊用便了解即可。此處只要知道走入這一行 程序便開始在消息循環(huán)中執(zhí)行酪碘,存活朋譬。因此要在它之前插入操作入口。
程序的基本運行環(huán)境到這里就搭建完成了兴垦。WTL其實是一套對WinAPI界面描述的封裝徙赢,可以非常輕松的擴展或引入到其他項目中。
開始創(chuàng)建自己的窗口
項目主入口完成后就可以開始通過WTL創(chuàng)建自己的窗口了探越。在此之前狡赐,我需要解釋幾個概念,熟悉C++的話可以跳過這部分钦幔。
一.C++中的變量枕屉。
如果你沒有C++基礎(chǔ) 那么你需要重新理清C++中的變量和對象關(guān)系。
對于變量 它在創(chuàng)建的時候在棧中分配內(nèi)存鲤氢。C++中的對象可與基本類型一樣直接創(chuàng)建搀擂,創(chuàng)建時即分配內(nèi)存,離開作用域時出棧釋放铜异。也可以在堆中創(chuàng)建即創(chuàng)建指針指向new的對象
void founction(){
yourClass obj1;
yourClass *obj2 = new class;
}//obj1釋放哥倔,obj2指針釋放 ,*obj2對象內(nèi)存溢出
二.模板。
WTL是基于模板開發(fā)揍庄,模板是實現(xiàn)泛型編程的基礎(chǔ)咆蒿。可以將模板看成一種描述方式蚂子,用于描述一個類型的對象遵循模板類型對象的類型聲明沃测。
舉個例子
//一個單純的類A
class A {
void functionA(){
}
}
//一個單純的類B
class B {
void functionB(){
}
}
//一個單純的類P
class P {
void functionP(){
}
}
//準(zhǔn)備一個遵循T模板的類C
template<class T>//聲明模板
class C:public T{
void functionC(){
}
}
//自己定義Class D
class D :public C<A>{
}
//D就是一個包含 functionC,functionA三個方法的類這樣的描述指明了D繼承于C 而C繼承于A同樣
//自己定義Class E
class E :public C<B>{
}
//E則是包含 functionC,functionB的類
模板可以用于描述一種繼承關(guān)系,也就可以在使用的時候再去定義一個對象繼承于誰
//自定義一個class F
template<class T /* = P */>//聲明模板同時注釋其類型
class F :public T{
void functionF(){
//調(diào)用自己繼承而來的functionP 繼承于F的類必須指定其模板參數(shù)為P及P的子類
functionP()
}
//或用于方法
//創(chuàng)建一個R類型的對象并調(diào)用其founctionP方法
template <class R /* = P */>
void founctionF2(){
R objR;
objR.founctionP();
}
void functionF3(){
//調(diào)用時必須使用P或P的子類
founctionF2<P>();
}
}
這么做則相當(dāng)于我們創(chuàng)建一個 F 類并且指定其繼承于 T 類,并且 T 必須是一個 P 的子類因為我們在 F 中直接使用了 P 的方法食茎。
刨除元編程來看蒂破,模板點像多態(tài),但與多態(tài)相比是邏輯逆序的别渔,多態(tài)是“我的子類繼承于我便擁有了我的接口和屬性”而模板是“使用我創(chuàng)建的類必須包含我調(diào)用的接口和屬性附迷,從而成為符合本模板邏輯的類”。強調(diào)邏輯復(fù)用性而非結(jié)構(gòu)哎媚。
WTL中模板主要用于聲明自定義控件的繼承類型 喇伯。
三. 虛函數(shù)
對于一個類中的函數(shù)使用virtual關(guān)鍵字 則這個函數(shù)是一個虛函數(shù)
virtual void founction(){};//虛函數(shù)
virtual void founction() = 0;//純虛函數(shù)
虛函數(shù)可以視作是一種接口聲明。
純虛函數(shù)更多是一種描述性用途拨与,它規(guī)定了包含純虛函數(shù)的類為基類 稻据,不可直接使用只能使用其子類。
回到WTL
在理解以上三點后 就可以開始了解WTL界面搭建的基本工具了买喧。
在Win程序中 一切界面元素皆是CWindow捻悯。在WTL中最基本的界面類是CWindowImpl匆赃。
在正式開始使用前先簡單了解一下CWindowImpl
它有三個模板參數(shù) 其中 T自定義的類 TBase 是WinAPI中的窗口類 TWinTraits 是設(shè)置模板,可以不使用 先無視它今缚。
從它的定義可以看出來兩個泛型參數(shù) TBase用于類型傳遞算柳,T則用于本地調(diào)用,讓CWindowImpl能夠調(diào)用到你自定義的類的一些類方法荚斯,如果你重寫了這些方法埠居。防止子類重寫這些方法后調(diào)用的仍然是父類的這些方法。
接著向上看
這里給與了Tbase以及TWinTraits 初始值因此如果你不寫這兩個參數(shù)事期,才不會報錯滥壕。這個類主要是設(shè)置窗口的一些默認(rèn)屬性∈奁可以不用關(guān)心他绎橘。接下來它又將TBase傳遞給了CWindowImplRoot。接著向上看唠倦。
這樣我們就找到了最終的目的地 傳遞到這里的TBase指定了自定義控件的實際基類称鳞。
并且它同時繼承于CMessageMap馬上就會用到。我們先看一看它稠鼻。
在CMessageMap中 定義了一個純虛函數(shù)ProcessWindowMessage冈止。也就是這里讓我們必須使用CWindowImpl的子類并且必須實現(xiàn)ProcessWindowMessage方法。CMessageMap貼心的提供了一整套宏命令用來實現(xiàn)消息和消息轉(zhuǎn)發(fā)候齿。后面用到時會講熙暴。
整理一下:
繼承于CWindowImpl的類需要至少傳三個參數(shù)<T,TBase,TWinTraits >其中T用于CWindowImpl調(diào)用。TBase必須是CWindow及它的子類慌盯,有默認(rèn)值CWinodw周霉,實際用途為傳遞到最后給CWindowImplRoot并被它繼承為該自定義類的基類。TWinTraits用于設(shè)置樣式有默認(rèn)值亚皂。
定義第一個自定義的窗口
此時創(chuàng)建了一個FirstWindow 類俱箱,它是一個基本的WTL對象。繼承于CWindow灭必。
由于CMessageMap中的純虛函數(shù)存在狞谱,因此必須添加
宏命令對其做出實現(xiàn)。
BEGIN_MSG_MAP(FirstWindow)
END_MSG_MAP()
這些是自動創(chuàng)建的構(gòu)造函數(shù)和析構(gòu)函數(shù)禁漓。如無需要跟衅,可以刪掉。
FirstWindow();
~FirstWindow();
FirstWindow::FirstWindow()
{
}
FirstWindow::~FirstWindow()
{
}
練習(xí)項目中可以將代碼全部寫在.h文件璃饱。
然后使用它
在WinMain函數(shù)中与斤,Moudle創(chuàng)建后肪康,消息循環(huán)開始執(zhí)行前創(chuàng)建并顯示這個窗口
其中CRect是描述位置大小的對象荚恶,可以設(shè)置上下左右邊相對于父控件的位置撩穿。
創(chuàng)建了Window對象后需要調(diào)用Create方法創(chuàng)建窗口
該方法有以下幾個參數(shù)
In_opt_ HWND hWndParent, //父控件的句柄
_In_ _U_RECT rect = NULL,//指定位置大小
_In_opt_z_ LPCTSTR szWindowName = NULL,//窗口名稱,會顯示在左上角系統(tǒng)欄谒撼。默認(rèn)樣式可見
_In_ DWORD dwStyle = 0,//窗口風(fēng)格類型食寡,屬性設(shè)置
_In_ DWORD dwExStyle = 0,//窗口擴展風(fēng)格
_In_ _U_MENUorID MenuOrID = 0U,//資源ID灶伊,可以理解為該對象的數(shù)字標(biāo)記祖娘,后面使用資源文件的時候會再次用到
_In_opt_ LPVOID lpCreateParam = NULL//16位機遺留參數(shù)。現(xiàn)在用不著
這里我簡單解釋一下句柄
句柄是是一個結(jié)構(gòu)體表示一個Window資源缚柏,也就相當(dāng)于它的索引辩蛋,通過句柄可以找到這個資源呻畸。也就可以通過句柄首發(fā)消息。CWindow對句柄進行了重載悼院。使得CWindow的賦值操作實際只是讓另一個CWindow對象的句柄指針指向目標(biāo)對象的句柄伤为。暫時了解一下就行了。
在Create方法中指定句柄通常用于設(shè)置控件位置据途,也就是說 rect參數(shù)設(shè)置的控件位置是相對于 hWndParent 的绞愚。而指定hWndParent為空的時候,窗口的位置是相對于屏幕的颖医,左上角為0.0點位衩,向右/向下為正軸。
此時我們添加的代碼創(chuàng)建了一個從屏幕左上角X/Y軸均為以10為起點到500為終點的FirstWindow類型的窗口熔萧。執(zhí)行一下看看
看起來左側(cè)邊距似乎是要比頂部多一些糖驴,這是因為系統(tǒng)窗口的樣式是有邊框的,分布在左右下側(cè)哪痰,在Win10中為透明樣式遂赠,大約8像素
窗口缺了一個關(guān)閉按鈕
可以在創(chuàng)建時dwStyle屬性中設(shè)置該參數(shù)∩谓埽可以使用 | 按位操作同時設(shè)置多個 跷睦。然后可以可以使用X按鈕關(guān)閉窗口。
Window.Create(NULL, rc, "HELLO WORLD",WS_VISIBLE | WS_SYSMENU,NULL,0U,NULL );
但是這并不能關(guān)閉程序!
點擊X只是隱藏了窗口肋演,程序仍然在主方法中循環(huán)抑诸,沒有任何變量被釋放。
我們要在關(guān)閉窗口的時候關(guān)閉程序爹殊,就要用到消息機制蜕乡。
在WTL窗口中的生命周期方法,界面交互都有對應(yīng)的消息分發(fā)到對應(yīng)的類中梗夸。這些消息的接收轉(zhuǎn)發(fā)层玲,都被封裝為一系列的宏命令,將其插入到BEGIN_MSG_MAP和END_MSG_MAP之間并實現(xiàn)對應(yīng)的方法即可接收消息
PostQuitMessage(0) 是WinApi方法,可以結(jié)束程序辛块。
MSG_WM_CLOSE(OnClose) 聲明了由OnClose方法接收關(guān)閉窗口的消息畔派。
可以更改方法名,但是一般使用原頭文件中的方法名以便閱讀
同理可以推得润绵,如窗口創(chuàng)建线椰,移動,繪制鼠標(biāo)時間等大量的生命周期/操作相應(yīng)等系統(tǒng)管理的事件都可以通過這個機制來操作
可以直接去對應(yīng)的頭文件中找到消息和消息參數(shù)定義尘盼。
但是要注意 調(diào)用該方法結(jié)束應(yīng)用前必須刪除掉所有你創(chuàng)建的窗口
DestroyWindow()可以刪除當(dāng)前窗口憨愉,也可以填入一個窗口的句柄用于刪除指定窗口。
繪圖
由于WinApi誕生之時還沒有Material Design這樣美觀的視覺表達規(guī)范卿捎,其系統(tǒng)控件樣式相當(dāng)?shù)膮T乏且充滿工程師設(shè)計風(fēng)格配紫,因此大多數(shù)時候,控件都需要實現(xiàn)自定義繪圖午阵,即使只是簡單的設(shè)置背景顏色笨蚁。
完成了上面最簡單的視窗控件后,我們來給它添加一個背景色趟庄。
繪圖方法在系統(tǒng)更新控件時被調(diào)用括细。因此它也依賴于消息循環(huán)。我們可以在消息的定義文件中找到它戚啥。
與OnClose一樣的添加方式
該方法的參數(shù) CDCHandle 是一個用于繪圖的對象但是我們并不能直接使用這個參數(shù)奋单。
簡要解析一下繪圖對象
前往CDCHandle的定義
可以看到他是一個CDCT類不同泛型參數(shù)的別名,與之對應(yīng)的還有一個CDC類
接下來前去看看CDCT的泛型參數(shù)發(fā)揮了什么作用
通過CDCT的定義 可以看出來 泛型參數(shù) t_bManaged聲明了一個CDCT對象所持有的HDC對象是否由自己管理猫十,進而在CDCT銷毀時銷毀HDC览濒。
此處的HDC是用于繪圖的由WinApi定義的句柄。
簡單來說這樣的做法是為了繪圖器持有的句柄可以被以值傳遞的方式傳遞拖云,不影響句柄贷笛。但是最終句柄只會在創(chuàng)建者銷毀時銷毀
當(dāng)然就算不理解也沒關(guān)系。知道接下來應(yīng)該這么做就行了宙项。
使用CPrintDC類乏苦,它在ATL中被定義用來從窗口句柄獲取HDC句柄
看一看CPrintDC的定義
使用CPrintDC
HBRUSH是WinApi提供的畫刷工具 CreateSolidBrush是創(chuàng)建單色畫刷,這里我們創(chuàng)建一個
GetClientRect是獲取當(dāng)前控件相對于自身的位置尤筐。也就是自己的大小汇荐。傳遞一個CRect的地址會對它賦值。
FillRect則是對對應(yīng)的區(qū)域指定畫刷盆繁。也就是繪圖操作掀淘。
動態(tài)繪圖
如果你接觸過其他平臺的界面API應(yīng)該可以直接想到下面這個注意點:
繪圖操作僅可以由系統(tǒng)調(diào)用繪圖方法是一個特定過程執(zhí)行的方法。不能主動調(diào)用
比如如下操作
通過消息添加一個鼠標(biāo)點擊的響應(yīng)事件油昂,當(dāng)鼠標(biāo)左鍵在窗口內(nèi)點擊的時候會調(diào)用OnLbuttonDown
在此處添加想要做的操作
Invalidate() 方法是可以用來更新控件的方法革娄,他會使得該控件失效并由系統(tǒng)在空閑的時間更新倾贰。
UpdateWindow() 方法可以讓失效的控件立即刷新,也就會重新繪制拦惋。如果不需要立即刷新 可以不用添加躁染。
可以看到,不論是直接使用畫刷 還是在外部調(diào)用OnPaint方法 都沒有讓界面顏色發(fā)生改變架忌。
界面變?yōu)樗{色 實際上是系統(tǒng)自動調(diào)用了OnPaint方法之后生效的。
接下來我用這段代碼演示一下這個過程
黑色方塊缺失是因為GIF錄制會有丟幀
添加的代碼在鼠標(biāo)點擊時先繪制背景然后在鼠標(biāo)周圍繪制一個黑點我衬。在鼠標(biāo)抬起時將黑點的位置取消叹放,然后再次更新繪圖。
然后補充一點功能
畫筆CPen和文字繪制
void printLine( CDCHandle dc) {
CPen pen;
pen.CreatePen(PS_SOLID, 2, RGB(255, 255, 255));
dc.SelectPen(pen.m_hPen);
CPoint start;
start.x = 10;
start.y = 10;
dc.MoveTo(start);
CPoint dest;
dest.x = mouseClickLocation.left;
dest.y = mouseClickLocation.top;
if (dest.x != 0)
{
dc.LineTo(dest);
}
}
void printText(CDCHandle dc) {
CString text;
CRect textLocation;
text = "hello world";
dc.SetTextColor(RGB(0, 0, 0));
dc.ExtTextOutA(mouseClickLocation.left, mouseClickLocation.top, ETO_OPAQUE, NULL, text, text.GetLength(), NULL);
}
通常來說如果你對 CPringDC的調(diào)用是一次獨立繪圖的話
你應(yīng)該在繪圖前使用SaveDC保存并在最后對它調(diào)用
RestoreDC(-1) 方法以恢復(fù)你使用之前的狀態(tài)
也許你注意到了 這里我已經(jīng)將OnPaint方法替換為了DoPaint方法挠羔,這牽扯到接下來要補充的一點
CDoubleBufferImpl
在進行繪圖時 如果你反復(fù)重繪井仰,會因為繪圖事會繪制一部分 顯示一部分,重繪過快就會在上一次繪制完成前就進行下一次繪制破加,進而導(dǎo)致閃爍俱恶。
此時就需要用到 CDoubleBufferImpl,它被設(shè)計成一個父類范舀,因此要使用時 需要使用多繼承合是。
先看看它的實現(xiàn)
CDoubleBufferImpl自身接收了會調(diào)用繪圖的消息。然后提供了一個接口 DoPaint
因此要這樣使用
1.添加類繼承
2.添加消息鏈接
CHAIN_MSG_MAP可以將對應(yīng)類中的消息鏈接到當(dāng)前類中
由于CDoubleBufferImpl已經(jīng)實現(xiàn)了MSG_WM_PAINT(OnPaint)锭环,因此需要注釋掉聪全。
3.實現(xiàn)DoPaint方法
也就是將你本來需要繪圖的內(nèi)容都放到這里。DC已經(jīng)在CDoubleBufferImpl中被獲取并傳遞到DoPaint了辅辩,因此直接使用就行了难礼。
看看效果
在鼠標(biāo)移動事件中如果進行復(fù)雜繪圖有可能導(dǎo)致繪圖錯誤,這可能是由于鼠標(biāo)回報率(中高端鼠標(biāo)可以達到1000HZ)過高對繪圖的調(diào)用超過GPU的渲染能力導(dǎo)致的玫锋。這種情況下需要主動降低繪圖頻率蛾茉,如限制在60幀內(nèi)之類的方案,同時對獨立窗口是分別獨立繪制的撩鹿,所以應(yīng)該盡量避免大量自定義繪圖窗口同時重繪
關(guān)于繪圖 這里就不過多延申了谦炬。
動態(tài)生成基礎(chǔ)控件
在進一步使用WTL封裝控件之前,需要先了解和使用一些MFC基本控件节沦。
通常給自定義控件添加子控件的過程放置在生命周期方法中的創(chuàng)建消息中吧寺。對于CWinodwImpl就是MSG_WM_CREATE
CButton
為FirstWindow創(chuàng)建一個CButton
注意對于一個子控件,需要為其指定父控件的句柄以確定坐標(biāo)系位置散劫。同時需要設(shè)置樣式為WS_CHILD或者WS_CHILDWINDOW(二者等價)稚机。
int OnCreate(LPCREATESTRUCT lpCreateStruct) {
//TODO:添加控件
CButton btn;
CRect btnRect;
btnRect.left = 50;
btnRect.right = 150;
btnRect.top = 50;
btnRect.bottom = 100;
btn.Create(m_hWnd, btnRect, "ClickBtnHere", WS_VISIBLE | WS_CHILD, NULL, 0U, NULL);
return 0;
}
添加響應(yīng)事件
按鈕響應(yīng)事件通過消息機制傳遞
通過COMMAND_HANDLER宏命令添加
注意紅色箭頭標(biāo)注的參數(shù),標(biāo)志著消息對應(yīng)的控件ID获搏,匹配一致才會調(diào)用赖条。
通常為了防止重復(fù)ID和增強代碼的可讀性失乾,控件的ID通過資源文件添加。
在菜單中打開資源窗口
找到你的項目.rc條目右鍵菜單點擊資源符號
選擇新建并輸入一個名稱
由于是宏命令一般用全大寫下劃線分割的命名風(fēng)格
資源符號的主要功能是讓你的控件在消息機制中通過ID匹配到對應(yīng)的句柄纬乍。動態(tài)創(chuàng)建的控件同樣能直接通過句柄尋找碱茁。所以資源主要是為了通過XML靜態(tài)創(chuàng)建的視圖使用。這一部分會在后面講控件布局的時候在做解釋仿贬。
COMMAND_HANDLER會固定的調(diào)用一個帶有四個參數(shù)和一個返回值的方法
為其添加對應(yīng)形式的響應(yīng)方法
在響應(yīng)中我通過系統(tǒng)調(diào)用方法傳遞而來的句柄找到了按鈕并改變了按鈕的標(biāo)題纽竣,并通過ID匹配對應(yīng)按鈕改變整個窗口的背景
同時也可以通過GetDlgItem方法獲取控件的句柄
注意只可獲取調(diào)用該方法的控件的子控件的句柄,也就是創(chuàng)建時 hWndParent 參數(shù)指定為該控件的控件茧泪。
CEdit
輸入框控件蜓氨,同樣通過Create方法創(chuàng)建
CEdit textField;
CRect tfRect;
tfRect.left = 200;
tfRect.right = 300;
tfRect.top = 50;
tfRect.bottom = 100;
textField.Create(m_hWnd, tfRect, nullptr, WS_VISIBLE | WS_CHILD | ES_MULTILINE | ES_AUTOVSCROLL, 0UL, 0U, NULL);
通過Style參數(shù)設(shè)置屬性。默認(rèn)狀態(tài)下為單行 ES_MULTILINE 設(shè)置為多行队伟,ES_AUTOVSCROLL設(shè)置為垂直自動滾動穴吹,默認(rèn)狀態(tài)下為單行,如果不設(shè)置滾動嗜侮,在字符填滿控件時不再接受輸入字符港令。
獲取輸入框文字內(nèi)容
SetWindowText方法和GetWindowText分別可以設(shè)置/獲取Cedit中的字符內(nèi)容。
CScrollBar
CScrollerBar 本身是作為獨立控件使用的 但是CWindow是有默認(rèn)的滾動條可以使用的 只需要在Style中設(shè)置WS_VSCROLL/WS_HSCROLL锈颗。
CScrollBar scroller;
CRect scrRect;
scrRect.left = 20;
scrRect.right = 40;
scrRect.top = 20;
scrRect.bottom = 200;
scroller.Create(m_hWnd, scrRect, "", SBS_VERT | WS_VISIBLE |WS_CHILD, NULL, 0U, NULL);
this->ShowScrollBar(0, 1);//顯示水平滾動條
this->ShowScrollBar(1, 1);//顯示垂直滾動條
SBS_VERT設(shè)置了滾動條的方向為垂直顷霹。默認(rèn)為水平
給滾動條設(shè)置屬性
SCROLLINFO info;
scroller.GetScrollInfo(&info);
scroller.SetScrollInfo(&info, false);
scroller.SetScrollRange(0, 100);
scroller.SetScrollPos(20, TRUE);
通過SCROLLINFO給滾動欄設(shè)置兩側(cè)代表的值
滾動條自身是不會隨著滾動而停在對應(yīng)的位置的,只能通過SetScrollPos方法令其停在對應(yīng)的位置击吱。
因此需要在滾動事件中記錄并設(shè)置滾動條停留在對應(yīng)的位置
添加滾動條滾動事件
先添加消息 MSG_WM_VSCROLL(OnVScroll)
然后添加響應(yīng)方法
void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar pScrollBar) {
int curPos = pScrollBar.GetScrollPos();
int destPos = curPos;
switch (nSBCode)
{
case SB_THUMBPOSITION:
destPos = nPos;
break;
}
pScrollBar.SetScrollPos(destPos);
}
nSBCode是滾動條事件的類型標(biāo)志泼返,包含各個方向的滾動,鼠標(biāo)拖動開始姨拥,停止等绅喉。先添加一個最簡單的事件
SB_THUMBPOSITION就是鼠標(biāo)拖著滑塊到達某個位置抬起后觸發(fā)
而對于其他事件則 nPos參數(shù)為0 需要自己在響應(yīng)中添加對應(yīng)的變更操作
注意不要讓目標(biāo)位置超出你設(shè)置的范圍
CComboBox
CComboBox box;
CRect lstRect;
lstRect.left = 50;
lstRect.right = 150;
lstRect.top = 120;
lstRect.bottom = 200;
box.Create(m_hWnd, lstRect, "listCtrl", WS_VISIBLE | WS_CHILD | CBS_DROPDOWNLIST, 0UL, 0U, NULL);
box.AddString("item1");
box.AddString("item2");
box.AddString("item3");
設(shè)置不同的Style會有不同的樣式 可以自己試試
關(guān)于常用的控件的例子就講這些 復(fù)雜的如樹形控件,文件選擇叫乌,菜單之類的特殊控件可以搜到其他更詳細的資料柴罐。
窗口布局
在改變窗口大小時,或者觸發(fā)控件事件時憨奸,有時會需要去改變界面布局革屠。
改變控件位置可以使用 MoveWindow()方法
方便使用 先將控件儲存為類成員變量
在滾動條滾動中改變按鈕的位置
void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar pScrollBar) {
int curPos = pScrollBar.GetScrollPos();
int destPos = curPos;
switch (nSBCode)
{
case SB_LINEUP:
destPos -= 1;
break;
case SB_LINEDOWN:
destPos += 1;
break;
case SB_THUMBTRACK:
destPos = nPos;
break;
case SB_THUMBPOSITION:
destPos = nPos;
break;
}
if (destPos < 0){destPos = 0;}
if (destPos > 100){destPos = 100;}
pScrollBar.SetScrollPos(destPos);
CRect btnRect;
btn.GetWindowRect(&btnRect);
int W = btnRect.Width();
int H = btnRect.Height();
btnRect.top = destPos;
btnRect.bottom = destPos + H;
btnRect.left = 50;
btnRect.right = 50 + W;
btn.MoveWindow(btnRect, TRUE);//第二個BOOL值參數(shù)指定是否立即重繪
}
響應(yīng)父窗口大小改變事件
通常來收移動控件位置大小主要用于在父窗口大小發(fā)生改變時合理布局
首先對一個窗口添加風(fēng)格WS_SIZEBOX 這樣就就可以拖動大小了
Window.Create(NULL, rc, "HELLO WORLD",WS_VISIBLE | WS_SYSMENU | WS_SIZEBOX,NULL,0U,NULL );
然后接收MSG_WM_SIZE消息
拖動窗口時會調(diào)用OnSize方法,對應(yīng)大小會通過size參數(shù)傳入排宰。
需要補充點窗口與坐標(biāo)系的內(nèi)容
獲取窗口位置通常有兩個方法
窗口有兩個坐標(biāo)區(qū)域似芝,一個是窗口自身的大小。還有一個是窗口的用戶區(qū)板甘,也就是去除邊框之外的用戶可操作的部分區(qū)域党瓮。
GetWindowRect() 獲取的是窗口相對于父窗口的位置 也就是整個窗口的大小位置
GetClientRect()獲取的是窗口自身用戶區(qū)的位置,永遠都是0.0為起點 其實也就是自身用戶區(qū)的大小盐类。
ScreenToClient()方法可以將獲取到的坐標(biāo)系轉(zhuǎn)換到對應(yīng)屏幕上的絕對位置
對于子控件來說通常通過GetClientRect獲取父控件的區(qū)域去布局寞奸, 通過GetWindowRect獲取其他處于同一個父控件之下的子控件的位置呛谜。此時獲取到的位置都是相對于父控件的用戶區(qū)的相對坐標(biāo)。
而如果要將父子控件都轉(zhuǎn)換到對應(yīng)的相同坐標(biāo)系
則需要先讓需要轉(zhuǎn)換坐標(biāo)系的控件自身調(diào)用GetWindowRect然后統(tǒng)一通過父控件調(diào)用ScreenToClient去轉(zhuǎn)換坐標(biāo)系 此時獲取到的就是控件相對于屏幕的絕對位置枪萄。
關(guān)于這一點的應(yīng)用隐岛,會在結(jié)束基礎(chǔ)內(nèi)容后補充的綜合布局實踐中使用到。當(dāng)然自己創(chuàng)建幾個控件試一試?yán)斫馄饋頃忧逦?/p>
接下來在OnSize中讓scroller貼緊左邊
void OnSize(UINT nType, CSize size) {
CRect clientRect;
GetClientRect(&clientRect);
CRect scrRect;
scrRect.top = clientRect.top;
scrRect.left = clientRect.left;
scrRect.right = scrRect.left + 20;
scrRect.bottom = clientRect.bottom;
scroller.MoveWindow(scrRect);
}
使用資源文件可視化布局
在開發(fā)中需要界面有相當(dāng)繁復(fù)但是固定的控件瓷翻,全部使用動態(tài)創(chuàng)建的方式代碼量較大聚凹,因此也可以通過資源文件拖控件去實現(xiàn)。只需要通過資源直接拖動控件到需要的位置后使用時通過資源ID獲取就可以了齐帚。節(jié)省了大量的布局代碼妒牙。
以對話框為例實踐一下。打開資源視圖童谒,右鍵選擇添加資源
對應(yīng)的類型比較多,功能各樣沪羔,可以直接百度了解饥伊。這里直接使用一個最基本的Dialog 對話框。不用點開加號蔫饰。
紅圈內(nèi)的ID就是對話框的資源ID琅豆,為了可讀性,改成一個合適的名稱篓吁。IDD_DIALOG_FIRST茫因。
也可以在對話框右鍵屬性選項中打開控件的屬性面板去設(shè)置
屬性面板有相當(dāng)多的選項可以設(shè)置≌燃簦可以自己點點了解下冻押。
基本的對話框給了兩個默認(rèn)的按鈕
對于獨立對話框,它是一個容器類型盛嘿,需要自己創(chuàng)建一個類去綁定和操作洛巢。因此其資源ID不可與其他容器類型的資源ID重復(fù),而對于容器內(nèi)的子控件次兆。只要在同一容器內(nèi)不重復(fù)即可稿茉。
此時使用ATL對話框類 CDialogImpl(與CWindow類似);
在對應(yīng)的對話框類中添加
enum
{
IDD = IDD_DIALOG_FIRST//資源ID
};
指定資源綁定。
#pragma once
#include "stdafx.h"
#include "resource.h"
class FirstDialog :public CDialogImpl<FirstDialog>
{
public:
enum
{
IDD = IDD_DIALOG_FIRST
};
BEGIN_MSG_MAP(FirstDialog)
END_MSG_MAP()
};
這樣我們使用對應(yīng)的類芥炭。就會調(diào)起被綁定的資源窗口
先回到資源文件打開工具箱給窗口添加控件
可以看到控件很多漓库,需要的話根據(jù)名字去官方文檔或百度就能找到詳細的用法解析。
添加一個StaticText控件 并通過屬性面板通過Caption屬性設(shè)置文本
使用剛才創(chuàng)建的類就可以獲取這個對話框园蝠。
使用DoModal方法模態(tài)彈出對話框
與前面一樣 關(guān)閉與按鈕的事件都需要自行處理渺蒿,不過使用資源文件時可以通過資源文件直接添加對應(yīng)的交互事件,會自動在對應(yīng)的文件中生成消息接收和對應(yīng)方法的代碼彪薛,方法實現(xiàn)會生成在.cpp中添加蘸嘶,所以先自己創(chuàng)建一個對應(yīng)的.CPP文件
前往控件的屬性面板點擊控件事件對點擊事件添加響應(yīng)
就會自動生成對應(yīng)的代碼
使用EndDialog方法關(guān)閉對話框良瞧。
可以通過GetDlgItem()獲取控件。
BOOL OnInitDialog(CWindow wndFocus, LPARAM lInitParam) {
GetDlgItem(IDOK).SetWindowText("BTNOK");
return 0;
}
LRESULT FirstDialog::OnBnClickedOk(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
// TODO: 在此添加控件通知處理程序代碼
EndDialog(0);
return 0;
}
制作自定義控件
先介紹一個WTL增強數(shù)據(jù)交換工具 DDX
DDX是一套類似于MSG_MAP的宏命令训唱,其實質(zhì)就是簡化和統(tǒng)一窗口與數(shù)據(jù)之間相互傳值的調(diào)用代碼褥蚯。
使用起來比較簡單,舉個例子况增。
首先在需要使用DDX的類中繼承CWinDataExchange父類
然后使用DDX命令 將要綁定的控件ID和對應(yīng)類型的變量進行綁定
當(dāng)然赞庶,要創(chuàng)建ID_TF_FIRST這個資源ID并賦值給之前創(chuàng)建的CEdit;
之后在需要交換數(shù)據(jù)的地方使用
DoDataExchange()方法就可以將數(shù)據(jù)從變量傳遞到控件,或DoDataExchange(TRUE)從控件傳遞到變量了
在自定義控件中可以定義自己的DDX宏澳骤,這樣可以方便的統(tǒng)一輸入和輸出歧强,便于代碼閱讀。
使用資源文件設(shè)置自定義視圖
使用純代碼創(chuàng)建自定義視圖很簡單为肮,繼承就行了摊册。
如果希望不需要通過代碼創(chuàng)建只要拖控件的話,則要使用Custom Control控件颊艳,并在使用前注冊茅特,并使用自定義的窗口類關(guān)聯(lián)。
首先拖一個CustomControl 類型的資源到對應(yīng)的視圖中 棋枕,然后指定一個資源ID和自定義一個Class名稱白修。
然后回到Main函數(shù)中注冊這個Class
創(chuàng)建一個自定義窗口類并關(guān)聯(lián)資源
這樣,該資源代表的控件就是你自定義的類對應(yīng)的控件了重斑,如果需要移動到其他窗口兵睛,可以直接在資源文件中操作,只要在使用時將對象與資源做關(guān)聯(lián)就行了窥浪。
給自定義的資源視圖添加控件
使用資源自定義視圖則不需要調(diào)用Create方法祖很,也就無法在這里創(chuàng)建子空間了,但是類與資源綁定時會調(diào)用SubclassWindow漾脂。就可以在這里重寫該方法添加創(chuàng)建子控件的操作突琳。
class FirstCustomItem :public CWindowImpl<FirstCustomItem,CWindow>
。符相。拆融。。啊终。
CButton btnL;
CButton btnR;
BOOL SubclassWindow(_In_ HWND hWnd) {
//先調(diào)用父類的SubclassWindow
BOOL result = CWindowImpl::SubclassWindow(hWnd);
if (result)
{
CRect lRect;
GetClientRect(&lRect);
lRect.right = lRect.right / 2;
CRect rRect;
GetClientRect(&rRect);
rRect.left = rRect.right / 2;
btnL.Create(m_hWnd, lRect, "0", WS_VISIBLE | WS_CHILD, 0UL, 0U, NULL);
btnR.Create(m_hWnd, rRect, "0", WS_VISIBLE | WS_CHILD, 0UL, 1U, NULL);
}
return result;
}
};
镜豹。。蓝牲。趟脂。。
添加自己的DDX
DDX宏本質(zhì)上也只是方法調(diào)用例衍,實際與“給自己的控件添加輸入輸出接口并手動調(diào)用”是一樣的昔期。其意義在于可以對一類輸入輸出統(tǒng)一處理統(tǒng)一聲明已卸。因為C++代碼往往行數(shù)是比較多的。DDX實質(zhì)上只是讓程序更易讀硼一。
添加一個自己的類繼承CWinDataExchange
template <class T>
class FirstDialogDDX :
public CWinDataExchange<T>
{
#define DDX_FD_TEXT(nID, var) \
if(nCtlID == (UINT)-1 || nCtlID == nID) \
{ \
if(!ddxFdText(nID, var, sizeof(var), bSaveAndValidate)) \
return FALSE; \
}
void ddxFdText(UINT nID, CString& nValue, BOOL bSave)
{
//獲取句柄
T* pT = static_cast<T*>(this);
HWND hWndCtrl = pT->GetDlgItem(nID);
ATLASSERT(hWndCtrl != NULL);
//發(fā)送操作消息
if (bSave)
{
//寫入
}
else
{
//取出
}
}
};
如果你對前面分析其他組件的過程有所記憶這里就不再去DDX中分析了累澡,可以自己去看一看。
原理非常簡單般贼。
模仿框架自帶的DDX添加一個新的宏定義愧哟。和對應(yīng)的調(diào)用方法。
bSave就是調(diào)用DoDataExchange時填入的參數(shù)哼蛆,根據(jù)它判斷操作方向蕊梧。
然后回到自定義的類中添加自定義的消息和消息處理
接下來回到自定義的類中添加消息接收和處理方法
#define WM_WRITE WM_USER + 1
#define WM_HANDLE_POINTER WM_USER + 3
#define WM_READ WM_USER + 2
class FirstCustomItem :public CWindowImpl<FirstCustomItem,CWindow>
{
public:
BEGIN_MSG_MAP(OnInitDialog)
MSG_WM_PAINT(OnPaint)
MSG_WM_SIZE(OnSize);
MESSAGE_HANDLER(WM_READ, notifactionRead)
MESSAGE_HANDLER(WM_WRITE, notifactionWrite)
END_MSG_MAP()
添加消息接收和對應(yīng)的處理方法
LRESULT notifactionRead(...) {
return readData();
}
LRESULT notifactionWrite( UINT , LPARAM lparm, WPARAM rparm, BOOL& /*bHandled*/) {
writeData(lparm);
return 0;
}
BOOL readData() {
CString strL;
btnL.GetWindowTextA(strL);
CString strR;
btnR.GetWindowTextA(strR);
if (strL == "1" && strR == "1")
{
return TRUE;
}
return FALSE;
}
void writeData(BOOL state) {
if (state)
{
btnL.SetWindowTextA("1");
btnR.SetWindowTextA("1");
}
else
{
btnL.SetWindowTextA("0");
btnR.SetWindowTextA("0");
}
}
順便去自定義控件給按鈕添加一個事件
LRESULT OnLClick(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
CString btnStr;
btnL.GetWindowTextA(btnStr);
if (btnStr == "0")
{
btnL.SetWindowTextA("1");
}
else
{
btnL.SetWindowTextA("0");
}
return 0;
}
LRESULT OnRClick(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
CString btnStr;
btnR.GetWindowTextA(btnStr);
if (btnStr == "0")
{
btnR.SetWindowTextA("1");
}
else
{
btnR.SetWindowTextA("0");
}
return 0;
}
好了 接下來就可以通過DDX將這個自定義控件與一個BOOL值之間做交互了
你應(yīng)該看出來了 。這個控件 就是一個有兩個可點擊按鈕并能通過DDX輸出按鈕上數(shù)字的&運算后取得的BOOL值的復(fù)合控件腮介。
回到使用它的地方 用DDX做綁定
BEGIN_DDX_MAP(FirstDialog)
DDX_FD_BOOL(IDC_CUSTOM_FIRST,m_SocketState)
END_DDX_MAP()
BOOL m_SocketState = 1;
給對話框的兩個按鈕添加事件用來重置狀態(tài)和進行DDX操作
LRESULT FirstDialog::OnBnClickedOk(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
// TODO: 在此添加控件通知處理程序代碼
m_SocketState = 0;
DoDataExchange(TRUE);
return 0;
}
LRESULT FirstDialog::OnBnClickedCancel(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
// TODO: 在此添加控件通知處理程序代碼
DoDataExchange(FALSE);
if (m_SocketState)
{
(GetDlgItem(IDC_STATIC)).SetWindowTextA("TRUE");
}
else
{
(GetDlgItem(IDC_STATIC)).SetWindowTextA("FALSE");
}
return 0;
}
看一看做了個什么肥矢。