📄 编写 windows 标准控件㈠ .txt
字号:
编写 Windows 标准控件㈠
相关的例子:下载>>> 作者:AoGo 于2007-11-16上传
--------------------------------------------------------------------------------
By Aogo
什么是标准控件?
在编写程序过程中,常常因为需要整件的控件界面风格,而自定义控件外观,常用的作法,是通过子类化或超类化 Windows 内建的控件如按钮(button)/编辑框(edit)等等,按管它们的重绘消息来达到目的,如果控件行为无法子类化而满足要求,则就需要自己写控件了。
使用SDK方式编程的人都有写控件的经历,绝大部分都是以实用性为前提,一般只要达到目的,完成计划的功能,就不会再管,这种使用一次的控件,确实不需要多费工夫,是正确的好方法,但是,好的程序员,可不会这样做,有心的朋友会封装起来以备下次使用,可是,再一次使用的时候,往往会发现,编写的这个控件,除了固定的功能,大部分的功能都需要改写才能适用新的任务,于是兼容原来的功能,添加了针对这一次的代码,再下一次,再改,改了无数遍,改到后面代码乱七八糟,文档修修改改,突然觉得,还不如多花点时间子类化Windows控件或用别人的来得方便,于是,宣布这个控件死期。长时间下去,认为任何时候需要的控件别人都已经写了,经常上网下载控件。另外就是在以后的编程中,只要能够子类化/超类化标准控件达到目的,无论是多么的为难,一定不会考虑自己写控件,就算要写,因为不知道如何才能写出标准的控件而为难。
所谓的标准控件,就是以Windows内建的控件的处理规则而编写的控件。当它进行封装后,特性与标准控件一样。标准控件规则很简单,就是尽量遵守标准。大体说明:
1. 只通过消息来实现对控件功能的控制。
2. 组合控件(一个控件包含多个控件的集合)只通过风格进行更改。
3. 性能优先的功能,如果同时提供给用户使用,必须使用第一条的规则,常用的作法是编写一个函数,内部直接使用,而用户则通过消息,间接调用。中间由控件编写者定义。
4. 外观通过父窗口接管而能够实现重绘,可是是标准消息,也可以是自定义消息,除了固定的细节,大部分的元素都能够通过重绘而实现整体外观的更改。
5. 控件内部的处理过程中,能够通知用户的,尽量通知,能够由用户取消的,则同时兼顾中断的操作。
6. 复杂的控件尽量把消息细分,尽量做到一个消息对应一个功能,如果一个消息同时实现多个功能,使用者学习往往很为难。想像一下--ListView控件中好多消息其实都是可以组合到一个消息中通过标志位控件而进行不同的处理的。
在这一次的教程中,我将实际地编写一个简单的控件针对上面的规则进行解说,考虑到初学者,这次编写的控件会很简单,上面规则可能难以体现它的重要性。在以后的控件教程里,我会通过本章控件的复杂化来说明遵守规则的必要性。
打开工程并使用代码
本教程是使用MASMPlus,如果你还没有安装,请到我的主页下载,或者直接通过这个链接下载:http://www.masmplus.com
因为代码着实还不少,在这里我就没有贴出来,可以直接通过这个链接进行下载:
http://www.aogosoft.com/Periodical/20071116/FlatButton.rar
下载的压缩包中包含完整代码的MASMPlus工程,使用MASMPlus打开Flatbutton.app即可。
主要文件有4个,UserControl.asm是使用这个控件的例子程序,也是工程首文件,UserControl.rc是资源文件,控件代码则在Control.asm中,是一个完整的可独立编译的模块,并与工程链接。Control.inc则是控件的头文件,包含所有的常数定义与消息说明。
试着编译一下:
正在处理工程文件...
ml.exe /c /coff /Fo"Control.obj" "D:\MASMPlus\Project\FlatButton\Control.asm"
正在处理工程 ...
ml.exe /c /coff /nologo /Fo"UseControl.obj" "D:\MASMPlus\Project\FlatButton\UseControl.ASM"
rc.exe /Fo"UseControl.res" "D:\MASMPlus1.2\Project\FlatButton\UseControl.rc"
转换 UseControl.RC 到 UseControl.ID ...
link.exe /SUBSYSTEM:WINDOWS /nologo /OUT:"UseControl.exe" "UseControl.obj" "Control.obj" "UseControl.RES"
准备就绪:D:\MASMPlus\Project\FlatButton\UseControl.exe
程序运行结果如下:
下面,我们就对整个代码进行解说。
(注意,为了方面初学的朋友,代码无优化,也有重复的地方,一切以通俗易懂为前提)
控件,最重要就是消息处理,本章中的FlatButton这个控件,功能简单,但是因为以标准控件为目标,所以扩展性与普通的Windows控件是一样的,因为这些涉及到相当多的基础知识,实在不知从哪开头,标准控件其实对相当多的消息都要进行处理,这里我只处理了大部分常用而通用的。
首先,从Control.asm这个文件开始讲述注册控件类的函数,在主文件中调用这个函数来注册,如果以后编译成DLL,则在DLL加载时调用。
RegisterFlatButton proc hInst:DWORD
LOCAL wc :WNDCLASSEX
invoke RtlZeroMemory,addr wc,sizeof WNDCLASSEX
mov wc.cbSize,sizeof WNDCLASSEX
mov wc.style,CS_HREDRAW or CS_VREDRAW or CS_PARENTDC or CS_GLOBALCLASS
mov wc.lpfnWndProc,offset FlatButtProc
mov wc.cbWndExtra,4
mov eax,hInst
mov wc.hInstance,eax
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszClassName,offset lpszFlatButtonClassName
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
ret
RegisterFlatButton endp
其它的普通注册窗口是一样的,mov wc.cbWndExtra,4,这一句,是要求每一个控件创建分配内存时,额外分配4字节,我们用于保存控件的结构。lpszFlatButtonClassName是控件的类名,就像Button/Static一样是一个系统全局名字。
mov wc.style,CS_HREDRAW or CS_VREDRAW or CS_PARENTDC or CS_GLOBALCLASS
这里,是把类的风格,设置为当水平与垂直尺寸变化时刷新窗口,CS_PARENTDC是表示,使用父窗口的DC,这样,控件本身不会创建DC,它是使用父窗口的DC的,因为一个程序最多只能同时拥有5个DC,所以,这样做可以减少控件过多时,资源的占用,当然去掉这一个风格也是可以的。CS_GLOBALCLASS则表示是全局类,这样编译成DLL后,主程序也可以使用,否则只能在DLL中使用。这里的标志很多,无法一一说明,相关的风格常数,请参数WNDCLASSEX在MSDN中的说明。
SendParentNotifyMessage proc hButt,fNotify
local nmh:NMHDR
mov eax,fNotify
mov nmh.code,eax
invoke GetDlgCtrlID,hButt
mov nmh.idFrom,eax
mov eax,hButt
mov nmh.hwndFrom,eax
invoke GetParent,hButt
lea ecx,nmh
invoke SendMessage,eax,WM_NOTIFY,nmh.idFrom,ecx
ret
SendParentNotifyMessage endp
这个函数使用提供的参数向父窗口发送标准的WM_NOTIFY 消息来通知父窗口。返回值是父窗口返回的值,WM_NOTIFY 不受 FBS_NOTIFY 的限制,同时,根据返回值与父窗口交互,有些操作可以取消或返回不同的值来进行默认处理,本章只使用了一次就是文本更改时的通知,下一章我会复杂化这个控件,而使用这个消息实现多方位的通知。
消息处理
请对照例子代码,把光标移到控件的窗口过程中(光标放在FlatButtProc 中,MasmPlus会自动判别位置才会显示),点击WM_图标来列出消息,我将从列出的顺序进行解说:
invoke GetWindowLong,hButt,0
mov ebx,eax
invoke GetWindowLong,hButt,GWL_STYLE
mov edi,eax
assume ebx:ptr FLATBUTTON
把窗口的内存地址保存在ebx,风格保存在edi,因为几乎所有的消息中都要使用到,在这里做为一个全局来使用,可以省掉很多事,assume ebx:ptr FLATBUTTON设置ebx为窗口结构FLATBUTTON的类型。在整个过程中,可以直接使用寻址来快速访问。
WM_CREATE 窗口创建
在这个消息中初始化,我们使用LocalAlloc分配了一块内存,并保存到控件的窗口字中,就是多分配的那4字节,以后可以使用invoke GetWindowLong,hButt,0来获得这个地址。所使用的结构是:
FLATBUTTON struct
fState dd ? ;按钮状态
dwData dd ? ;用户自定义数据
hFont dd ? ;字体
FLATBUTTON ends
依照功能的强弱,结构体是不可定的,比如,以后想增加图标显示功能,则可以在上面添加新成员,利用消息进行处理。或者是多样式的控件,比如工具栏等等。
创建失败后,直接返回-1,CreateWindowEx将直接返回。
WM_ERASEBKGND 背景重绘
首先设置字体,只有再存在时,才会选入DC中,否则使用默认值,成员hFont在标准的Windows消息WM_SETFONT/WM_GETFONT中被修改。
mov eax,[ebx].hFont
.if eax!=0
invoke SelectObject,wParam,eax
.endif
.if !(edi & FBS_OWNERDRAW) ;不是由父级重绘
invoke GetClientRect,hButt,addr rt
invoke SendParentMessage,hButt,WM_CTLCOLORBTN,wParam,hButt
invoke SelectObject,wParam,eax
invoke GetWindowLong,hButt,GWL_EXSTYLE
.if !(eax & WS_EX_TRANSPARENT) ;没有透明的扩展风格
invoke PatBlt,wParam,rt.left,rt.top,rt.right,rt.bottom,PATCOPY
.endif
.endif
然后我们判断是否包含父级重绘风格,是的话,就直接结束,这样背景将是完全透明的。紧接着,发送重绘的消息给父窗口,这里我们是使用已有的Button的消息,如果觉得不想使用这些规则,可以另外定义一个消息:
WM_CTLCOLORFLATBTN equ WM_USER+*
只要大于WM_USER就可以了。使用默认消息的好处是,如果父窗口未处理,是会返回默认的设置的,与系统颜色一致,所以,下面紧接着我们不管返回值,直接选入DC。
判断扩展风格是否有透明的风格,如果有,我们就不画背景,否则就把整个客户区填充。
WM_PAINT 重绘
文本重绘,WM_ERASEBKGND与这里分开,其实在有些时候不是必须的,比如显示实时性要求比较高时,可以考虑在背景重绘消息中直接返回0,而在这个消息中有选择性地画背景,这样可以做到只画一次而防止闪烁(但是无闪烁刷新可不是这么简单,现在的教程中控件整个窗口只是一个操作元素,所以不用考虑,以后会讲解多元素时的刷新处理,如工具栏这类控件),大家可能又要问,那为什么不直接在WM_ERASEBKGND消息中完成所有的工作,事实上,这是可行的,但是问题在于,类似金山词霸这种屏幕取词的软件,只会在WM_PAINT消息中抓取文本输出,因为这是控件的标准行为,所以,万一真的要在WM_ERASEBKGND或WM_PAINT选择一个完成所有的重绘工作,请选择WM_PAINT消息。同时可以直接在注册控件时,把wc.hbrBackground设置为0,表示无背景。
invoke GetParent,hButt
invoke GetWindowLong,eax,GWL_STYLE
.if eax & WS_CLIPCHILDREN
invoke SendMessage,hButt,WM_ERASEBKGND,ps.hdc,0
.endif
首先判断父窗口是否有剪切子级控件的风格,因为控件使用了类风格CS_PARENTDC,表示使用父窗口的DC来重绘,如果父级剪切子级,那控件的WM_ERASEBKGND消息是接收不到的,也就是PAINTSTRUCT结构中的ps.fErase永远为0,事实上,背景重绘消息是在WM_PAINT中发送出去的。如果是这样,我们就手动发送消息WM_ERASEBKGND给本身来通知父级窗口重绘。
.if (edi & FBS_OWNERDRAW)
mov dis.CtlType,ODT_BUTTON invoke GetDlgCtrlID,hButt
mov dis.CtlID,eax
mov dis.itemID,eax
mov dis.itemAction,ODA_DRAWENTIRE
.if [ebx].fState & FBF_FOCUS
or dis.itemAction,ODA_FOCUS
.endif
xor eax,eax
mov dis.itemState,eax
.if [ebx].fState & FBF_PUSHED
or dis.itemState,ODS_FOCUS
.endif
.if [ebx].fState & FBF_HOVER
or dis.itemState,ODS_SELECTED
.endif
.if edi & WS_DISABLED
or dis.itemState,ODS_DISABLED
.endif
mov eax,hButt
mov dis.hwndItem,eax
mov eax,ps.hdc
mov dis.hdc,eax
invoke CopyRect,addr dis.rcItem,addr rt
mov eax,[ebx].dwData
mov dis.itemData,eax
invoke SendParentMessage,hButt,WM_DRAWITEM,dis.CtlID,addr dis
判断是否包含由父级重绘的风格,是的话,不再重绘,而是由父级重绘。参考标准的WM_DRAWITEM消息,然后填充好成员。由父窗口处理。总是包含ODA_DRAWENTIRE,是因为控件本身只有一个模块,而不像标准Button一样,可以通过风格变化成复选框/组合框。同样,这个并不是一定要使用WM_DRAWITEM消息,也可以使用自己定义的消息。
WS_DISABLED是标准风格,当窗口无效时,这个风格就会存在。所以直接使用。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -