窗口之父CXWnd的封装
[http://blog.csdn.net/mythma]
在一般的程序中的对话框、按钮、编辑框等都属于窗口。在MFC中,类CWnd作为窗口类的基类,抽象出了它们的共同特征,并由此派生出若干的子类。在普通的windows程序中,我们可以方便的使用MFC为我们封装好的这些类(CDialog, CButton ,CEdit等),而在游戏程序中,由于需要与dx/opengl结合以及效率等原因,因此不直接使用它们,这就需要我们自己来封装。如此之多的控件如何封装能?我们不妨也为所有的窗口、控件抽象一个基类,就叫她CXWnd。那就让我们从 CXWnd开始,一步一步来实现我们真正的UI吧。
我们先撇开DX具体绘制窗口的过程,看看窗口、控件有什么共同的特征,以及如何在CXWnd中来实现。
1、 窗口的大小与位置
窗口是一个矩形的区域,因此用两个变量就可以表示其大小(宽:m_Wdith, 高:m_Height)。
窗口的位置也可以用两个变量表示( X坐标: m_XPos , Y坐标:m_YPos)。但这两个变量表示窗口的绝对坐标(相对于屏幕左上角)还是相对坐标(相对于父窗口的坐上角)呢?
从用户的角度(即创建窗口)看,使用相对坐标方便一点;
从程序的角度(即绘制窗口)看,使用绝对坐标方便一点。
我们封装CXWnd的目的在于使用,因此要从用户的角度出发,即m_XPos和m_YPos表示的是窗口的相对坐标。当绘制窗口的时候,我们需要获得窗口的绝对坐标,因此需要提供一个成员函数来转化——GetAbsolutePos(float& x, float& y)。
(图就不用画了吧,:))
2、 窗口之间的联系
为了更具代表性,想一下(就不画了哦)一个带属性页的对话框的窗口层次,与它相关的窗口有一个父窗口(不包括祖父哦),若干的子窗口(各种控件),以及若干个同级窗口,因此就需要有一个指向其父窗口的指针(CXWnd* m_wndParent),一个指向其子窗口链表的指针(CXWnd* m_wndChild,m_wndChild指向窗口链表的第一个窗口,该窗口z序在子窗口中最小,既显示在最前面),再加上同级窗口间的指针(CXWnd* m_wndNextSibling指向下一个同级窗口;CXWnd* m_wndPreviousSibling指向前一个同级窗口)。这么多窗口之间用这四种指针联系起来,画出来就是一棵窗口关系树。为什么同级窗口需要两个指针呢?在消息循环中就可以找到答案了。
对这些指针,需要哪些操作呢?
除了基本的Get***Wnd() 和Set***Wnd()外,还需要:
添加一个子窗口AddChildWnd(wndChild : CXWnd*)
删除一个子窗口DeleteChildWnd(wndChild : CXWnd*)
删除所有的子窗口DeleteAllChildWnd(),用来释放内存。
获取子窗口个数 GetChildCount()
以上几个都是对单向链表的基本操作。
3、 消息循环
从经典的消息函数WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)那,得到消息后,主窗口需要传递下去,于是CXWnd有了一个相似的函数——PostMessage(UINT msg, WPARAM wParam, LPARAM lParam, void* Data);,其中对消息进行分类处理。
1) 鼠标消息
鼠标消息处理,当然是如下三个(不过在CXWnd中不需要实现它们,只是声明为纯虚函数,因为各个控件的鼠标处理函数都不尽相同)
virtual void OnMouseDown(int Button, int X, int Y) = NULL;
virtual void OnMouseMove(int X, int Y) = NULL;
virtual void OnMouseUp(int Button, int X, int Y) = NULL;
如何得知是哪个窗口得到了鼠标消息呢?那就需要挨个搜索各个窗口,看鼠标当前的坐标(注意是绝对坐标)在哪个窗口上。因此加入函数CursorIntersect(FLOAT X, FLOAT Y)来完成此功能。
鼠标消息是如何在窗口树中传递的呢?范围肯定是从大到小扫描,即先看是否在某窗口上,若在则看是否在该窗口的子窗口上……,终于找到最上的最底层的窗口,于是子窗口先处理鼠标消息,即最上的子窗口具有处理鼠标消息的优先权。因此消息的传递是从上而下,就称之为正序消息传递吧——PostMsgToChildren(……)负责把消息正序传给每一个子窗口。
2) 键盘消息
同鼠标消息一样,键盘消息的处理有如下两个纯虚成员函数:
virtual void OnKeyDown(WPARAM Key, LPARAM Extended) = NULL;
virtual void OnKeyUp(WPARAM Key, LPARAM Extended) = NULL;
键盘消息的处理的特殊之处在于处理它的窗口(控件)需要获得焦点(原因很简单),因此每个窗体需要一个存放其获取焦点的子窗体的指针成员变量(CXWnd* m_wndFocus)和一个判断其是否获得焦点的bool成员变量(bool m_bFocus),从而就有了SetFocusWnd(……)和GetFocus()成员函数。
键盘消息的传递与鼠标消息的传递一样。
3) 窗口绘制消息
窗口的绘制是与具体的窗口对象相关的,因此在CXWnd中不予实现,但窗口绘制的消息是要传递的,那该消息是如何传递的呢?
每个窗口都有自己的z序,最后面的要先绘制,最前面的要后绘制(要不就被当住了),对应与窗口树就是,父窗口要先绘制,然后最下子窗口后绘制,最后是最上的子窗口。因此最下的窗口具有处理窗口绘制消息的优先权。而最上的子窗口排在子窗口链表的最前面,因此该消息需要逆序从后往前传递。因此需要一个专门的传递该消息的成员函数——PostMsgToChildrenReverse(……)。
窗口的z序不是固定的,因此需要一个函数来改变它们的z序 ——MoveToFront(……)。
至此,CXWnd的主体功能已经完成了,看看它有那些成员吧: