📄 编写 windows 标准控件㈡ .txt
字号:
编写 Windows 标准控件㈡
相关的例子:下载>>> 作者:AoGo 于2007-12-16上传
--------------------------------------------------------------------------------
上一章我们编写了一个简单地平坦按钮控件,最后留的习题,不知道大家有没有试着去完成,其实是很简单地,问题在于绘出右边的小三角形的图形,自己画?用图片?呵呵,都不用,用 API 函数吧:
invoke DrawFrameControl,hdc,lpRT,DFC_SCROLL,DFCS_SCROLLDOWN
这个函数通过指定不同的标志性参数,来重绘按钮,菜单,滚动条,窗口标题栏等不同的部位,比如上面函数就表示要重画滚动条的向下滚动键头的图形。多试试不同的参数看看,你会发现很有意思的东西。
学习编程,举一反三是很重要的,可是让你快速积累经验,比如,在 MSDN 或者 API 手册中,搜索上面这个函数时,会有相似的函数列出来,这个时候一定要依次稍稍看一下,不要求立即使用,但是脑子里有印象,下一次说不定就能用上。
除了这个图形,还有那个分隔线,用 DraEdge 吧:
invoke SetRect,addr lpRT,top,left,bottom,left+2 ;宽度2个像素,可画出分隔线
invoke DrawEdge,hdc,lpRT,BDR_SUNKENOUTER,BF_RECT
原来本章是打算完成这个习题,然后封装。并且,告诉大家在不动控件代码的情况下,通过接管通知消息来扩展在为颜色选择器的功能,文章都已经写得差不多了,突然发现,我是要写“编写”的教程,而不是使用,而接管消息实现什么功能的,这已经脱离了这个范畴,所以,苦恼了几天,还是下决心重写。T_T 。。。
在本章中,我们将编写一个新的控件,这个控件和之前的相比,会复杂得多,它的重点在于告诉大家如何使用不可定的操作集,比较简单的,就是列表控件了。
同样,例子已经写好,代码仍然很多,下载之后,使用 MASMPlus 打开它,先编译一下吧。
程序运行界面如下:
正在处理工程文件...
ml.exe /c /coff /Fo"List.obj" "D:\MASMPlus1.2\Project\ControlLIB\List.asm"
ml.exe /c /coff /Fo"Control.obj" "D:\MASMPlus1.2\Project\ControlLIB\Control.asm"
link.exe /LIB "list.obj" /OUT:"Control.LIB" "Control.obj"
正在处理工程 ...
ml.exe /c /coff /nologo /Fo"UseControl.obj" "D:\MASMPlus1.2\Project\ControlLIB\UseControl.ASM"
link.exe /SUBSYSTEM:WINDOWS /nologo /OUT:"UseControl.exe" "UseControl.obj"
准备就绪:D:\MASMPlus1.2\Project\ControlLIB\UseControl.exe
看上面的编译信息,你是否查觉到了什么呢?呵呵,从这一章开始,我们将使用建立LIB的方式来写控件,前一章的FlatButton控件也包含在内,因为两个控件都是单独的OBJ链接成LIB的,所以,如果你只使用其中一个控件,只需要调用其相应的注册函数就可以了,没有注册的控件调用,是不会包含到你的程序中的,这样,当你的控件越来越多时,也只需引用一个库,用多少个就插入多少个到程序中,简单方便。这就是控件库了。
同时,不仅仅是控件,包含单独的函数也是可以的,比如你写了一个排序的函数,只需要按独立的方式编写并编译,再全部一次链接,这样这个函数也是一个模块。
有任何不明折的地方,单击右键看一下参数设置吧。
本章写的控件,是 List,列表控件,按上一章的方式,我们来进行解说。
首先,是确定 List 所需的结构:
列表控件是一个多操作无素的控件,比如可以有不定个数的项目,每个项目文本不同,所以,我们需要使用内存来进行管理,我使用的方法是与索引对应的数组来保存每一个项目内存,当删除时释放对应的内存并移动数组,为什么不使用更好的内存管理呢?更好的结构同时也是更复杂的,这个时候就要按最初的想法来确定,如果每一个项目都要有状态,比如多选,或者是为了适应更大量数据处理,就必须用链表或者别的方法,而我的最初设计,只是做一个纯列表框控件,所以,这个时候使用直接的DWORD数组,反而会更好,因为它操作简单,寻址方便。
下面针对某些成员进行介绍。
hHeap
保存堆句柄,关于内存模式的选择,我用的是堆内存,自己创建一个堆来使用,好处是,只需要释放堆,内存就全部释放了,不需要每个项目都去遍历再释放。当然大型数据处理时,就需要根据用户的情况而定。这里我就不重复了,关于内存管理,如果细说几乎可以写本书了。
fStyle
保存控件的某些设置,MLFS_FOCUS表示控件是当前焦点,这样不需要每次去GetFocus再判断,这个标志在WM_SETFOCUS/WM_KILLFOCUS中被添加或删除,MLFS_SHARED标识当前的图像列表是否是用户设置的,比如用户手动设置了图像列表,这样我们不会在控件销毁时去删除它。
iMaxWidth
水平最大宽度,每添加一个项目,会自动获得字符串的长度对比后保存最长的,用于设置水平滚动条
iHeight
项目高度,所有项目的高度都是相同的。当翻页,上下,刷新等操作时,需要高度进行计算。
rt
客户区域,在WM_SIZE消息中保存,因为大小的变化是一定会有WM_SIZE消息的,保存后在其它消息中使用时,不需每次都使用GetClientRect来得到。
lpItems是一个指向 DWORD 的数组,用于保存每一个项目内存的地址,而列表框项目的索引与数组的索引是一样的,所以操作与寻址都是很方便的,而指向的项目的结构,则是这样:
本控件的窗口风格比较少:
MLS_HASBITMAP equ 1h ;显示图像
MLS_OWNERDRAW equ 8h ;由父级重绘
MLS_CLASS equ 20h ;需要所有按键,默认只处理方向键,其它交给对话框处理(如果是在对话框中)
我们不再使用 WM_COMMAND 通知码了,只使用通知消息,也即是WM_NOTIFY,事实上,只有简单地控件,才会不使用 WM_NOTIFY 而使用 WM_COMMAND。
消息的文档我就不再细述了,单单对文档来解说,是没有效果的,参照代码来看,首先是函数:
注册控件函数 RegisterMyList
注册列表控件的函数,wc.style不再使用 CS_PARENTDC,并且需要双击。
发送通知消息函数 ParentNotify
向父窗口发送通知消息,与上一章不同的时,列表控件发送通知消息时,使用的是自己的结构:
前面的 hdr 是原本的 NMHDR 结构,这是固定的,后面是一个枚举结构体,其实都是表示一个成员,只是在使用时用于区分是哪个通知消息。
;通知消息要小于NM_LAST,大于NM_LAST的是系统使用的。注意系统通知消息为负数,所以这里是减而不是加
NMN_SELCHANGING equ NM_LAST-1 ;选择更改中,返回非零值取消选择更改,iCurrPos=要选择的项目
MLN_SELCHANGED equ NM_LAST-2 ;选择更改的,iOldSel=指向旧的选择索引
MLN_CLICK equ NM_CLICK ;项目单击,XY=当前坐标,可以取消
MLN_SETFOCUS equ NM_SETFOCUS ;设置焦点,hWndFocus=失去焦点的窗口句柄
MLN_KILLFOCUS equ NM_KILLFOCUS ;失去焦点.hWndFocus=得到焦点的窗口句柄
MLN_DBLCLK equ NM_DBLCLK ;双击,iPos=当前选择
MLN_IMAGECHANGED equ NM_LAST-3 ;图像列表更改了。hImageList=新的图像列表
重绘项目函数 DrawItem
DrawItem proc uses esi edi ebx,hDlg,hdc,lpMYLIST,Item_Index,lpRECT,WndStyle
hDlg 列表控件句柄
hdc 设备上下文,这个参数如果没0,函数将从hDlg获得DC。
lpMYLIST 窗口结构地址
Item_Index 项目索引,为-1表示重绘的是空白部分
LPRECT 重绘区域
WNdStyle 窗口风格,将从这里判断是否包含父组重绘风格。
这个函数是公用也是唯一的重绘项目函数,自动判断是重绘还是通知父级。
消息处理
WM_CREATE 窗口创建
invoke LocalAlloc,LMEM_ZEROINIT or LMEM_FIXED,sizeof MYLIST
test eax,eax
jnz @@SkipFail
@@: ;内存不足,直接返回-1,窗口创建失败
dec eax
jmp @@Ret
@@SkipFail:
mov ebx,eax
invoke SetWindowLong,hList,0,ebx ;保存到实例的窗口字中
mov [ebx].iHeight,MIN_HEIGHT
invoke ImageList_Create,ICON_SIZE,ICON_SIZE,ILC_COLOR32 or ILC_MASK,0,0
mov [ebx].hImage,eax
invoke SendMessage,hList,MLM_RESTORE,0,0
创建内存并保存,设置小最高度,创建图像列表,然后初始化。
WM_SIZE 窗口大小变化
invoke GetClientRect,hList,addr [ebx].rt
mov sci.fMask,SIF_RANGE or SIF_PAGE or SIF_DISABLENOSCROLL
mov eax,[ebx].rt.bottom
xor edx,edx
div [ebx].iHeight
mov ecx,[ebx].iCount
sub ecx,eax
.if [ebx].iTop>ecx ;自动设置正确位置
mov [ebx].iTop,ecx
mov sci.nPos,ecx
or sci.fMask,SIF_POS
.endif
mov sci.cbSize,sizeof SCROLLINFO
mov sci.nPage,eax ;设置滚动条一页的范围
mov eax,[ebx].iCount
dec eax
mov sci.nMax,eax
xor eax,eax
mov sci.nMin,eax
invoke SetScrollInfo,hList,SB_VERT,addr sci,TRUE
mov sci.fMask,SIF_RANGE or SIF_PAGE or SIF_DISABLENOSCROLL
mov eax,[ebx].iMaxWidth
mov sci.nMax,eax
mov eax,[ebx].rt.right
mov sci.nPage,eax
xor eax,eax
mov sci.nMin,eax
invoke SetScrollInfo,hList,SB_HORZ,addr sci,TRUE
invoke InvalidateRect,hList,0,0
保存窗口客户区域,然后将高度和项目高做除法计算出一页显示的项目数。设置到滚动条上。消息滚动条总是使用iMaxWidth来设置。
然后刷新整个客户区域,因为我们的控件并没有包含大小变化时刷新的类标志。
WM_PAINT ;界面重绘
invoke BeginPaint,hList,addr ps
mov eax,[ebx].rt.top
mov p.y,eax
mov edi,[ebx].iTop
@@:
cmp edi,[ebx].iCount
jae @F
mov eax,[ebx].iHeight
add eax,p.y
invoke SetRect,addr ItemRt,[ebx].rt.left,p.y,[ebx].rt.right,eax
invoke DrawItem,hList,ps.hdc,ebx,edi,addr ItemRt,fStyle
inc edi
mov eax,ItemRt.bottom
mov p.y,eax
cmp eax,[ebx].rt.bottom ;比较项目是否已经在底部
jb @B
@@:
;如果项目不足一页,使用默认画刷画出下面的部分
mov eax,[ebx].rt.bottom
.if p.y<eax
invoke SetRect,addr ItemRt,[ebx].rt.left,p.y,[ebx].rt.right,[ebx].rt.bottom
invoke DrawItem,hList,ps.hdc,ebx,-1,addr ItemRt,fStyle
.endif
invoke EndPaint,hList,addr ps
从保存的iTop开始重绘,iTop是当前可见页的第一项索引,设置区域之后,交给重绘函数。
当超过总数或者项目不足一页,自动跳出,之后,判断是否需要画空白部分。同样是交给重绘函数
WM_LBUTTONDOWN 鼠标按下
invoke ParentNotify,hList,MLN_CLICK,lParam
.if eax==0
mov eax,lParam
shr eax,16
xor edx,edx
div [ebx].iHeight
add eax,[ebx].iTop
invoke SendMessage,hList,MLM_SETCURSEL,eax,MSC_SETFOCUS
.endif
首先发送消息给父窗口,如果返回0,表示继续处理,计算当前点击位置,然后再发送消息设置当前选择项目。
这样做是为什么呢?比如,用户想做复选项目时,可以使用两个图形,设置dwData,然后在这里处理点击并复选或者不选。呵呵。
WM_LBUTTONDBLCLK 双击
invoke ParentNotify,hList,MLN_DBLCLK,[ebx].iSel
并没有特别的处理,但是发送通知消息给父窗口。让父窗口处理
WM_KEYDOWN ;键盘按下
mov edi,[ebx].iTop
mov ecx,[ebx].iSel
mov eax,wParam
.if eax==VK_UP
.if sdword ptr ecx>0
dec ecx
invoke SendMessage,hList,MLM_SETCURSEL,ecx,MSC_SETVISIBLE
.endif
.elseif eax==VK_DOWN
mov eax,[ebx].iCount
dec eax
.if ecx<eax
inc ecx
invoke SendMessage,hList,MLM_SETCURSEL,ecx,MSC_SETVISIBLE
.endif
上下键按下时,判断是否在可移动区域,然后设置索引,调用MLM_SETCURSEL消息
.elseif eax==VK_PGUP
mov eax,[ebx].rt.bottom
xor edx,edx
div [ebx].iHeight
sub edi,eax
.if sdword ptr edi<0
xor edi,edi
.endif
sub ecx,eax
.if SDWORD ptr ecx<0
xor ecx,ecx
.endif
invoke SendMessage,hList,MLM_SETCURSEL,ecx,0
mov [ebx].iTop,edi
mov sci.nPos,edi
call @@SetScrollPos
.elseif eax==VK_PGDN
mov eax,[ebx].rt.bottom
xor edx,edx
div [ebx].iHeight
add ecx,eax
mov edx,[ebx].iCount
.if ecx>=edx
mov ecx,edx
dec ecx
.endif
add edi,eax
mov edx,[ebx].iCount
sub edx,eax
.if edi>=edx
mov edi,edx
dec edi
.endif
invoke SendMessage,hList,MLM_SETCURSEL,ecx,0
mov [ebx].iTop,edi
mov sci.nPos,edi
call @@SetScrollPos
.endif
向下/向上翻页。思路是,翻页是以iTop为准,向上时,如果位置是最顶或最底部了,则自动限制在范围内。当前选择与当前顶位置单独设置可以在翻页后,选择项目位置坐标不变。
WM_SETFOCUS ;设置焦点
invoke ParentNotify,hList,MLN_SETFOCUS,wParam
or [ebx].fStyle , MLFS_FOCUS
invoke InvalidateRect,hList,0,TRUE
通知父窗口,然后加上MLFS_FOCUS风格。接着重绘
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -