对下面这样的界面我们一定很熟悉,一个对话框,右上角有两个按钮,一个是小问号(我称之“问号按钮”),一个是叉(关闭按钮),点一下问号按钮,鼠标光标通常就变成了一个带问号的箭头,用这个光标点击一下对话框里的元素,就能弹出一个简要的帮助说明。
这个功能我十分喜欢,因为它很直观,简单,不用查询繁琐的帮助文档去寻找答案。从事软件开发之后,我写过很多程序,很多都有用户界面的,却一般都没有实现这个功能,今天想起来,想尝试实现它,于是有此文。
我最早是这样考虑的:要在标题栏上增加一个问号按钮,得参考一些系统菜单操作API,点这个问号按钮会产生一个系统消息,我在处理这个消息的时候把对话框的鼠标光标改为带问号的指针,用这个带问号的指针点击对话框的某个元素,就产生一个“WM_LBUTTONDOWN”的消息,处理这个消息,根据鼠标光标的位置判定鼠标点击的是对话框上的那个元素,获取这个元素的帮助字符串,创建一个没有Title的很小的窗口,把这个小窗口在鼠标光标位置处Pop出来,然后在上面Draw一些帮助字符串,当这个小窗口失去焦点,或被用户用鼠标点击了一下,或按了一下键盘什么的,就会Hide起来,当用户又执行了上述的“问号点击操作”之后,这个小窗口才会再次Show出,最后在对话框关闭的时候Destroy这个小窗口。
看起来是不是比较复杂?我也觉得复杂,首先在对话框的标题栏上增加一个问号按钮就不容易,有没有简单些的方法?当然有,看下图:
其实只需要在对话框属性上选中Context Help这个选项就OK了,把对话框显示出来就能发现右上角的小问号按钮。(感谢AShen同学教了我这招)现在点击这个小问号,鼠标光标就自动变成了带问号的指针,然后再点击对话框中的某一处……当然了,你没有编写接下去的处理代码,所以什么都没有出现,而鼠标光标又变回了正常的箭头。
我们现在关心的是这么一个点击的动作会产生些什么消息,消息确实是有的,并且非常好用,完全没有我前面推测的什么根据光标位置判定点击的对话框元素等等那么复杂,这个消息叫WM_HELP,MSDN上能找到,我大致说明一下:
lphi = (LPHELPINFO) lParam;
typedef struct tagHELPINFO {
UINT cbSize;
int iContextType
int iCtrlId;
HANDLE hItemHandle;
DWORD dwContextId;
POINT MousePos;
} HELPINFO, FAR *LPHELPINFO;
WM_HELP的lParam参数是指向HELPINFO这个结构的指针,我们需要用到这个结构的几个成员,一是iContextType==HELPINFO_WINDOW的时候,iCtrlId的值,这个值就是点击的对话框上元素的ID,具体看resource.h中的定义;另一个就是MousePos,光标位置;至于其它,就自己看看MSDN,在别处会用到的,但这里就简单些,暂时不用,此文只是起个抛砖引玉的作用。
现在只剩下一个问题了,那就是如何实现那个pop出来的小窗口?我马上想到了tooltip control,什么是tooltip control?我想你天天见,天天用,只是不知道它就是tooltip control而已,看看下面这截图:
当鼠标光标停留在工具栏上片刻(通常是你最长的双击间隔时间),光标下面就会出现一个小提示窗口,你把鼠标移动到别处后,这个小提示就消失或者换成别的了,当然它还有别的形式,这里就不一一列举了,这个就是tooltip control,没错,它是一个控件,它的实现在comctl32.dll中,这个dll是windows带的,这也意味着使用它并不需要额外的库。不过说了这么多最后我还是发现这个tooltip control不怎么适合,我在这里提起它是鼓励读者你自己去试试看,具体参考MSDN,在“PlatformSDK/User Interface/Windows Common Control/Tooltip Controls”下能找到。最后我是自己手动实现了这么一个窗口的,下面我将给出代码:
在对话框的WM_INITDIALOG消息处理中:
{
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = MyToolTipProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = g_hInstance;
wndclass.hIcon = LoadIcon(g_hInstance, (LPCTSTR)IDI_ICONAPP);
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = TEXT("MyToolTipWindow");
RegisterClass (&wndclass);
hwndToolTip = CreateWindow(TEXT("MyToolTipWindow"), TEXT(""),
WS_POPUP|WS_BORDER, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, hDlg, (HMENU)NULL, g_hInstance, NULL);
}
其中"MyToolTipWindow"是我要注册的窗口类名,你可以起别的名字,注意这个窗口是没有标题栏的,hDlg是这个小小窗口的父窗口,也就是对话框的句柄,g_hInstance是进程实例句柄,我是把它存在一个全局变量中的,所以有个“g_”前缀,还要注意一下,把hwndToolTip作为全局变量或者static变量,以保存它的值,因为之后我们还需要用。
在对话框的WM_DESTROY消息中:
{
DestroyWindow(hwndToolTip);
}
把这个tooltip control删除。
好,现在我们看看"MyToolTipWindow"这个窗口的消息处理函数MyToolTipProc:
#define WM_USER_SETTEXT (WM_USER+10) //wParam : Pointer to the TCHAR string; lParam : NULL
#define WM_USER_SHOWWINDOW (WM_USER+11) //wParam: mouse.x; lParam : mouse.y
// Message handler for MyToolTipWindow.
LRESULT CALLBACK MyToolTipProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static TCHAR szToDisp[1024];
switch (message)
{
case WM_CREATE:
return TRUE;
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_KEYDOWN:
case WM_KILLFOCUS:
ShowWindow(hwnd, FALSE);
break;
case WM_PAINT:
{
RECT rect;
PAINTSTRUCT ps;
HDC hdc = BeginPaint (hwnd, &ps);
GetWindowRect(hwnd, &rect);
rect.right = rect.right-rect.left;
rect.bottom = rect.bottom-rect.top;
rect.top = 0;
rect.left = 0;
DrawText(hdc, szToDisp, lstrlen(szToDisp), &rect, DT_LEFT|DT_WORDBREAK);
EndPaint (hwnd, &ps);
}
break;
case WM_USER_SETTEXT:
lstrcpy(szToDisp, (TCHAR *)wParam);
return TRUE;
case WM_USER_SHOWWINDOW:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint (hwnd, &ps);
DWORD dwRtn = GetTabbedTextExtent(hdc, szToDisp, lstrlen(szToDisp), 0, NULL);
EndPaint (hwnd, &ps);
UINT iScreenWidth = GetSystemMetrics(SM_CXFULLSCREEN);
UINT iXPos = wParam+LOWORD(dwRtn)+4>iScreenWidth?iScreenWidth-LOWORD(dwRtn)-4:wParam;
MoveWindow(hwnd, iXPos, lParam+21, LOWORD(dwRtn)+4, HIWORD(dwRtn)+3, FALSE);
ShowWindow(hwnd, TRUE);
}
return TRUE;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
接到WM_LBUTTONDOWN,WM_RBUTTONDOWN,WM_KEYDOWN,WM_KILLFOCUS这几个消息之后,就把窗口隐藏起来,用WM_USER_SETTEXT这个消息设置要在mytooltip中显示的文本,然后用WM_USER_SHOWWINDOW把mytooltip show出来,WM_USER_SHOWWINDOW里面那些处理是为了根据要显示的文本和鼠标的位置调整mytooltip的窗口大小和位置,其它倒是很简单,没什么好解释的。
最后就是在对话框处理函数中加入对WM_HELP消息的响应:
{
LPHELPINFO lphi = (LPHELPINFO) lParam;
if (lphi->iContextType==HELPINFO_WINDOW)
{
TCHAR szDisp[256];
memset(szDisp, 0, sizeof(szDisp));
LoadString(g_hInstance, lphi->iCtrlId, szDisp, 255);
SendMessage(hwndToolTip, WM_USER_SETTEXT, (WPARAM)(szDisp), NULL);
if (0==lstrlen(szDisp))
break;
SendMessage(hwndToolTip, WM_USER_SHOWWINDOW, lphi->MousePos.x, lphi->MousePos.y);
}
}
LoadString函数能在程序资源中Load出相应对话框元素ID的string,这个需要自己编辑一下string资源。大功告成,看看效果吧:
留点作业给读者了,这个MyToolTip并不怎么完美,也许你看出来了,如果文本太长了,不会自动换行,很难看,而且没有“阴影效果”,这两个功能就交给读者你了,写好了给我一份,我先干活去了。byebye!