防止用户进行正常的GUI 操作

赵湘宁


本文范例程序

问题:

    我在VC6.0中建立了一个SDI工程,工程中将主框架窗口切分为两个视图窗口。如何防止用户移动主窗口以及调整切分视图的大小?

   
有时候总感觉对不起用户,原因是编程人员总是出于自己的目的,限制用户进行这样或那样正常的图形界面操作。如果某个程序限制我移动窗口、调整窗口大小或限制使用剪切、粘贴等功能的话,我的第一感觉就是将这个程序丢进垃圾箱。
   
但我是谁,凭什么对此妄加评论呢?也许在某些情况下限制窗口移动和调整窗口大小的操作是明智的呢。也许你在为总统编写一个核武器控制程序呢。不管怎样总是有这样的需求。限制调整窗口大小的最简单的方法是创建窗口时不要用WS_THICKFRAME。如果一个窗口没有了框架,那调整大小的途径就堵住了,但关闭WS_THICKFRAME后,窗口还有一个标题框,用它鼠标点住它还可以移动窗口。为此你还必须屏蔽掉WS_CAPTION,这样一来,你的应用程序连标题框都没了。你当然要让总统知道他正在使用的是核武器控制程序,而不是太空战游戏。如何避免这种进退两难的局面呢?如何让应用程序有标题框而又不能移动窗口呢?并且你还不想使用瘦框架,而使用肥框架又不能调整串口大小该如何实现?
   
实际上,蛋糕会有的,而且你还能吃到蛋糕。你既可以让窗口成为粗框架并带有标题框,同时又可以限制窗口的移动和调整窗口的大小。诀窍就是处理WM_NCHITTEST消息。当鼠标位于非客户区时,Windows会发送这个暧昧的消息来检查鼠标特定的位置。所谓非客户区:就是......,这是菜鸟级的问题啦,我在这就不罗嗦了,非客户区所包括的范围有:菜单、标题况和窗口边界。通常你根本不必关心非客户区的存在以及它包含一些什么区域,但有时候,比如现在你最好还是卷起袖子认真研究一下它吧。
   
当用户在标题框中点击鼠标时,Windows发送WM_NCHITTEST消息。默认的窗口过程检查鼠标坐标并返回一个特定的鼠标点击测试代码(HIT-TEST CODE)。例如,如果鼠标是在标题框中,默认的窗口过程返回HTCAPTION。如果鼠标是在左边界或右边界,默认的窗口过程分别返回HTLEFTHTRIGHT。根据这些返回的代码值。Windows进行窗口移动或调整窗口大小的操作。
通过重载WM_NCHITTEST消息处理函数限制窗口的移动和调整窗口大小,返回值是由其它的值代替HTCAPTIONHTLEFTHTRIGHT,那到底返回值是什么呢?首先可能想到返回HTNOWHERE,但这样的话会有一个问题:如果另一个窗口覆盖在你的窗口上,这时单击你的应用窗口的标题框的话,什么事也不会发生。我的意思是Windows不会激活你的应用程序窗口。那返回HTTRANSPARENT如何呢?也不行。HTTRANSPARENTHTNOWHERE都使得现存的窗口框架状态不明确。
   
正确的返回值应该是HTBORDER,当用户单击粗框架窗口(可调整窗口大小)的边界时,默认的窗口过程返回相同的值。如果返回HTBORDERWindows激活应用程序窗口时就不会初始化任何移动窗口或调整窗口大小的操作。
   
本文附带了一个示例工程NoSize,这个程序示范了如何实现以上讨论的内容,所有的工作都在CMainFrame类中完成。其中关键的函数是CMainFrame ::OnNcHitTest。它将所有不接受的点击测试代码映射到HTBORDER

UINT CMainFrame::OnNcHitTest(CPoint point)
{
//
获得鼠标位置代码。
UINT hit = CFrameWnd::OnNcHitTest(point);

//
不接受以下的代码值,将它们映射到HTBORDER
static char DisallowCodes[] = {
HTLEFT,HTRIGHT,HTTOP,...,
HTSIZE,HTCAPTION };

return strchr(DisallowCodes, hit)) ? HTBORDER: hit;
}

   
在实际的范例程序中还包含TRACE诊断代码,这些代码有助于你研究程序执行的过程细节。利用上面的代码行。用户便不能移动窗口以及调整窗口大小了。
但事情真是这样吗?你知不知道Windows应用程序中有多少种方法可以移动窗口和调整窗口大小?如标题框中的最大/最小按钮以及系统菜单中的移动大小等命令(见图三),不要跟我说你搞忘了。


