WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd)

转载必须注明原文转自C++博客(cppblog),作者毕达哥拉斯半圆,谢谢合作。

上一篇序言得到了很多高人的帮助鼓励和意见,并且给出了一些框架做参考,我这几天拼命消化这些信息,比较了一些架构,最终决定以完整的应用框架为主,并不先开发完整的控件库,可以先采用Windows自带的控件,或者DX控件,@vczh 的Gac控件也是可以用的嘛,@fzy 提出了快速的迭代开发方法,对我是非常好的建议,但是个人水平问题速度很可能快不起来。^_^

Any Way, 千里之行始于足下,就从最简单的窗口应用开始,请各位朋友继续批评指正以及各种吐槽~,哈哈。

Windows是以消息循环为主体,面向过程的软件结构,这是汇编、C语言对OS开发的必然结果,所以开发框架的第一步就构建面向对象的体系结构,其核心使用了CBThook,下面逐步阐述。

先回顾一个典型的Windows App是这样的:
 1 int APIENTRY _tWinMain(HINSTANCE hInstance,
 2                      HINSTANCE hPrevInstance,
 3                      LPTSTR    lpCmdLine,
 4                      int       nCmdShow)
 5 {
 6     UNREFERENCED_PARAMETER(hPrevInstance);
 7     UNREFERENCED_PARAMETER(lpCmdLine);
 8 
 9     MSG msg;
10     HACCEL hAccelTable;
11 
12     // 初始化全局字符串
13     LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
14     LoadString(hInstance, IDC_TESTWIN32, szWindowClass, MAX_LOADSTRING);
15     //注册窗口
16     MyRegisterClass(hInstance, szWindowClass, WndProc);
17 
18     // 执行应用程序初始化:
19     if (!InitInstance (hInstance, nCmdShow))
20     {
21         return FALSE;
22     }
23 
24     while (GetMessage(&msg, NULL, 00))
25     {
26         if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
27         {
28             TranslateMessage(&msg);
29             DispatchMessage(&msg);
30         }
31     }
32 
33     return (int) msg.wParam;
34 }
35

然后是很熟悉的WndProc
 1 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 2 {
 3     int wmId, wmEvent;
 4     PAINTSTRUCT ps;
 5     HDC hdc;
 6     switch (message)
 7     {
 8     case WM_PAINT:
 9         break;
10 
11     //message
12     //message
13     //message
14 
15     case WM_COMMAND:
16         break;
17     case WM_CREATE:
18         return 0;
19     case WM_DESTROY:
20         PostQuitMessage(0);
21         break;
22     default:
23         return DefWindowProc(hWnd, message, wParam, lParam);
24     }
25     return 0;
26 }
27 
上面的MyRegisterClass调用WinApi RegisterClassEx 注册窗口,InitInstance调用CreateWindow创建窗口,而while循环做消息处理,直到应用退出。

作为WarmGUI的第一个类CWindow,应该像MFC::CWnd那样中封装RegisterClass和CreateWindow,这应该是很平凡的事情,照做就可以了,麻烦在于对WndProc封装时,WndProc必须是一个全局的函数,只能作为CWindow的静态函数,而static修饰的函数是没有this指针的,因此在消息循环中,我们只能得到HWND句柄,却不知道这个句柄是哪一个CWindow的实例。

MFC解决的办法是使用CBT钩子,HCBT_CREATEWND类型的CBT钩子可以在窗口创建时指定回调函数,在这个回调函数中,将HWND和CWnd做一个映射,CWnd::Attach函数完成这个功能,这样WndProc就可以用FromHandlePermanent函数从hwnd找到CWnd实例,然后可以在消息处理中调用CWnd::On-Message()什么的了。

