为什么是WTL
项目是toB的控制端软件,WTL程序包小,仅有一个EXE文件,对不同版本操作系统兼容性好,B端客户还用XP的不在少数,发布与使用的便捷性强。
为什么写这篇文章
WTL是基于模板对窗口封装,接近系统底层,灵活度高,只是由于资料匮乏开发上手比较困难,关于WTL的渊源我就不介绍了,重要的是,如果你在网络上搜索WTL的开资料,只能找到各种各样的广告以及WTL for MFC Programmers这篇文章的翻译版本,如果你没有MFC基础,看起来将有一定的困难,而像我这样没有C++基础,则更加头疼。作为IOS我有还行的C语言基础,Objective-C也使用了许多与C++类似的语法因此我大概用了一周时间学习C++和一周时间熟悉WTL最基本的框架使用以及几天时间了解公司项目框架构成后,上手并完成了几个界面模块的拧螺丝工作。这篇文章当然不足以让你成为WTL项目负责人,但是应该能帮助你顺利的上手。
这篇文章都要讲什么
利用WTL进行Win窗口界面程序开发主要是UI部分内容,主要有以下几个部分
- 环境配置
- 创建第一个窗口
- 自定义绘图
- 基础控件:CButton CEdit CScrollerBar
- 动态及使用资源文件页面布局
- 制作自定义控件
- WTL扩展增强-DDX
你需要提前准备什么?
需要先学好C++吗?需要先看一看MFC吗?都不需要,但是你至少需要:
1.有C语言基础。
2.理解面向对象的程序开发,不论是C++还是JAVA,OC或者SWIFT等其他面向对象的开发语言,理解OOP即可。
3.如果你完全没有C++基础,也可以照着我的代码一步步做,关于C++面向对象的特性例如多继承,模板编程等,我也会在到的地方做出简单直观的解释,C++开发的其他资料比较详实如果有理解不了的内容,百度一下。
4.如果你有一些客户端开发基础IOS/安卓,会有一些帮助。
5.如果你是MFC开发人员可以直接看WTL for MFC Programmers。
更重要的资料
作为一篇以引入为目的的教程,我不会过多的介绍Windows系统的功能以及所有各种复杂的控件
如果你需要相关的资料 微软的官方文档是最佳参考资料
https://docs.microsoft.com/en-us/cpp/mfc/reference/mfc-classes?view=vs-2019
WTL基于ATL,ATL中的类大部分与MFC通用,因此官方的MFC 类文档是参考和学习价值极高的 某度甚至检索到一大堆广告 都不会把你引向官方文档
这里是WTL的下载地址
https://sourceforge.net/projects/wtl/
WTL项目中也带有一些例程,可以参考。
正式开始教程
项目搭建
1.下载安装VS。我使用的是Visual Studio2015 vs的版本对WTL影响不大,默认配置即可。
2.下载WTL,并取出include文件夹,这就是项目需要的WTL的全部文件
3.在VS中创建一个C++空项目
4.在项目中添加对WTL的引用
5.创建Main.cpp文件和stdafx.h文件。并分别写入以下内容
//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是将对应文件的代码复制到当前文件中因此要注意如果你不知道怎么回事,就不要改变引用顺序 ,包括其中插入的宏命令位置。
Main.CPP是程序入口。CAppModule _Module;是保存主线程ID和消息循环的实例。
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这个循环是主消息循环,控制着应用的生命周期。Win程序的运行依赖于消息机制。边用便了解即可。此处只要知道走入这一行 程序便开始在消息循环中执行,存活。因此要在它之前插入操作入口。
程序的基本运行环境到这里就搭建完成了。WTL其实是一套对WinAPI界面描述的封装,可以非常轻松的扩展或引入到其他项目中。
开始创建自己的窗口
项目主入口完成后就可以开始通过WTL创建自己的窗口了。在此之前,我需要解释几个概念,熟悉C++的话可以跳过这部分。
一.C++中的变量。
如果你没有C++基础 那么你需要重新理清C++中的变量和对象关系。
对于变量 它在创建的时候在栈中分配内存。C++中的对象可与基本类型一样直接创建,创建时即分配内存,离开作用域时出栈释放。也可以在堆中创建即创建指针指向new的对象
void founction(){
yourClass obj1;
yourClass *obj2 = new class;
}//obj1释放,obj2指针释放 ,*obj2对象内存溢出
二.模板。
WTL是基于模板开发,模板是实现泛型编程的基础。可以将模板看成一种描述方式,用于描述一个类型的对象遵循模板类型对象的类型声明。
举个例子
//一个单纯的类A
class A {
void functionA(){
}
}
//一个单纯的类B
class B {
void functionB(){
}
}
//一个单纯的类P
class P {
void functionP(){
}
}
//准备一个遵循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的类
模板可以用于描述一种继承关系,也就可以在使用的时候再去定义一个对象继承于谁
//自定义一个class F
template<class T /* = P */>//声明模板同时注释其类型
class F :public T{
void functionF(){
//调用自己继承而来的functionP 继承于F的类必须指定其模板参数为P及P的子类
functionP()
}
//或用于方法
//创建一个R类型的对象并调用其founctionP方法
template <class R /* = P */>
void founctionF2(){
R objR;
objR.founctionP();
}
void functionF3(){
//调用时必须使用P或P的子类
founctionF2<P>();
}
}
这么做则相当于我们创建一个 F 类并且指定其继承于 T 类,并且 T 必须是一个 P 的子类因为我们在 F 中直接使用了 P 的方法。
刨除元编程来看,模板点像多态,但与多态相比是逻辑逆序的,多态是“我的子类继承于我便拥有了我的接口和属性”而模板是“使用我创建的类必须包含我调用的接口和属性,从而成为符合本模板逻辑的类”。强调逻辑复用性而非结构。
WTL中模板主要用于声明自定义控件的继承类型 。
三. 虚函数
对于一个类中的函数使用virtual关键字 则这个函数是一个虚函数
virtual void founction(){};//虚函数
virtual void founction() = 0;//纯虚函数
虚函数可以视作是一种接口声明。
纯虚函数更多是一种描述性用途,它规定了包含纯虚函数的类为基类 ,不可直接使用只能使用其子类。
回到WTL
在理解以上三点后 就可以开始了解WTL界面搭建的基本工具了。
在Win程序中 一切界面元素皆是CWindow。在WTL中最基本的界面类是CWindowImpl。
在正式开始使用前先简单了解一下CWindowImpl
它有三个模板参数 其中 T自定义的类 TBase 是WinAPI中的窗口类 TWinTraits 是设置模板,可以不使用 先无视它。
从它的定义可以看出来两个泛型参数 TBase用于类型传递,T则用于本地调用,让CWindowImpl能够调用到你自定义的类的一些类方法,如果你重写了这些方法。防止子类重写这些方法后调用的仍然是父类的这些方法。
接着向上看
这里给与了Tbase以及TWinTraits 初始值因此如果你不写这两个参数,才不会报错。这个类主要是设置窗口的一些默认属性。可以不用关心他。接下来它又将TBase传递给了CWindowImplRoot。接着向上看。
这样我们就找到了最终的目的地 传递到这里的TBase指定了自定义控件的实际基类。
并且它同时继承于CMessageMap马上就会用到。我们先看一看它。
在CMessageMap中 定义了一个纯虚函数ProcessWindowMessage。也就是这里让我们必须使用CWindowImpl的子类并且必须实现ProcessWindowMessage方法。CMessageMap贴心的提供了一整套宏命令用来实现消息和消息转发。后面用到时会讲。
整理一下:
继承于CWindowImpl的类需要至少传三个参数<T,TBase,TWinTraits >其中T用于CWindowImpl调用。TBase必须是CWindow及它的子类,有默认值CWinodw,实际用途为传递到最后给CWindowImplRoot并被它继承为该自定义类的基类。TWinTraits用于设置样式有默认值。
定义第一个自定义的窗口
此时创建了一个FirstWindow 类,它是一个基本的WTL对象。继承于CWindow。
由于CMessageMap中的纯虚函数存在,因此必须添加
宏命令对其做出实现。
BEGIN_MSG_MAP(FirstWindow)
END_MSG_MAP()
这些是自动创建的构造函数和析构函数。如无需要,可以删掉。
FirstWindow();
~FirstWindow();
FirstWindow::FirstWindow()
{
}
FirstWindow::~FirstWindow()
{
}
练习项目中可以将代码全部写在.h文件。
然后使用它
在WinMain函数中,Moudle创建后,消息循环开始执行前创建并显示这个窗口
其中CRect是描述位置大小的对象,可以设置上下左右边相对于父控件的位置。
创建了Window对象后需要调用Create方法创建窗口
该方法有以下几个参数
In_opt_ HWND hWndParent, //父控件的句柄
_In_ _U_RECT rect = NULL,//指定位置大小
_In_opt_z_ LPCTSTR szWindowName = NULL,//窗口名称,会显示在左上角系统栏。默认样式可见
_In_ DWORD dwStyle = 0,//窗口风格类型,属性设置
_In_ DWORD dwExStyle = 0,//窗口扩展风格
_In_ _U_MENUorID MenuOrID = 0U,//资源ID,可以理解为该对象的数字标记,后面使用资源文件的时候会再次用到
_In_opt_ LPVOID lpCreateParam = NULL//16位机遗留参数。现在用不着
这里我简单解释一下句柄
句柄是是一个结构体表示一个Window资源,也就相当于它的索引,通过句柄可以找到这个资源。也就可以通过句柄首发消息。CWindow对句柄进行了重载。使得CWindow的赋值操作实际只是让另一个CWindow对象的句柄指针指向目标对象的句柄。暂时了解一下就行了。
在Create方法中指定句柄通常用于设置控件位置,也就是说 rect参数设置的控件位置是相对于 hWndParent 的。而指定hWndParent为空的时候,窗口的位置是相对于屏幕的,左上角为0.0点,向右/向下为正轴。
此时我们添加的代码创建了一个从屏幕左上角X/Y轴均为以10为起点到500为终点的FirstWindow类型的窗口。执行一下看看
看起来左侧边距似乎是要比顶部多一些,这是因为系统窗口的样式是有边框的,分布在左右下侧,在Win10中为透明样式,大约8像素
窗口缺了一个关闭按钮
可以在创建时dwStyle属性中设置该参数。可以使用 | 按位操作同时设置多个 。然后可以可以使用X按钮关闭窗口。
Window.Create(NULL, rc, "HELLO WORLD",WS_VISIBLE | WS_SYSMENU,NULL,0U,NULL );
但是这并不能关闭程序!
点击X只是隐藏了窗口,程序仍然在主方法中循环,没有任何变量被释放。
我们要在关闭窗口的时候关闭程序,就要用到消息机制。
在WTL窗口中的生命周期方法,界面交互都有对应的消息分发到对应的类中。这些消息的接收转发,都被封装为一系列的宏命令,将其插入到BEGIN_MSG_MAP和END_MSG_MAP之间并实现对应的方法即可接收消息
PostQuitMessage(0) 是WinApi方法,可以结束程序。
MSG_WM_CLOSE(OnClose) 声明了由OnClose方法接收关闭窗口的消息。
可以更改方法名,但是一般使用原头文件中的方法名以便阅读
同理可以推得,如窗口创建,移动,绘制鼠标时间等大量的生命周期/操作相应等系统管理的事件都可以通过这个机制来操作
可以直接去对应的头文件中找到消息和消息参数定义。
但是要注意 调用该方法结束应用前必须删除掉所有你创建的窗口
DestroyWindow()可以删除当前窗口,也可以填入一个窗口的句柄用于删除指定窗口。
绘图
由于WinApi诞生之时还没有Material Design这样美观的视觉表达规范,其系统控件样式相当的匮乏且充满工程师设计风格,因此大多数时候,控件都需要实现自定义绘图,即使只是简单的设置背景颜色。
完成了上面最简单的视窗控件后,我们来给它添加一个背景色。
绘图方法在系统更新控件时被调用。因此它也依赖于消息循环。我们可以在消息的定义文件中找到它。
与OnClose一样的添加方式
该方法的参数 CDCHandle 是一个用于绘图的对象但是我们并不能直接使用这个参数。
简要解析一下绘图对象
前往CDCHandle的定义
可以看到他是一个CDCT类不同泛型参数的别名,与之对应的还有一个CDC类
接下来前去看看CDCT的泛型参数发挥了什么作用
通过CDCT的定义 可以看出来 泛型参数 t_bManaged声明了一个CDCT对象所持有的HDC对象是否由自己管理,进而在CDCT销毁时销毁HDC。
此处的HDC是用于绘图的由WinApi定义的句柄。
简单来说这样的做法是为了绘图器持有的句柄可以被以值传递的方式传递,不影响句柄。但是最终句柄只会在创建者销毁时销毁
当然就算不理解也没关系。知道接下来应该这么做就行了。
使用CPrintDC类,它在ATL中被定义用来从窗口句柄获取HDC句柄
看一看CPrintDC的定义
使用CPrintDC
HBRUSH是WinApi提供的画刷工具 CreateSolidBrush是创建单色画刷,这里我们创建一个
GetClientRect是获取当前控件相对于自身的位置。也就是自己的大小。传递一个CRect的地址会对它赋值。
FillRect则是对对应的区域指定画刷。也就是绘图操作。
动态绘图
如果你接触过其他平台的界面API应该可以直接想到下面这个注意点:
绘图操作仅可以由系统调用绘图方法是一个特定过程执行的方法。不能主动调用
比如如下操作
通过消息添加一个鼠标点击的响应事件,当鼠标左键在窗口内点击的时候会调用OnLbuttonDown
在此处添加想要做的操作
Invalidate() 方法是可以用来更新控件的方法,他会使得该控件失效并由系统在空闲的时间更新。
UpdateWindow() 方法可以让失效的控件立即刷新,也就会重新绘制。如果不需要立即刷新 可以不用添加。
可以看到,不论是直接使用画刷 还是在外部调用OnPaint方法 都没有让界面颜色发生改变。
界面变为蓝色 实际上是系统自动调用了OnPaint方法之后生效的。
接下来我用这段代码演示一下这个过程
黑色方块缺失是因为GIF录制会有丢帧
添加的代码在鼠标点击时先绘制背景然后在鼠标周围绘制一个黑点。在鼠标抬起时将黑点的位置取消,然后再次更新绘图。
然后补充一点功能
画笔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的调用是一次独立绘图的话
你应该在绘图前使用SaveDC保存并在最后对它调用
RestoreDC(-1) 方法以恢复你使用之前的状态
也许你注意到了 这里我已经将OnPaint方法替换为了DoPaint方法,这牵扯到接下来要补充的一点
CDoubleBufferImpl
在进行绘图时 如果你反复重绘,会因为绘图事会绘制一部分 显示一部分,重绘过快就会在上一次绘制完成前就进行下一次绘制,进而导致闪烁。
此时就需要用到 CDoubleBufferImpl,它被设计成一个父类,因此要使用时 需要使用多继承。
先看看它的实现
CDoubleBufferImpl自身接收了会调用绘图的消息。然后提供了一个接口 DoPaint
因此要这样使用
1.添加类继承
2.添加消息链接
CHAIN_MSG_MAP可以将对应类中的消息链接到当前类中
由于CDoubleBufferImpl已经实现了MSG_WM_PAINT(OnPaint),因此需要注释掉。
3.实现DoPaint方法
也就是将你本来需要绘图的内容都放到这里。DC已经在CDoubleBufferImpl中被获取并传递到DoPaint了,因此直接使用就行了。
看看效果
在鼠标移动事件中如果进行复杂绘图有可能导致绘图错误,这可能是由于鼠标回报率(中高端鼠标可以达到1000HZ)过高对绘图的调用超过GPU的渲染能力导致的。这种情况下需要主动降低绘图频率,如限制在60帧内之类的方案,同时对独立窗口是分别独立绘制的,所以应该尽量避免大量自定义绘图窗口同时重绘
关于绘图 这里就不过多延申了。
动态生成基础控件
在进一步使用WTL封装控件之前,需要先了解和使用一些MFC基本控件。
通常给自定义控件添加子控件的过程放置在生命周期方法中的创建消息中。对于CWinodwImpl就是MSG_WM_CREATE
CButton
为FirstWindow创建一个CButton
注意对于一个子控件,需要为其指定父控件的句柄以确定坐标系位置。同时需要设置样式为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;
}
添加响应事件
按钮响应事件通过消息机制传递
通过COMMAND_HANDLER宏命令添加
注意红色箭头标注的参数,标志着消息对应的控件ID,匹配一致才会调用。
通常为了防止重复ID和增强代码的可读性,控件的ID通过资源文件添加。
在菜单中打开资源窗口
找到你的项目.rc条目右键菜单点击资源符号
选择新建并输入一个名称
由于是宏命令一般用全大写下划线分割的命名风格
资源符号的主要功能是让你的控件在消息机制中通过ID匹配到对应的句柄。动态创建的控件同样能直接通过句柄寻找。所以资源主要是为了通过XML静态创建的视图使用。这一部分会在后面讲控件布局的时候在做解释。
COMMAND_HANDLER会固定的调用一个带有四个参数和一个返回值的方法
为其添加对应形式的响应方法
在响应中我通过系统调用方法传递而来的句柄找到了按钮并改变了按钮的标题,并通过ID匹配对应按钮改变整个窗口的背景
同时也可以通过GetDlgItem方法获取控件的句柄
注意只可获取调用该方法的控件的子控件的句柄,也就是创建时 hWndParent 参数指定为该控件的控件。
CEdit
输入框控件,同样通过Create方法创建
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参数设置属性。默认状态下为单行 ES_MULTILINE 设置为多行,ES_AUTOVSCROLL设置为垂直自动滚动,默认状态下为单行,如果不设置滚动,在字符填满控件时不再接受输入字符。
获取输入框文字内容
SetWindowText方法和GetWindowText分别可以设置/获取Cedit中的字符内容。
CScrollBar
CScrollerBar 本身是作为独立控件使用的 但是CWindow是有默认的滚动条可以使用的 只需要在Style中设置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设置了滚动条的方向为垂直。默认为水平
给滚动条设置属性
SCROLLINFO info;
scroller.GetScrollInfo(&info);
scroller.SetScrollInfo(&info, false);
scroller.SetScrollRange(0, 100);
scroller.SetScrollPos(20, TRUE);
通过SCROLLINFO给滚动栏设置两侧代表的值
滚动条自身是不会随着滚动而停在对应的位置的,只能通过SetScrollPos方法令其停在对应的位置。
因此需要在滚动事件中记录并设置滚动条停留在对应的位置
添加滚动条滚动事件
先添加消息 MSG_WM_VSCROLL(OnVScroll)
然后添加响应方法
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是滚动条事件的类型标志,包含各个方向的滚动,鼠标拖动开始,停止等。先添加一个最简单的事件
SB_THUMBPOSITION就是鼠标拖着滑块到达某个位置抬起后触发
而对于其他事件则 nPos参数为0 需要自己在响应中添加对应的变更操作
注意不要让目标位置超出你设置的范围
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");
设置不同的Style会有不同的样式 可以自己试试
关于常用的控件的例子就讲这些 复杂的如树形控件,文件选择,菜单之类的特殊控件可以搜到其他更详细的资料。
窗口布局
在改变窗口大小时,或者触发控件事件时,有时会需要去改变界面布局。
改变控件位置可以使用 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值参数指定是否立即重绘
}
响应父窗口大小改变事件
通常来收移动控件位置大小主要用于在父窗口大小发生改变时合理布局
首先对一个窗口添加风格WS_SIZEBOX 这样就就可以拖动大小了
Window.Create(NULL, rc, "HELLO WORLD",WS_VISIBLE | WS_SYSMENU | WS_SIZEBOX,NULL,0U,NULL );
然后接收MSG_WM_SIZE消息
拖动窗口时会调用OnSize方法,对应大小会通过size参数传入。
需要补充点窗口与坐标系的内容
获取窗口位置通常有两个方法
窗口有两个坐标区域,一个是窗口自身的大小。还有一个是窗口的用户区,也就是去除边框之外的用户可操作的部分区域。
GetWindowRect() 获取的是窗口相对于父窗口的位置 也就是整个窗口的大小位置
GetClientRect()获取的是窗口自身用户区的位置,永远都是0.0为起点 其实也就是自身用户区的大小。
ScreenToClient()方法可以将获取到的坐标系转换到对应屏幕上的绝对位置
对于子控件来说通常通过GetClientRect获取父控件的区域去布局, 通过GetWindowRect获取其他处于同一个父控件之下的子控件的位置。此时获取到的位置都是相对于父控件的用户区的相对坐标。
而如果要将父子控件都转换到对应的相同坐标系
则需要先让需要转换坐标系的控件自身调用GetWindowRect然后统一通过父控件调用ScreenToClient去转换坐标系 此时获取到的就是控件相对于屏幕的绝对位置。
关于这一点的应用,会在结束基础内容后补充的综合布局实践中使用到。当然自己创建几个控件试一试理解起来会更加清晰。
接下来在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);
}
使用资源文件可视化布局
在开发中需要界面有相当繁复但是固定的控件,全部使用动态创建的方式代码量较大,因此也可以通过资源文件拖控件去实现。只需要通过资源直接拖动控件到需要的位置后使用时通过资源ID获取就可以了。节省了大量的布局代码。
以对话框为例实践一下。打开资源视图,右键选择添加资源
对应的类型比较多,功能各样,可以直接百度了解。这里直接使用一个最基本的Dialog 对话框。不用点开加号。
红圈内的ID就是对话框的资源ID,为了可读性,改成一个合适的名称。IDD_DIALOG_FIRST。
也可以在对话框右键属性选项中打开控件的属性面板去设置
属性面板有相当多的选项可以设置。可以自己点点了解下。
基本的对话框给了两个默认的按钮
对于独立对话框,它是一个容器类型,需要自己创建一个类去绑定和操作。因此其资源ID不可与其他容器类型的资源ID重复,而对于容器内的子控件。只要在同一容器内不重复即可。
此时使用ATL对话框类 CDialogImpl(与CWindow类似);
在对应的对话框类中添加
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()
};
这样我们使用对应的类。就会调起被绑定的资源窗口
先回到资源文件打开工具箱给窗口添加控件
可以看到控件很多,需要的话根据名字去官方文档或百度就能找到详细的用法解析。
添加一个StaticText控件 并通过属性面板通过Caption属性设置文本
使用刚才创建的类就可以获取这个对话框。
使用DoModal方法模态弹出对话框
与前面一样 关闭与按钮的事件都需要自行处理,不过使用资源文件时可以通过资源文件直接添加对应的交互事件,会自动在对应的文件中生成消息接收和对应方法的代码,方法实现会生成在.cpp中添加,所以先自己创建一个对应的.CPP文件
前往控件的属性面板点击控件事件对点击事件添加响应
就会自动生成对应的代码
使用EndDialog方法关闭对话框。
可以通过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增强数据交换工具 DDX
DDX是一套类似于MSG_MAP的宏命令,其实质就是简化和统一窗口与数据之间相互传值的调用代码。
使用起来比较简单,举个例子。
首先在需要使用DDX的类中继承CWinDataExchange父类
然后使用DDX命令 将要绑定的控件ID和对应类型的变量进行绑定
当然,要创建ID_TF_FIRST这个资源ID并赋值给之前创建的CEdit;
之后在需要交换数据的地方使用
DoDataExchange()方法就可以将数据从变量传递到控件,或DoDataExchange(TRUE)从控件传递到变量了
在自定义控件中可以定义自己的DDX宏,这样可以方便的统一输入和输出,便于代码阅读。
使用资源文件设置自定义视图
使用纯代码创建自定义视图很简单,继承就行了。
如果希望不需要通过代码创建只要拖控件的话,则要使用Custom Control控件,并在使用前注册,并使用自定义的窗口类关联。
首先拖一个CustomControl 类型的资源到对应的视图中 ,然后指定一个资源ID和自定义一个Class名称。
然后回到Main函数中注册这个Class
创建一个自定义窗口类并关联资源
这样,该资源代表的控件就是你自定义的类对应的控件了,如果需要移动到其他窗口,可以直接在资源文件中操作,只要在使用时将对象与资源做关联就行了。
给自定义的资源视图添加控件
使用资源自定义视图则不需要调用Create方法,也就无法在这里创建子空间了,但是类与资源绑定时会调用SubclassWindow。就可以在这里重写该方法添加创建子控件的操作。
class FirstCustomItem :public CWindowImpl<FirstCustomItem,CWindow>
。。。。。
CButton btnL;
CButton btnR;
BOOL SubclassWindow(_In_ HWND hWnd) {
//先调用父类的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宏本质上也只是方法调用,实际与“给自己的控件添加输入输出接口并手动调用”是一样的。其意义在于可以对一类输入输出统一处理统一声明。因为C++代码往往行数是比较多的。DDX实质上只是让程序更易读。
添加一个自己的类继承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);
//发送操作消息
if (bSave)
{
//写入
}
else
{
//取出
}
}
};
如果你对前面分析其他组件的过程有所记忆这里就不再去DDX中分析了,可以自己去看一看。
原理非常简单。
模仿框架自带的DDX添加一个新的宏定义。和对应的调用方法。
bSave就是调用DoDataExchange时填入的参数,根据它判断操作方向。
然后回到自定义的类中添加自定义的消息和消息处理
接下来回到自定义的类中添加消息接收和处理方法
#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()
添加消息接收和对应的处理方法
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值之间做交互了
你应该看出来了 。这个控件 就是一个有两个可点击按钮并能通过DDX输出按钮上数字的&运算后取得的BOOL值的复合控件。
回到使用它的地方 用DDX做绑定
BEGIN_DDX_MAP(FirstDialog)
DDX_FD_BOOL(IDC_CUSTOM_FIRST,m_SocketState)
END_DDX_MAP()
BOOL m_SocketState = 1;
给对话框的两个按钮添加事件用来重置状态和进行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;
}
看一看做了个什么。