Wtl之奇技淫巧篇:一、SDI如何居中显示视图

时间:2021-10-14 12:57:32

Wtl的sdi应用,视图默认铺满框架的客户区。视图通常用modeless对话框,所有的界面元素都拥挤在左上角,这明显很丑陋。我们尝试让视图居中显示,保持原始大小,这是个很典型的问题,看似简单,诸多细节,逐一解决后,对Wtl的理解程度,马上能达到通透的水平。

Wtl比较臭名昭著的一点:没有官方资料。许多问题只能靠分析源代码来解决。本文详细的描述整个解决过程,以及如何快速的阅读、分析Wtl源代码。

一、Google之路:
    本世纪只要有最低智商的人,首先的方式肯定是Google,我们来看看能否通过Google来找到答案。非常遗憾,我们只能找到一篇Mfc领域的文章:

Making the SDI view smaller than the CFrameWnd:

http://www.codeproject.com/Articles/13621/Making-the-SDI-view-smaller-than-the-CFrameWnd

这篇文章讲解了在mfc中,怎样让视图"比框架窗口小",他花费了大量的精力解决闪烁的问题...事后我发现他完全走错了路子:1、闪烁的问题并非因为他所理解的原因;2、他的解决方法,如果将视图设置得更小一些,仍然会闪烁。使用Wtl center view sdi之类的关键词,无论你怎么搜索,相信你也找不到一篇...任何一篇文章,说明如何处理。你不必再尝试,因为我整整花费了两个小时,专注,中途绝对没有转移视线。Google大师没有找到的,你肯定也找不到。偷懒没用的时候,你只有动用终极手段,读代码、理解,然后自己搞定。当然,这种终极手段远没有那么辛劳,只要随时注意大而化之...

二、第一步:创建时居中显示:
    在CMainFrame类的OnCreate函数中:MESSAGE_HANDLER(WM_CREATE, OnCreate)
    m_hWndClient=m_view.Create(m_hWnd);
    这里m_view创建了视图窗口,m_hWndClient保留了视图的句柄。我们在这里居中显示,试试看...
    m_view.CenterWindow(m_hWnd);//当然,这个实在整个窗体居中,我们可以自行写函数处理在客户区居中。为了快速实现,我们暂时忽略细节。
    你会很失望,因为运行之后,这行代码没有发生任何作用。原因何在?CMainFrame的基类,肯定对视图的显示做了处理,让对话框铺满窗体,需要改变其大小。我们先用一个暴力的方法,让基类不知道这是视图:将m_hWndClient=m_view.Create(m_hWnd)修改为m_view.Create(m_hWnd)。再看看...果然,对话框居中显示,很正常。基类明显针对m_hWndClent处理,当m_hWndClient为NULL的时候,代码也肯定做了判断,因此程序能正常运行。

我们当然不能用这种粗暴的方式,程序员一般总要装得绅士一些...那么雅致一点,就意味着大量的工作,我们首先要做的,是找到基类里这部份内容。

三、第二步:CMainFrame的继承关系
  先简单阅读一下向导生成的CMainFrame代码,方式很简单,看继承自哪些类,看消息映射,看函数的名字...除非必要,不要过多的看函数的细节。
  1、CMainFrame类的继承关系:
      在vs2013中,鼠标指向类名,然后右键在快捷菜单中点击"转向定义",很容易查出CMainFrame的继承关系。

class CMainFrame :
public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler template <class T, class TBase = ATL::CWindow, class TWinTraits = ATL::CFrameWinTraits>
class ATL_NO_VTABLE CFrameWindowImpl : public CFrameWindowImplBase< TBase, TWinTraits > template <class TBase = ATL::CWindow, class TWinTraits = ATL::CFrameWinTraits>
class ATL_NO_VTABLE CFrameWindowImplBase : public ATL::CWindowImplBaseT< TBase, TWinTraits >

很清晰,CMainFrame<-CFrameWindowImpl<-CFrameWindowImplBase<-ATL::CWindowImplBaseT,到Atl一层我们暂时不用管了...Wtl的提供的框架基类,包括两层CFrameWindowImpl<-CFrameWindowImplBase,向导创建的CMainFrame和这两个基类,就是Wtl关于框架类的全部源代码。

四、第三步:找到修改视图大小的地方

1、查看CMainFrame和两个基类的消息映射表:
    可以看到CMainframe的映射表最后,链接了CFrameWindowImpl。同样,后者链接了CFrameWindowImplBase。解释一下所谓的链接

CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
    意思是,本类的消息映射表中没有处理的,将在链接的CFrameWindowImpl<CMainFrame>的映射表中继续响应。本类的消息映射表中处理了的,如果bHandled为false,表示没有处理完成,消息仍然会在CFrameWindowImpl<CMainFrame>的映射表中继续响应。如果为bHanded如果为true,则表示消息处理完成,映射表中就不会往下传递,即使链接了CFrameWindowImplBase,且CFrameWindowImplBase的消息映射表有响应函数,它也不会执行。
    可以直观的设置断点,然后单步执行,能看到在消息映射表中从上到下执行的过程。
    因此,消息映射表的顺序是非常重要的,如果CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)放在最前面...这个顺序会倒过来,派生类就不太好覆盖基类的处理。
2、分别查看三个类的消息映射表:

那么,和创建、位置有关的,我们在CFrameWindowImpl中,看到Onsize函数,视图的位置、大小就是在这里改变的:

OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled){
if(wParam != SIZE_MINIMIZED)
{
T* pT = static_cast<T*>(this);
pT->UpdateLayout();
}
bHandled = FALSE;
return ;
}

3、理解模板多态:

这部分代码比较好理解,这就是"模板多态" T* pT = static_cast<T*>(this);这里按继承关系,T是我们传入的CMainFrame类型,将this转化成CMainFrame类的指针,然后执行pT->UpdateLayout();而UpdateLayout()在CFrameWindowImplBase中实现,因此:
    首先,我们的CMainFrame中没有重新定义UpdateLayout,但由于CMainFrame归根揭底是从CFrameWindowImplBase中继承下来的,拥有这个函数
所以这种情形下,执行的是CFrameWindowImplBase的UpdateLayout()
    然后,假设我们在CMainFrame里定义了完全同型的UpdateLayout函数,那么,指向CMainFrame指针的pT,当然只会执行我们定义的UpdateLayout,基类定义的函数就成为摆设。这就是所谓的模板多态...基类不知道派生类会有多少种、各自什么名称,所以继承的时候要将派生类名称传递给基类
这也是这种继承方式的来由:class CMainFrame : public CFrameWindowImpl<CMainFrame>
我们可以看看UpdateLayout的代码,继续动用"转向定义"

4、UpdateLayout代码分析:
    多数情况下,我们在翻看代码的时候,不必深入函数的实现细节。比如UpdateLayout,从名字上可以看到,是更新窗体的布局。函数在父类CFrameWindowImpl中调用,在祖父类CFrameWindowImplBase实现,调用是采用模板多态。由于CMainFrame和父类中都没有覆盖,因此调用的就是祖父类CFrameWindowImplBase中定义的函数。一般情况下这么理解基本就可以了。
    只有在特殊情况下,我们才需要详细分析某个函数,因为我们要弄清它如何改变视图大小、我们也要阻止它。
   下面就是祖父类中的UpdateLayout的代码,注释比较清晰。

void UpdateLayout(BOOL bResizeBars = TRUE)
{
RECT rect = { };
GetClientRect(&rect); //获取整个应用的客户区rect,这只是除去窗口的标题、边框之后,剩下的窗体工作区域 // position bars and offset their dimensions
UpdateBarsPosition(rect, bResizeBars); //该rect减去菜单、工具栏、状态栏所占区域
//此处得到的rect是全部客户区,可以在这个范围内居中显示 //如果不要铺满视图,则注释掉下面的语句,会出现状态栏残痕,这是UpdateBarsPosition要处理的
// resize client window
if(m_hWndClient != NULL) //这里将客户区铺满。如果注释掉,则大小变化的时候,状态栏会出现异常,前面部分区域没有消除
::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE);
} void UpdateBarsPosition(RECT& rect, BOOL bResizeBars = TRUE)
{
// resize toolbar
if(m_hWndToolBar != NULL && ((DWORD)::GetWindowLong(m_hWndToolBar, GWL_STYLE) & WS_VISIBLE))
{
if(bResizeBars != FALSE)
{
::SendMessage(m_hWndToolBar, WM_SIZE, , ); //相当于调用函数,消息执行完后才执行下一条,是同步代码,可以理解为调用某个函数
::InvalidateRect(m_hWndToolBar, NULL, TRUE);
}
RECT rectTB = { };
::GetWindowRect(m_hWndToolBar, &rectTB);
rect.top += rectTB.bottom - rectTB.top;
} // resize status bar
if(m_hWndStatusBar != NULL && ((DWORD)::GetWindowLong(m_hWndStatusBar, GWL_STYLE) & WS_VISIBLE))
{ //这里没让原来区域失效,因为铺满地窗体将覆盖它,但我们若没有铺满窗体,则这里必须同样失效。
if(bResizeBars != FALSE)
::SendMessage(m_hWndStatusBar, WM_SIZE, , ); RECT rectSB = { };
::GetWindowRect(m_hWndStatusBar, &rectSB);
rect.bottom -= rectSB.bottom - rectSB.top;
}
}

五、解决方案一:在CMainFrame中覆盖UpdateLayout
    我们将代码拷贝到CMainFrame,注释掉改变视图大小的几行语句

void UpdateLayout(BOOL bResizeBars = TRUE)
{
RECT rect = { };
GetClientRect(&rect); //获取整个应用的客户区rect,这只是除去窗口的标题、边框之后,剩下的窗体工作区域 // position bars and offset their dimensions
UpdateBarsPosition(rect, bResizeBars); //该rect减去菜单、工具栏、状态栏所占区域
//此处得到的rect是全部客户区,可以在这个范围内居中显示 //如果不要铺满视图,则注释掉下面的语句,会出现状态栏残痕,这是UpdateBarsPosition要处理的
// resize client window
//if(m_hWndClient != NULL) //这里将客户区铺满。如果注释掉,则大小变化的时候,状态栏会出现异常,前面部分区域没有消除
// ::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,
// rect.right - rect.left, rect.bottom - rect.top,
// SWP_NOZORDER | SWP_NOACTIVATE);
}

重新运行,显然,我们看到视图居中显示了。效果如下:

Wtl之奇技淫巧篇:一、SDI如何居中显示视图

但遗憾的是,当我们将应用最大化,或者改变大小的时候,状态栏出现了残留痕迹,视图原来的位置也没有擦除,仍然保留残痕:

Wtl之奇技淫巧篇:一、SDI如何居中显示视图

当然,改变大小后,视图保持了以前在框架中的位置,没有居中,这是因为我们没有在onsize中处理。我们先解决简单的,为CMainFrame响应WM_ONSIZE消息,在消息映射表加上MESSAGE_HANDLER(WM_SIZE, OnSize),然后消息处理函数:

LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if (m_view.IsWindow())
{
m_view.CenterWindow(m_hWnd);
}
bHandled = False;
return ;
}

这里设置bHandled = False;这样基类的onsize会执行UpdateLayout,这里就不用多此一举。至于残留痕迹问题,我们继续看代码。

六、第四步:找出主窗体大小改变时,残痕产生的原因
    上面说的残痕,很明显,整个界面都紊乱了,我们首先要找到原因。按照上面同样的方式,我们可以看到,父类只有个OnSize函数,祖父类消息映射中则处理了两个:擦除背景的消息,是return 1,也就是说,如果存在视图,默认的背景擦除就不调用了,这里直接处理。

这等于屏蔽了背景擦除或者重画。

LRESULT OnEraseBackground(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
{
if(m_hWndClient != NULL) // view will paint itself instead就是说这由视图来做
return ; bHandled = FALSE;
return ;
}

为什么要屏蔽?因为,视图铺满客户区的情形下...根本无需擦除背景。同时,前面在UpdateLayout中,状态栏没有发消息重画,也是同样的原因。所以,这里可以看出,Wtl的Frame类设计的基础,就是视图铺满客户区。
    题外话,祖父类中还处理了一个消息OnSetFocus,即程序启动之后,视图即获得焦点

LRESULT OnSetFocus(UINT, WPARAM, LPARAM, BOOL& bHandled)
{
if(m_hWndClient != NULL)
::SetFocus(m_hWndClient); bHandled = FALSE;
return ;
}

七、解决方案之二:解决背景擦除问题,让祖父类的OnEraseBackground失去作用
    我们为CMainFrame响应WM_ERASEBKGND消息

MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
LRESULT OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
return DefWindowProc(uMsg, wParam, lParam);
}

这里的代码很简单,即对于WM_ERASEBKGND消息,调用默认的消息处理函数。注意这是一个宏,调用的也不是win32 api而是基类的函数。由于bHandle默认为true,因此该消息对Chain的父类、祖父类不再可见。这样,重新运行,不错,现在视图能够正常的居中,无论最大化、改变主窗体大小,都能正常居中,不再有残影。到此为止,我们的居中小视图事业...算是圆满解决?

No,接下来还剩下一个大的问题,闪烁!
    我们最大化主窗体,或者改变大小,都能看到明显的闪烁。细节看出态度,态度决定一切...我们,怎么能闪烁不已呢?

八、第五步:找出闪烁产生的原因
    首先,闪烁之Google大计。很痛苦,很痛苦,当百度活得滋润的时候,你完全是在受它的折磨和蹂躏。当我终于发现bing也能找到不少国外的资料时,你发现它确实很弱智。最终,你还得使用Google,无论你用什么办法。一时手痒,搜索了一下闪烁,哗啦啦,铺天盖地,无论是Win32、mfc、Wtl、Duilib、Qt...闪烁无所不在。
    同时,解决的方法也无奇不有,双缓冲?自行处理擦除?局部擦除?尽量避免重画?一个悲哀的结论是:即使微软自己的程序,闪烁也几乎无所不在。
    我悄悄地尝试了各种方式...对不起,没有一种能够消除刚才的闪烁...上面提到的哪篇Mfc居中显示视图的文章,用Wtl原样实现,闪烁还是很明显,毫无变化。虽然奇怪,后来发现,他的视图设置得比较大,几乎铺满了框架...而我的视图很小,遮挡不住啊。
    将这位老先生的视图改小,我那个...去!这位费劲九牛二虎之力,致力于消除闪烁,号称比微软普通软件都要不闪亮的兄弟...这视图仍然是闪烁滴,你说这事儿闹的。

既然各种方法没用,我们反过来思考,多数闪烁现象,是因为窗体控件太多,在屏幕不同刷新周期显示,各种法门大体从快速、一次显示角度出发,或者减少擦除出发。但我们这里遇到的问题,整个框架,只有一个窗体,也就是我们的视图,没道理闪烁。

我再仔细观察了一下,闪烁的现象:最大化时,显示视图的同时,视图原来的位置跳动了一下,看到原位置视图、视图内的文字都跳动一下然后消失,再正常的显示居中的视图本身。这说明什么呢?月黑风高,一道闪电从窗外怯生生的探进头来...Onsize中居中,此时在正确的位置显示视图。但居中之前,很明显在原来的位置已经显示了视图,只是瞬间被擦除。
    瞬间,自动擦除背景、原位置显示再瞬间消失...这样怎可能不闪烁?

九、解决方案之三:消除闪烁
    那么...当窗体大小变化时,我们先隐藏之...OnSize先令其居中,然后显示之...问题岂非解决?

//Hide it here
LRESULT OnGetMinMaxInfo(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if (m_view.IsWindow())
{
m_view.ShowWindow(SW_HIDE);
}
bHandled = false;
return TRUE;
} //center it here
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if (m_view.IsWindow())
{
m_view.CenterWindow(m_hWnd);
m_view.ShowWindow(SW_SHOW);
}
bHandled = False;
return ;
} //fix position changed here
//
LRESULT OnWindowPosChanged(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
if (m_view.IsWindow() && !m_view.IsWindowVisible())
{
m_view.ShowWindow(SW_SHOW);
}
bHandled = False;
return ;
}

解决了这个问题后,很悲哀的看到了一篇:duilib启动程序时会闪一下(一闪而过)的解决方案。这也说明,但凡Win32编程,道理都是互通的。
这个几乎是相同的问题...但这哥们只是处理了创建之初,没有遇到主窗体大小改变的情形。
    所以,设计器中创建modeless对话框的时候,默认visble属性为false,不是没有道理的,我们手工的Show...可以避免这类启动时的问题。最后的效果,在原来居中的情形下,主窗体变大后,正常的居中显示视图:

Wtl之奇技淫巧篇:一、SDI如何居中显示视图

十、留下作业?
    我们当然不是打算仅仅使视图居中,我们还需要切换不同的视图,这些视图基本上是modeless对话框,这就是简单的界面框架,让wtl能够实现主要流行界面。那么接下来我们还剩下哪些工作?
    1、将m_view改为指针?这意味着视图必需自删除
    2、切换视图,这需要有通用的方式处理PreTranslateMessage
    3、视图能够在框架中指定位置,并随大小移动?
        这需要提供相对位置、相对大小的函数,或者在CMainFrame中使用CDialogResize
    4、最最重要的是:将上述内容写成嵌入类?
        这绝对是必要的...但大家能够看到,我一向遵循从具体到抽象、有必要才抽象的次序。尤其是界面相关的编程中,先实现效果,再抽象就是件很简 单的事情。嵌入类是什么?前面我们看了两个基类的代码,嵌入类就很明显...使用模板多态的类。我们CMainFrame继承自该类,并将消息映射Chain到该类...上面出现的大量重载函数、消息映射、消息处理函数,就不用再写了。当然,要保证消息映射表中,嵌入类在 CFrameWindowImpl之前。

十一、有Wtl的书籍吗?
    我还真没找到,即使可怜的如Mfc程序员的Wtl指南、Wtl指导教程之类,都存在版本严重滞后的问题,也存在知识陈旧的问题,Win98干我们甚是?
C++ 11之后,Wtl也成为较好的UI选择之一,模板编程的思维比较接近。
    如果真没有...或许空闲的时候,我准备就Wtl最新的版本,结合C++ 11,结合一个具体项目的一部分...用本文的风格来完成?考虑到Wtl的冷僻程度,这或许是个注定亏本的买卖。没有契机...很遗憾,Wtl的小世界,暂时,仍然要生活在碎片化资料、不同版本资料之中,与Google相伴。

本文作者:毕丹军(11084184@qq.com),转载请略礼貌些,保留出处。