WARMGUI::CWindow将采用CBT的方法,下面是山寨过程,应该有更好的实现方法,请各位大佬多指教 ^_^
首先类CWindowManager继承std::map,定义一个HWND-CWindow map,
 1 //the CWindowManager is a map of HWND-CWindow
 2 class CWindow;
 3 //define the HWND-CWindow map
 4 typedef map <HWND, CWindow*> CWindowMap;
 5 typedef pair<HWND, CWindow*> WindowPair;
 6 typedef map <HWND, CWindow*>::iterator WndIterator;
 7 typedef pair<WndIterator, bool> IterBool;
 8 
 9 class CWindowManager : private CWindowMap
10 {
11 private:
12     CWindowManager(void);
13     ~CWindowManager(void);
14 public:
15     bool Add(CWindow* pwnd);  //add a window to map
16     bool Remove(HWND hwnd);     //remove a window by hwnd
17     void Clear();               //remove all items
18     CMyWindow* Find(HWND hwnd); //find the window by hwnd
19 
20 public:
21     //get CWindowManager instance as singleton pattern
22     static CWindowManager * GetInstance();
23 };
24 
其中的代码是很平凡的,调用std::map的函数而已,不做举例了。
用一个全局变量
1 CWindow* gpInitWnd;
记住当前正在创建的CWindow*,并用互斥锁保证不会对其发生读写冲突。事实上只要保证当前线程内不发生读写冲突就可以了,因为CBT是以线程为单位创建的,不过第一个版本的CWindow暂时不考虑,以后再说。
这是CBT-hook代码:
 1 static HHOOK ghook = 0;
 2 
 3 //Create a WH_CBT Hook
 4 static bool HookCrate()
 5 {
 6    HANDLE hThread = GetCurrentThread();
 7    DWORD dwThreadId = GetThreadId(hThread);
 8    if (hThread) {
 9         ghook = SetWindowsHookEx(
10             WH_CBT,
11             MyCBTProc,    //set the CBT proc
12             0,
13             dwThreadId);
14         if (!ghook)
15             return false;
16    }
17 
18    return (0);
19 }
20 
21 //Destroy WH_CBT Hook
22 static void HookDestroy()
23 {
24    if (ghook) {
25         UnhookWindowsHookEx(ghook);
26         ghook = 0;
27    }
28 }
29 
他的回调函数如下:
 1 static LRESULT CALLBACK MyCBTProc(int nCode, WPARAM wParam, LPARAM lParam)
 2 {
 3     if (nCode == HCBT_CREATEWND) {
 4         //GetInitPwnd() return gpInitWnd, the window is creating
 5         CWindow * pwnd = GetInitPwnd();
 6         if (pwnd) {
 7             HWND hwnd = pwnd->GetSafeHwnd();//return pwnd->_hwnd
 8             if (!hwnd) {
 9                 //first time call this proc, the CWindow have no HWND yet,
10                 //Attach() will attach the wParam to pwnd, eg. pwnd->_hwnd = wParam
11                 pwnd->Attach((HWND)wParam);
12                 //call the PreCreateWindow before WM_CREATE
13                 if (!(pwnd->PreCreateWindow((LPCREATESTRUCT)lParam)))
14                     return (1);
15                 return (0);
16             } else {
17                 //second time call this proc, i donw know why for now.
18                 //but this is second chance to decide what is the style
19                 //of the window, or the window should will be created or not,
20                 //if you want create it, return 0, else return non-zero.
21                 return (0);
22             }
23         } else {
24             return (1);
25         }
26     } else
27         return CallNextHookEx(ghook, nCode, wParam, lParam);
28 }
29 
注释中我解释了每个步骤的含义,MyCBTProc返回0则窗口被创建,返回非零则窗口被销毁。尤其要注意的是这个回调会被调用两次,我还不知道为什么,暂时也没时间搞了,请教一下高人指点。

CWindow的Create函数是这样的:
 1 bool CWindow::Create(TCHAR *  szClassName,    //NULL for default class name
 2                             TCHAR * szWindowName,
 3                                        DWORD dwStyle,
 4                                              RECT& rect,
 5                             CWindow * pParentWnd,
 6                                       LPVOID lpParam)
 7 {
 8     if (!pParentWnd)
 9         return false;
10 
11     TCHAR * szcn = (szClassName) ? szClassName : _gszClassName;
12 
13     _hInst = pParentWnd->GetInstance();
14     if (!MyRegisterClass(_hInst, szcn)) {
15         DWORD dwErr = GetLastError();
16         if (dwErr != ERROR_CLASS_ALREADY_EXISTS) //0x00000582
17             return false;
18     }
19 
20     SetInitWnd(this);    //gpInitWnd = this
21     HookCrate();
22     
23     HWND hWnd = CreateWindow(szcn,
24                         szWindowName,
25                         dwStyle,
26                         rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
27                         pParentWnd->GetSafeHwnd(),
28                         NULL,    //menu
29                         _hInst,
30                         lpParam);
31     HookDestroy();
32     UnsetInitWnd();    //gpInitWnd = 0;
33 
34     if (!hWnd)
35         return FALSE;
36 
37     _pParent = pParentWnd;
38 
39     ShowWindow(hWnd, SW_SHOW);
40     UpdateWindow(hWnd);
41 
42     return TRUE;
43 }
过程如下:首先注册窗口类,然后设定gpInitWnd=自己,创建CBTHook,创建窗口,销毁CBTHook,设定gpInitWnd=0,最后显示窗口。

CWindow的WndProc如下:
 1 LRESULT CALLBACK CWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 2 {
 3     //FromHandlePermanent get CWindow* from CWindowManager(a HWND-CWindow* map)
 4     //it call CWindowManager::Find(HWND hwnd) function and return CWindow*
 5     //it is MFC::CWnd Function
 6     CWindow * pwnd = FromHandlePermanent(hWnd);
 7     if (!pwnd) return FirstDefaultWndProc(hWnd, message, wParam, lParam);
 8 
 9     //Message Message and Message
10     LRESULT r = 0;
11     switch (message)
12     {
13         case WM_CREATE                      :
14             r = pwnd->OnCreate((LPCREATESTRUCT)lParam);
15             if (r)
16                 return (r);
17             break;
18         case WM_DESTROY                     :
19             pwnd->OnDestroy();
20             return (0);
21         case WM_PAINT:
22             pwnd->WindowPaint();
23             return (0);
24         case WM_SIZE                        :
25             pwnd->OnSize((UINT)wParam, LOWORD(lParam), HIWORD(lParam));
26             return (0);
27         case WM_DISPLAYCHANGE               :
28             pwnd->WindowPaint();
29             return (0);
30         //message
31         //message
32         //and message
33         default:
34             return DefWindowProc(hWnd, message, wParam, lParam);
35     }
36     return DefWindowProc(hWnd, message, wParam, lParam);
37 }
现在可以对各种消息机制做一个比较好的封装,比如对WM_PAINT调用WindowPaint函数:
 1 void CWindow::WindowPaint()
 2 {
 3     PAINTSTRUCT ps;
 4     HDC hdc = BeginPaint(_hwnd, &ps);;
 5 
 6     OnPaint(hdc, &ps);
 7 
 8     EndPaint(_hwnd, &ps);
 9 }
在这个函数内部实现BeginPaint和EndPaint对,保证我们不会因为忙乱而忘记这对冤家(常见错误之一呀),CWindow和他的子类只要重写OnPaint就可以了。
现在可以把消息循环封装进来了:
 1 void CWindow::RunMessageLoop(HACCEL hAccelTable)
 2 {
 3     MSG msg;
 4 
 5     while (GetMessage(&msg, NULL, 00))
 6     {
 7         if (hAccelTable) {
 8             if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
 9             {
10                 TranslateMessage(&msg);
11                 DispatchMessage(&msg);
12             }
13         } else {
14             TranslateMessage(&msg);
15             DispatchMessage(&msg);
16         }
17     }
18 }

差不多已经做完了,我们看看CWindow的声明:
 1 #define msgfun virtual
 2 namespace WARMGUI {
 3     class WARMGUI_API CWindow
 4     {
 5     public:
 6         CWindow(void);
 7         virtual ~CWindow(void);
 8 
 9         HWND GetSafeHwnd();
10         HINSTANCE GetInstance();
11 
12         void Attach(HWND hwnd);
13         void Dettach();
14         virtual BOOL InitInstance(  HINSTANCE hInstance,
15                                     HINSTANCE hPrevInstance,
16                                     LPTSTR lpCmdLine,
17                                     int nCmdShow,
18                                     TCHAR * szClassName,
19                                     LPTSTR szTitle);
20 
21         virtual void  RunMessageLoop(HACCEL hAccelTable);
22         virtual BOOL PreCreateWindow(LPCREATESTRUCT cs);
23 
24         bool Create(TCHAR *  szClassName,
25                     TCHAR * szWindowName,
26                            DWORD dwStyle,
27                               RECT& rect,
28                     CWindow * pParentWnd,
29                       LPVOID lpParam = 0);
30 
32         void WindowPaint();
33 
34     protected:
35         HWND      _hwnd;
36         HINSTANCE _hInst;
37         CWindow * _pParent;
38 
39     protected:
40         ///OnCreate must return 0 to continue the creation of the CWnd object. If the application returns –1, the window will be destroyed. 
41         msgfun int  OnCreate                    (LPCREATESTRUCT cs);
42         msgfun void OnDestroy                   ();
43         msgfun void OnSize                      (UINT nType, int cx, int cy);
44         //bla bla bla hao duo message
45         //bla bla and bla hai you hao duo message
46 
47     protected:
48         static CWindow * FromHandlePermanent(HWND hWnd);
49         static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
50 
51     private:
52         virtual ATOM MyRegisterClass(HINSTANCE hInstance, TCHAR * szClassName);
53     };
54 }
现在可以从CWindow继承,并重载他的各种消息函数,并在WinMain中调用,一个最简单的应用框架已经有了,显然,他必须是Hello, World:
 1 using namespace WARMGUI;
 2 
 3 class CMyWindow : public CWindow {
 4 public:
 5     CMyWindow();
 6     ~CMyWindow();
 7     
 8     msgfun void OnPaint(LPPAINTSTRUCE ps);x
 9 };
10 
11 void CMyWindow::OnPaint(HDC hdc, LPPAINTSTRUCT /*ps*/)
12 {
13     HBRUSH brush = CreateSolidBrush(RGB(00255));
14     
15     TCHAR hello[] = L"Hello, World!";
16     RECT rect;
17     rect.left = 100, rect.top = 100, rect.right = 200, rect.bottom = 200;
18     COLORREF oldBkg = SetBkColor(hdc, RGB(2552550));
19     COLORREF oldClr = SetTextColor(hdc, RGB(00255));
20     DrawText(hdc, hello, _tcslen(hello), &rect, DT_CENTER);
21     SetTextColor(hdc, oldClr);
22     SetBkColor(hdc, oldBkg);
23     DeleteObject(brush);
24 }
25 
26 int APIENTRY _tWinMain(HINSTANCE hInstance,
27                      HINSTANCE hPrevInstance,
28                      LPTSTR    lpCmdLine,
29                      int       nCmdShow)
30 {
31     CMyWindow mainwnd;
32 
33     if (mainwnd.InitInstance(hInstance, hPrevInstance, lpCmdLine, nCmdShow, szClassName, szTitle)) {
34         mainwnd.RunMessageLoop(0);
35     } else {
36         return (-1);
37     }
38 
39     return (0);
40 }
41 

posted on 2012-09-04 00:25 毕达哥拉斯半圆 阅读(2072) 评论(14)  编辑 收藏 引用

评论

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:21 万连文

1.return路径太多,不容易排错
2.CMyWindow* Find(HWND hwnd);我想是应该加一个const
3.Add和Remove方法的参数不对称,你确信是这样子吗?
4.CWindow * pwnd指针前后都有空格是排版导致的吗?
5.WndIterator为何不用CWindowMap::iterator去定义,名字上有那么大差别,它们表达的含义真的不是一个事物吗?
6.RECT rect = { 100, 100, 200, 200 };不好吗?

因为你说了“代码简洁漂亮”,吸引了我,结果有点失望。Any Way, 千里之行始于足下。  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:22 Richard Wei

对于如何关联HWND和CWnd常用有3种方法:
(1)MFC的Map,就是博主上面用的
(2)把CWnd指针放在CreateWindowEx的最后一个参数里,在回调WM_NCCREATE里从CREATESTRUCT里取出来,再保存CWnd*到SetWindowLong(GWL_USERDATA)中,后面其他消息就直接取出CWnd*调用
(3)ATL的Thunk机制

第一种方法容易理解,但是低效
第二种方法把CreateParams和UserData用掉了,别人没法再用了,通用性不是很好
第三种方法简单高效,但是会涉及到机器码  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:34 溪流

class CWindowManager : private CWindowMap
对这个有异议,把CwindowMap作为CWindowManager的一个成员貌似也可以满足需求?  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:35 毕达哥拉斯半圆

@万连文
多谢帮助啊多谢!按你说的修改修改!  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:37 毕达哥拉斯半圆

@Richard Wei
多谢指教!第三个我还真不会呢,能甩个链接学习一下吗?  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 09:41 毕达哥拉斯半圆

@溪流
当然可以了,我考虑WindowManager这个类除了做map没有其他任何工作,所以就用继承了,如果他还有其他方面的工作,我就把map作为成员组合进来,不知道会不会更好?  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:09 Richard Wei

@毕达哥拉斯半圆
貌似上面的朋友 溪流 写过,http://www.cppblog.com/Streamlet/archive/2010/10/24/131064.aspx  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:13 Richard Wei

高级点的可以参考下这个http://www.cppblog.com/cexer/archive/2008/08/06/58169.html  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:15 毕达哥拉斯半圆

@万连文
按您说的改了改,发现一个问题,
1. return路径太多,不容易排错
这个是从MFC的习惯而来的,当OnMESSAGE函数调用时,如果返回非0值,就从消息循环中返回这个值,如果是0值,则有两种情况,一种是直接返回,另一种是继续执行DefWndProc. 如果保持与MFC的习惯一致,不知道有没有更好的办法,请教一下。

我把宣传中的简洁漂亮什么的去掉了,在高人眼中,我这还是太业余啦!哈哈  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:24 毕达哥拉斯半圆

@Richard Wei
@溪流
刚看到这个,相见恨晚相见恨晚! 我向你们好好学习学习.  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:50 溪流

@毕达哥拉斯半圆
感觉没事不用继承的好……如此,C++中的private继承就想不到什么应用场景了,困惑中……  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 10:50 溪流

@毕达哥拉斯半圆
互相学习,不同的方案各有各的优缺点呗~  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 11:42 毕达哥拉斯半圆

@溪流
额,也不是吧,毕竟WindowManager就是个map,或者改名叫CWndMap就理顺了?  回复  更多评论   

# re: WarmGUI(1) 第一个类,用CBTHook构建CWindow (山寨版MFC::Cwnd) 2012-09-04 11:50 溪流

@毕达哥拉斯半圆
也不顺,我就是觉得private继承一点也没有存在的必要……感觉你的场景就是一个典型的组合场景……  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理


<2011年10月>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

导航

统计

常用链接

留言簿(3)

随笔档案

相册

contact

搜索

最新评论

阅读排行榜

评论排行榜