说起状态栏其实都见过,但是具体不知道是哪个位置(我以前也不知道。。),先来看下图
如上图所示就是状态栏的位置,对于实现状态栏我大致分为如下过程:
对话框 | IDD_DIALOG1 |
字符串表 | STRINGTABLE |
菜单 | IDR_MENU1 |
图标 | IDI_ICON1 |
对画框过程 | _ProcDlgMain |
改变状态栏大小 | _Resize |
总体实现 | 创建显示 |
首先是资源文件:这次依旧是使用ResEdit资源编辑工具,相比以往的程序这次需要创建一个字符串表(在使用版本信息的时候也曾经使用过)因为很少使用,感觉有点陌生,我直接在添加资源里面选择了“添加字符串表”选项,在里面依次添加即可,注意格式就行,在完成后会在resourc.h文件中自动为字符串项设置ID,如果想更改的话可以自己以文本的格式打开这个文件修改(他自己分配的ID数值有的貌似很大书写不方便,可以改成方便书写的,方便起见。。),因为需要在鼠标放在命令项(例如菜单中的“文件”“打开文件”按钮)时在对话框的状态栏显示提示字符(也就是对应的字符串表中的字符串),所以字符串表中的字符串要和菜单中的命令项相对应,罗云斌老师的书中介绍的程序代码将菜单和字符串表的命令ID设为一样的,这样确实很方便,但是我在使用ResEdit资源编辑工具编写的时候会出现一些错误,我感觉字符串表还是写成IDS_开头的比较直观一些,容易理解,但是如果这样写还会遇见一个问题,就是在代码中使用 MenuHelp()函数的时候,需要注意第六个参数dwMenuHelp, 我们可以看一下源代码中在.const段中定义了常量:
dwMenuHelp dd 2,40000,0,0
一共是四个双字,第一个双字代表的就是命令项的基数(这个不太容易理解,我刚写的时候很不解,其实就是以字符串表第一项为基数0以后的每项依次加1,而 IDS_OPEN这一项基数就为2,写错的话你就会发现状态栏出现的提示字符是错乱的,张冠李戴了。。。,第二个双字的基数就需要写字符串表第一项的ID值)具体看一下资源脚本代码如下:
// Generated by ResEdit 1.6.6
// Copyright (C) 2006-2015
// http://www.resedit.net
#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
#include "resource.h"
//
// Menu resources
//
LANGUAGE 1023, 63
IDR_MENU1 MENU
{
POPUP "文件(&F)"
{
MENUITEM "打开文件(&O)", IDM_OPEN
MENUITEM "文件另存为(&C)", IDM_SAVES
MENUITEM SEPARATOR
MENUITEM "页面设置(&P)", IDM_PAGESETUP
MENUITEM SEPARATOR
MENUITEM "退出(&X)", IDM_EXIT
}
POPUP "查看(&V)"
{
MENUITEM "查找字符串(&S)", IDM_FIND
MENUITEM " 替换字符串(&R)", IDM_REPLACE
MENUITEM SEPARATOR
MENUITEM "选择字体(&F)", IDM_SELFONT
MENUITEM " 选择颜色(&B)", IDM_SELCOLOR
}
}
//
// Dialog resources
//
LANGUAGE 1023, 63
IDD_DIALOG1 DIALOGEX 0, 0, 186, 95
STYLE WS_POPUP | WS_VISIBLE | WS_SYSMENU | WS_SIZEBOX ;注意此处风格需要选择可以拖动边框的的WS_THICKFRAME 这个也可以写为WS_SIZEBOX
CAPTION "状态栏示例"
MENU IDR_MENU1
FONT 8, "Ms Shell Dlg", 0, 0, 0
{
}
//
// String Table resources
//
LANGUAGE 1023, 63
STRINGTABLE
{
IDS_MENUHELP "包含文件操作的命令"
IDS_MENUHELP1 "包含操作视图的命令"
IDS_OPEN "打开需要编辑的文件"
IDS_SAVES "以另外一个文件名保存文件"
IDS_PAGESETUP "选择打印机以及设置页边距.纸张大小等打印参数"
IDS_EXIT "退出本程序"
IDS_FIND "在窗口中搜索文字"
IDS_REPLACE "在窗口中搜索文字并替换"
IDS_SELFONT "选择窗口中文字使用的字体"
IDS_SELCOLOR "选择窗口的背景颜色"
}
//
// Icon resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDI_ICON1 ICON "icon3.ico"
注意上面字符串表的每一项与菜单的命令项都是对应的,次序是一致的,这一点需要注意,很容易犯错误。
关于资源文件的编写还有一点:在创建对话框的时候,以前也写过对话框的程序,但是关于对话框的风格类型(style)以前都没有过多的关注,因为ResEdit工具只要选择就会自动生成资源代码,但是这次写就遇见问题了。刚开始我使用ResEdit编写完资源代码就编译生成22.resc文件然后和程序实现代码生成的22.obj等文件链接起来以后构建并运行,对话框如自己所想出现在屏幕上,但是拖动状态栏下面那个小三角的话窗口的大小并不能改变,刚开始我以为是创建窗口对话框函数的风格指定错误了,但是一个一个参数对照后发现,并不是这的原因,创建状态栏和窗口的函数的参数都没有错误,最后我去查看资源文件对话框的风格(style),我查看了一下资料关于窗口的风格的定义值,发现WS_THICKFRAME这个风格可以使窗口的边框来回拖动改变边框的大小(对话框和窗口的风格通用的),加上以后重新编译连接构建运行,拖动窗口,OK问题解决了,由于这个错误我也顺便看了下关于WS_SYSMENU这个有点陌生的风格,就是加上窗口右上角的叉号,添加上默认的窗口的一些属性。
还有一点就是源代码中:
invoke CreateStatusWindow,WS_CHILD OR WS_VISIBLE OR SBS_SIZEGRIP,NULL,hWinMain,ID_STATUSBAR
这句代码中SBS_SIZEGRIP这个风格与SBARS_SIZEGRIP这个风格是一样的,都是在状态栏右下角显示一个小三角用来拖动改变父窗口大小,但是在RadASM工具中SBARS_SIZEGRIP并没有显示蓝色(就是像其他声明好的风格一样,但是他依然起作用了)。
下面看一下程序源代码:
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include Comctl32.inc
includelib Comctl32.lib
IDI_ICON1 equ 102
IDD_DIALOG1 equ 100
IDR_MENU1 equ 101
IDM_EXIT equ 40003
IDS_MENUHELP equ 40000
ID_STATUSBAR equ 1
ID_EDIT equ 2
.data
hInstance dd ?
hWinMain dd ?
hWinStatus dd ?
hWinEdit dd ?
lpsz1 dd ?
lpsz2 dd ?
.const
szClass0 db 'msctls_statusbar32',0
szClass db 'EDIT',0
szFormat0 db '%02d:%02d:%02d',0
szFormat1 db '字节数:%d',0
sz1 db '插入',0
sz2 db '改写',0
dwStatusWidth dd 60,140,172,-1
dwMenuHelp dd 2,40000,0,0
.code
_Resize proc
LOCAL @stRect:RECT,@stRect1:RECT
invoke MoveWindow,hWinStatus,0,0,0,0,TRUE
invoke GetWindowRect,hWinStatus,addr @stRect
invoke GetClientRect,hWinMain,addr @stRect1
mov ecx,@stRect1.right
sub ecx,@stRect1.left
mov eax,@stRect1.bottom
sub eax,@stRect1.top
sub eax,@stRect.bottom
add eax,@stRect.top
invoke MoveWindow,hWinEdit,0,0,ecx,eax,TRUE
ret
_Resize endp
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
LOCAL @szBuffer[128]:byte
LOCAL @stST:SYSTEMTIME
LOCAL @stPoint:POINT,@stRect:RECT
mov eax,wMsg
.if eax == WM_TIMER
invoke GetLocalTime,addr @stST
movzx eax,@stST.wHour
movzx ebx,@stST.wMinute
movzx ecx,@stST.wSecond
invoke wsprintf,addr @szBuffer,addr szFormat0,eax,ebx,ecx
invoke SendMessage,hWinStatus,SB_SETTEXT,0,addr @szBuffer
.elseif eax == WM_CLOSE
invoke KillTimer,hWnd,1
invoke EndDialog,hWnd,NULL
.elseif eax == WM_INITDIALOG
mov eax,hWnd
mov hWinMain,eax
invoke CreateStatusWindow,WS_CHILD OR WS_VISIBLE OR SBS_SIZEGRIP,NULL,hWinMain,ID_STATUSBAR ;参数SBS_SIZEGRIP和SBARS_SIZEGRIP是一样的效果
mov hWinStatus,eax
invoke SendMessage,hWinStatus,SB_SETPARTS,4,offset dwStatusWidth
mov lpsz1,offset sz1
mov lpsz2,offset sz2
invoke SendMessage,hWinStatus,SB_SETTEXT,2,lpsz1
invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr szClass,NULL,WS_CHILD OR WS_VISIBLE OR ES_MULTILINE OR\
ES_WANTRETURN OR WS_VSCROLL OR ES_AUTOHSCROLL,0,0,0,0,hWnd,ID_EDIT,hInstance,NULL
mov hWinEdit,eax
call _Resize
invoke SetTimer,hWnd,1,300,NULL
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDM_EXIT
invoke EndDialog,hWnd,NULL
.elseif ax == ID_EDIT
invoke GetWindowTextLength,hWinEdit
invoke wsprintf,addr @szBuffer,addr szFormat1,eax
invoke SendMessage,hWinStatus,SB_SETTEXT,1,addr @szBuffer
.endif
.elseif eax == WM_MENUSELECT
invoke MenuHelp,WM_MENUSELECT,wParam,lParam,lParam,hInstance,hWinStatus,offset dwMenuHelp
.elseif eax == WM_SIZE
call _Resize
.elseif eax == WM_NOTIFY
.if wParam == ID_STATUSBAR
mov eax,lParam
mov eax,[eax + NMHDR.code]
.if eax == NM_CLICK
invoke GetCursorPos,addr @stPoint
invoke GetWindowRect,hWinStatus,addr @stRect
mov eax,@stRect.left
mov ecx,eax
add eax,140
add ecx,172
.if (@stPoint.x >= eax) && (@stPoint.x <= ecx)
mov eax,lpsz1
xchg eax,lpsz2
mov lpsz1,eax
invoke SendMessage,hWinStatus,SB_SETTEXT,2,lpsz1
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
start:
invoke InitCommonControls
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_DIALOG1,NULL,offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
源代码中还有一处值得注意:
看下面代码:
Resize proc
LOCAL @stRect:RECT,@stRect1:RECT
invoke MoveWindow,hWinStatus,0,0,0,0,TRUE
invoke GetWindowRect,hWinStatus,addr @stRect
invoke GetClientRect,hWinMain,addr @stRect1
mov ecx,@stRect1.right
sub ecx,@stRect1.left
mov eax,@stRect1.bottom
sub eax,@stRect1.top
sub eax,@stRect.bottom
add eax,@stRect.top
invoke MoveWindow,hWinEdit,0,0,ecx,eax,TRUE
ret
_Resize endp
这个子函数功能是为了实现改变状态栏的大小及位置
GetWindowRect,hWinStatus,addr @stRect 这个函数都知道是为了获取窗口的左上角以及右下角的坐标(但是这个函数需要注意的是它的坐标系是以屏幕的左上角为原点的,这个需要好好品味一下),注意在源代码中第一个参数指定的是状态栏的句柄,也就是说获取的是状态栏的大小及其坐标,(不要认为是主窗口的大小,注意句柄,我就理解错了。。)
GetClientRect,hWinMain,addr @stRect1 这个函数而是以当前窗口的左上角的点为原点建立的坐标系(说的都有点俗。。。)这样的话右下角的坐标(.right)其实也是这个窗口的宽(以前就很纠结这个怎么把坐标直接当成宽了,现在才知道。。。),对于这个函数必须知道客户区(窗口除了标题栏和工具栏剩下的就是客户区)注意在源代码中第一个参数指定的是主窗口的句柄,也就是说这个函数返回的是主窗口的客户区的大小及坐标,也就是除了标题栏和菜单的其它区域。
MoveWindow,hWinEdit,0,0,ecx,eax,TRUE 这个函数的第五个参数在我写的时候可是很纠结,
mov ecx,@stRect1.right
sub ecx,@stRect1.left
上面这两句代码是为了的得到主窗口客户区的宽度,也就是状态栏的宽度,这个好理解,但是下面:
mov eax,@stRect1.bottom
sub eax,@stRect1.top ;得到主窗口客户区的高度
sub eax,@stRect.bottom
add eax,@stRect.top ;后两句的意思就是eax+(@[email protected])
意思就是主窗口客户区的高度减去状态栏的高度正好是文本编辑
区的高度。
这个因为一点理解错就会全盘搞错,注意。
代码依然
下面介绍一下一些陌生的API函数和结构:
NMHDR
消息通过下面的宏定义之:
ON_NOTIFY( wNotifyCode, idControl, memberFxn )
wNotifyCode
通知消息标识符代码,如 TBN_BEGINADJUST。
idControl
发送通知的控件的标识符。
memberFxn
接收到通知时调用的成员函数。
API函数
MoveWindow()
功能:
改变指定窗口的位置和大小.对顶窗口来说,位置和大小取决于屏幕的左上角;对子窗口来说,位置和大小取决于父窗口客户区的左上角.对于Owned窗口,位置和大小取决于屏幕左上角.
原型:
BOOL MoveWindow( HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint )
参数:
hWnd指定了窗口的句柄
x指定了CWnd的左边的新位置。
y指定了CWnd的顶部的新位置。
nWidth指定了CWnd的新宽度。
nHeight指定了CWnd的新高度。
bRepaint指定了是否要重画CWnd。如果为TRUE,则CWnd象通常那样在OnPaint消息处理函数中接收到一条WM_PAINT消息。如果这个参数为FALSE,则不会发生任何类型的重画操作。这应用于客户区、非客户区(包括标题条和滚动条)和由于CWnd移动而露出的父窗口的任何部分。当这个参数为FALSE的时候,应用程序必须明确地使CWnd和父窗口中必须重画的部分无效或重画。
MoveWindow给窗口发送WM_WINDOWPOSCHANGING,WM_WINDOWPOSCHANGED,WM_MOVE,WM_SIZE和WM_NCCALCSIZE消息。
返回值:无
GetWindowRect()
功能:
该函数返回指定窗口的边框矩形的尺寸。该尺寸以相对于屏幕坐标左上角的屏幕坐标给出。
原型:
BOOL GetWindowRect(HWND hWnd,LPRECT lpRect);
参数:
hWnd:窗口句柄。(注意句柄,返回指定句柄的窗口,而不是当前窗口句柄)
lpRect:指向一个RECT结构的指针,该结构接收窗口的左上角和右下角的屏幕坐标。
返回值:
如果函数成功,返回值为非零:如果函数失败,返回值为零。
CreateStatusWindow()
功能:
创建一个状态栏
原型:
HWND CreateStatusWindow(
LONG style, ;风格 与窗口风格通用
LPSTR lpszText, ;要在状态栏显示的字符串
HWND hwndParent, ;父窗口
WORD wID ;状态栏控键ID
);
参数:
LONG style : 可以是一下取值的组合:
SBSAR_SIZEGRIP(同SBS_SIZEGRIP)-------显示状态栏右下角的斜条,用户可以拖动这里来改变主窗口的大小。
CCS_TOP,CCS_BOTTOM或CCS_NOMOVEY---------代表状态栏的初始位置,分别表示位于主窗口的上方,下方(默认值),和禁止沿 Y 方向移动。
CCS_NOPARENTALIGN--------状态栏只自动设置自己的高度,不自动设置自己的宽度,也不自动移动位置。
CCS_NORESIZE----------禁止状态栏所有的自动移动和自动设置自己大小的特性,并禁止CCS_TOP,CCS_BOTTOM,CCS_NOMOVEY和CCS_NOPARENTALIGN风格。
LPSTR lpszText:指向一个初始化的时候显示在状态栏上的字符串。
HWND hwndParent:指明状态栏的父窗口。
WORD wID :状态栏控件的ID,这个ID值可以用来在WM_NOTIFY消息中判断消息是否来自与状态栏。
返回值:
状态栏的句柄
MenuHelp()
功能:
很多程序都是有菜单和状态栏,当用鼠标点开菜单移动选择各菜单项的时候,会在状态栏显示关于该菜单项的说明。这个功能MFC是封装好的,用向导生成即可,但用API编程就得自己编写代码了,查了一下MSDN,有个MenuHelp函数可以做到
原型:
void MenuHelp(
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
HMENU hMainMenu,
HINSTANCE hInst,
HWND hwndStatus,
LPUINT lpwIDs
);
参数用法:
前面的参数好理解,关键是最后一个参数lpwIDs,MSDN说明是一个UINT数组指针,数组元素必须是成对的,第一个是字符串资源ID,第二个是菜单句柄,想想好象比较麻烦,得在程序加载菜单后得到每个菜单项的句柄然后填写这个数组?不厌其烦写了代码一试,却满不是那么回事,根本不起作用。于是看了看MFC的源代码,晕,居然没用这个函数,是自己处理的。再找其他SDK写的例程,发现这个数组只有4个元素,两对,第二对是0,0,标志数组结束,第一对中的字符串资源ID在资源中根本没有,第二个菜单句柄值为0x1100,代码中也没有再对这个数组进行操作了,但却能正常工作,这下更晕了。仔细研究了这个"神奇"的程序,终于知道了MenuHelp的用法:
UINT wIDs[] = {MH_BASE,0x1100,0,0}
这个MH_BASE可以随便定义一个数值,不过必须保证该值加上菜单命令ID的数值后不能超过0xFFFF。
然后在资源中加入帮助字符串,该字符串的资源ID必须是MH_BASE加上相应的菜单命令ID。而子菜单的字符串的资源ID必须是0x1100加上相应的序号,这个子菜单的帮助字符串是指:比如菜单上一般都会有"文件"这个菜单项,点开"文件"会弹出下拉菜单,显示"打开","关闭"等,鼠标移到这两个选项上在状态栏分别显示"打开文件"和"关闭",而移出这两个选项并且下拉菜单未关闭,则会显示这个子菜单的帮助字符串"文件操作"。第二个子菜单比如"编辑"的帮助字符串资源ID就是0x1100+1。
比如菜单中"打开"和"关闭"的命令ID分别为CM_OPEN和CM_CLOSE,就需要在资源中如下定义字符串:
STRINGTABLE DISCARDABLE
BEGIN
MH_BASE + CM_OPEN "打开文件"
MH_BASE + CM_CLOSE "关闭"
0x1100 + 0 "文件操作"
END
程序中响应WM_MENUSELECT消息,在其中调用MenuHelp(WM_MENUSELECT,wParam,lParam,GetMenu(hWnd),hInstance,hStatusWnd,wIDs);就可以了。
返回值:无
GetCursorPos()
功能:
该函数检取光标的位置,以屏幕坐标表示。
原型:
BOOL GetCursorPos(LPPOINT lpPoint)
参数:
IpPoint:POINT结构指针,该结构接收光标的屏幕坐标。
返回值:
如果成功,返回值非零;如果失败,返回值为零
InitCommonControls()
功能:
注册并初始化通用控件窗口类
原型:
void InitCommonControls(VOID);
参数:无
返回值:无
说明:
函数InitCommonControls是个空函数,不做任何事情。
但如果你调用了该函数,则链接器会将你的程序链接到comctl32.lib,然后在程序启动时,会加载comctl32.dll。
真正初始化的工作是在该库的入口点处做的,在这里会注册通用控件窗口类。
然后应用程序就可以创建控件窗口,就象创建其它的子窗口控件一样。