图三

所幸的是搞定它们很容易。屏蔽掉标题框中的最大/最小按钮,方法是在PreCreateWindow函数中调整窗口风格:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
//
在标题框中去掉最大化/最小化按钮
cs.style &= ~(WS_MINIMIZEBOX|WS_MAXIMIZEBOX);
return CFrameWnd::PreCreateWindow(cs);
}

要从系统菜单中去掉那几个不想要的命令,必须在CMainFrame ::OnCreate函数中加上以下代码:

static UINT BadCommands[] = { 
SC_SIZE, SC_MOVE,
SC_MINIMIZE, SC_MAXIMIZE, SC_RESTORE, 0
};
CMenu *pSysMenu = GetSystemMenu(FALSE);
for (int i=0; BadCommands[i]; i++) {
pSysMenu->RemoveMenu(BadCommands[i], MF_BYCOMMAND);
}

现在系统菜单变成了如下图四的样子,


图四

   
你可能认为这下万事大吉了,但是还有一个漏洞,想一想还有哪里可以让用户移动窗口或调整窗口的大小?记住Windows中是有很多猫腻的!。
MFC建立的应用程序一般都会有状态条,这个状态条最右边的下角有一个三角形的尺寸手柄,


图五

用这个小玩意儿可以调整窗口的大小。

如何搞掂它呢?用下列代码即可:

//
CMainFrame::OnCreate
ModifyStyle(WS_THICKFRAME,0); 
//
去掉粗框架
m_wndStatusBar.Create(...);
ModifyStyle(0,WS_THICKFRAME); 
//
恢复粗框架

   
在上面的代码中,先是在创建状态条前关闭或屏蔽掉WS_THICKFRAME,创建完状态条之后再将WS_THICKFRAME恢复回来。原理是当创建状态条的时候,MFC根据窗口风格确定状态条上是否应该有尺寸手柄。至于在状态条创建以后如何关闭尺寸手柄,目前我还没有找到实现的方法。为了达到目的只好欺骗一下MFC 。图六是去掉尺寸手柄后的状态条。



图六

   
最后一个要解决的问题是限制使用切分窗口的分割条来调整视图窗口的大小,解决方法很简单,要做两件事情:首先是防止用鼠标操作切分条;其次是用普通光标指针代替尺寸调整光标指针。
具体方法是重载两个函数:OnLButtonDown OnMouseMove

//
重载后不允许调整大小
void CMySplitterWnd::OnLButtonDown(UINT, CPoint)
{
return; //
就这么简单
}

//
重载后不允许设置光标指针
void CMySplitterWnd::OnMouseMove(UINT, CPoint)
{
return; //
同上
}

   
我真是太喜欢这种代码了,直接return,其它什么也不做。通常当在切分条上点击鼠标时,MFC执行的是如下代码:

// (
WinSplit.cpp )
void CSplitterWnd::OnLButtonDown(UINT, CPoint pt)
{
if (m_bTracking)
return;
StartTracking(HitTest(pt));
}

为了防止Windows检测鼠标的点击,重载这个函数,并且在这个函数中除了return以外什么也不做。同样,如法炮制 CSplitterWnd::OnMouseMove ,通常当鼠标移到切分条上时,光标指针会变为象电路图中电容器一样的东西。如图七,

图七

为了防止出现这个光标指针,重载CSplitterWnd::OnMouseMove并返回return,让电容器短路,搞掂(图八)。


图八

   
第三个调整切分条的函数是:CSplitterWnd::DoKeyboardSplit。这个函数是用来实现窗口|切分菜单命令的,用户通过它使用键盘来调整切分窗口。这里不用重载CSplitterWnd::DoKeyboardSplit,把窗口|切分菜单命令从菜单中去掉不就行啦。

    哈哈!从OnNcHitTest、最大化/最小化按钮、系统菜单到欺骗MFC状态条……,最后是切分 条。现在窗口应该保险了,试试看吧,肯定既不能移动窗口,也不能调整窗口大小。如果客户打电话来抱怨或删除你的程序的话请不要感到意外。最后祝编程愉快!