窗口封装

我们又知道,Win32 API是面向过程的(虽然可以说Win是一个OO系统),而我们希望可以利用Win32 API进行快乐的OOP(不需要重复上面的逻辑),于是,我们需要包装API,封装Windows窗口。从上面的逻辑可以看出,要封装窗口主要需解决怎样封装窗口消息处理机制。由于交给Windows的标准窗口过程是全局/静态的,此时,将面临两个问题:

1.怎么知道将窗口过程中的消息转发给哪个封装好的窗口类实例?(也就是HWND到对应窗口类实例的转换)

2.假设第1个问题解决了,怎样将消息传递给相应窗口类的实例?

重点是解决第1个问题,下面来看MFCATL分别是怎么来解决这两个问题。

一、MFC窗口消息封装机制

我们通过一个手工产生(not by wizzard)的最简单的MFC程序(基于CWinAppCWnd的两个类,以省掉不必要的代码和麻烦)开始调试分析。

01

BOOL CMFCApp::InitInstance()

02

{

03

    CMFCWin* pMainWnd = new CMFCWin;

04

    if (NULL == pMainWnd)

05

        return FALSE;

06

07

    CString strWndClass;

08

    strWndClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW,

09

        ::LoadCursor(NULL, IDC_ARROW), HBRUSH(COLOR_WINDOW+1), NULL);

10

11

    HMENU hMenu = ::LoadMenu(NULL, MAKEINTRESOURCE(IDR_MAINFRAME));

12

    // Bruce:开始创建主窗口

13

    if (!pMainWnd->CreateEx(WS_EX_APPWINDOW, strWndClass,

14

        _T("MFCBased without Wizzard"), WS_OVERLAPPEDWINDOW,

15

        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

16

        NULL, hMenu, NULL))

17

    {

18

        AfxMessageBox(_T("CreateEx Failed!"));

19

        return FALSE;

20

    }

21

22

    m_pMainWnd = pMainWnd;

23

    pMainWnd->ShowWindow(m_nCmdShow);

24

    pMainWnd->UpdateWindow();

25

26

    return TRUE;

进入pMainWnd->CreateEx

01

BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,

02

    LPCTSTR lpszWindowName, DWORD dwStyle,

03

    int x, int y, int nWidth, int nHeight,

04

    HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)

05

{

06

    // . . .

07

    // BruceHook? 好戏来了。

08

    AfxHookWindowCreate(this);

09

    HWND hWnd = ::AfxCtxCreateWindowEx(cs.dwExStyle, cs.lpszClass,

10

            cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,

11

            cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);

12

13

}

AfxHookWindowCreate,看上去是装了个Hook,进去看。

01

void AFXAPI AfxHookWindowCreate(CWnd* pWnd)

02

{

03

    // Bruce:这里取的什么数据?

04

    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();

05

    if (pThreadState->m_pWndInit == pWnd)

06

        return;

07

08

    if (pThreadState->m_hHookOldCbtFilter == NULL)

09

    {

10

        // Bruce:果然,这里装了个WH_CBT钩子

11

        pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,

12

            _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());

13

        if (pThreadState->m_hHookOldCbtFilter == NULL)

14

            AfxThrowMemoryException();

15

    }

16

    // . . .

17

    pThreadState->m_pWndInit = pWnd;

18

}

先来说_afxThreadState.GetData()_afxThreadState是一个全局CThreadLocal模板对象,是对TLS的封装,记录了线程相关的私有数据,_afxThreadState后面还会看到。接下来我们看到安装了一个WH_CBT钩子,_AfxCbtFilterHookhook procedure,用来监视窗口的激活,创建,销毁等消息,也就是在窗口被激活,创建,销毁的时候系统会先调用这个函数。下面来看_AfxCbtFilterHook做了些什么。

01

LRESULT CALLBACK

02

_AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)

03

{

04

    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();

05

    if (code != HCBT_CREATEWND) // Bruce:只关心窗口创建消息,其他跳过

06

    {

07

        // wait for HCBT_CREATEWND just pass others on...

08

        return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,

09

            wParam, lParam);

10

    }

11

    // . . .

12

    LPCREATESTRUCT lpcs = ((LPCBT_CREATEWND)lParam)->lpcs;

13

    // . . .

14

    HWND hWnd = (HWND)wParam;

15

    WNDPROC oldWndProc;

16

    if (pWndInit != NULL)

17

    {

18

        AFX_MANAGE_STATE(pWndInit->m_pModuleState);

19

20

        // Bruce:检查该窗口映射是否存在?后面说明这个函数

21

        ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);

22

23

        // Bruce:添加窗口映射(HWND  CWnd

24

        pWndInit->Attach(hWnd);

25

        // allow other subclassing to occur first

26

        pWndInit->PreSubclassWindow();

27

28

        WNDPROC *pOldWndProc = pWndInit->GetSuperWndProcAddr();

29

        ASSERT(pOldWndProc != NULL);

30

31

        // Brucesubclass,重新设置窗口过程

32

        WNDPROC afxWndProc = AfxGetAfxWndProc();

33

        oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,

34

                (DWORD_PTR)afxWndProc);

35

        ASSERT(oldWndProc != NULL);

36

        if (oldWndProc != afxWndProc)

37

            *pOldWndProc = oldWndProc;

38

39

        pThreadState->m_pWndInit = NULL;

40

    }

41

    // . . .

42

}

这里可以看到,钩子函数仅监视窗口的创建,通过FromHandlePermanent/Attach完成了HWNDCWnd的映射,并重新设置了窗口过程。这个窗口过程视链接MFC的方式不同要么是AfxWndProcBaseAfxWndProc,即Windows想要的标准窗口过程。

01

CWnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd)

02

{

03

    CHandleMap* pMap = afxMapHWND();

04

    CWnd* pWnd = NULL;

05

    if (pMap != NULL)

06

    {

07

        // only look in the permanent map - does no allocations

08

        pWnd = (CWnd*)pMap->LookupPermanent(hWnd);

09

        ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd);

10

    }

11

    return pWnd;

12

}

13

14

BOOL CWnd::Attach(HWND hWndNew)

15

{

16

    ASSERT(m_hWnd == NULL);     // only attach once, detach on destroy

17

    ASSERT(FromHandlePermanent(hWndNew) == NULL);

18

        // must not already be in permanent map

19

20

    if (hWndNew == NULL)

21

        return FALSE;

22

23

    CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist

24

    ASSERT(pMap != NULL);

25

26

    pMap->SetPermanent(m_hWnd = hWndNew, this);

27

    // . . .

28

}

通过查看FromHandlePermanent/Attach的实现,可以发现HWNDCWnd的映射建立在Map的机制上,另外,查看afxMapHWND()的实现,可以发现,这个映射关系也保存在线程的TLS中,因此有了MFC的一个先天限制,不能把一个MFC对象从某线程手上交给另一线程,也不能够在线程之间传递MFC对象指针(了解更多可以参考《Win32多线程程序设计》MFC多线程一章)。下面再看看窗口过程。

01

WNDPROC AFXAPI AfxGetAfxWndProc()

02

{

03

#ifdef _AFXDLL // BruceUse MFC in a Shared DLL

04

    return AfxGetModuleState()->m_pfnAfxWndProc; // Bruce:也就是AfxWndProcBase

05

#else // BruceUse MFC in a Static Library

06

    return &AfxWndProc;

07

#endif

08

}

09

10

LRESULT CALLBACK

11

AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

12

{

13

    AFX_MANAGE_STATE(_afxBaseModuleState.GetData());

14

    return AfxWndProc(hWnd, nMsg, wParam, lParam);

15

}

16

17

LRESULT CALLBACK

18

AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

19

{

20

    // special message which identifies the window as using AfxWndProc

21

    if (nMsg == WM_QUERYAFXWNDPROC)

22

        return 1;

23

24

    // Bruce:查找Map,找到HWND对应的CWnd实例

25

    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);

26

    ASSERT(pWnd != NULL);

27

    ASSERT(pWnd==NULL || pWnd->m_hWnd == hWnd);

28

    if (pWnd == NULL || pWnd->m_hWnd != hWnd)

29

        return ::DefWindowProc(hWnd, nMsg, wParam, lParam);

30

    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); // Bruce:有了CWnd,可以调用窗口实例对应的窗口过程

31

}

32

33

LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,

34

    WPARAM wParam = 0, LPARAM lParam = 0)

35

{

36

    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();

37

    MSG oldState = pThreadState->m_lastSentMsg; // save for nesting

38

    pThreadState->m_lastSentMsg.hwnd = hWnd;

39

    pThreadState->m_lastSentMsg.message = nMsg;

40

    pThreadState->m_lastSentMsg.wParam = wParam;

41

    pThreadState->m_lastSentMsg.lParam = lParam;

42

43

    #ifdef _DEBUG

44

        _AfxTraceMsg(_T("WndProc"), &pThreadState->m_lastSentMsg);

45

    #endif

46

47

    // Catch exceptions thrown outside the scope of a callback

48

    // in debug builds and warn the user.

49

    LRESULT lResult;

50

    TRY

51

    {

52

        // . . .

53

        // Bruce:“终于找到属于我的窗口过程了!”,WindowProc是一个virtual function,注意与AfxWndProc的区别

54

        lResult = pWnd->WindowProc(nMsg, wParam, lParam);

55

    }

56

// . . .

57

}

到此,我们已经完整的看到了MFC封装窗口消息机制的过程,简单总结一下:

通过Hook技术安装一个WH_CBT钩子,在窗口创建时(必须在这个时候,这样才不会有漏网的窗口消息)建立HWND到窗口类实例(如CWnd实例)的映射关系,并保存在创建该窗口的线程的TLS中,然后在需要的时候从TLS中取出数据查找关系MapMFC就是通过这样的方式解决了开始提出的第1个问题,而第2个问题则通过虚函数来实现。

二、ATL窗口消息封装机制

这里也手工写了一个简单的ATL窗口程序,基于CWindowImpl窗口类。开始调试。

01

int APIENTRY _tWinMain(

02

    HINSTANCE hInst,

03

    HINSTANCE /*hInstPrev*/,

04

    LPTSTR pszCmdLine,

05

    int nCmdShow)

06

{

07

    _Module.Init(0, hInst);

08

09

    HMENU hMenu = ::LoadMenu(_Module.GetResourceInstance(),

10

        MAKEINTRESOURCE(IDR_MENU));

11

12

    CATLWin mainWnd;

13

    // Bruce:开始创建主窗口

14

    if (!mainWnd.Create(NULL, CWindow::rcDefault,

15

        _T("ATLBased without Wizzard"), 0, 0, (UINT)hMenu))

16

    {

17

        ::MessageBox(NULL, _T("Create Failed"), _T("ATLBased Error"), MB_OK);

18

        return -1;

19

    }

20

21

    mainWnd.CenterWindow();

22

    mainWnd.ShowWindow(nCmdShow);

23

    mainWnd.UpdateWindow();

24

25

    MSG msg;

26

    while (::GetMessage(&msg, NULL, 0, 0))

27

    {

28

        ::TranslateMessage(&msg);

29

        ::DispatchMessage(&msg);

30

    }

31

32

    _Module.Term();

33

    return msg.wParam;

34

}

依旧是从创建窗口开始,看看CWindowImpl::Create做了什么。

01

HWND Create(HWND hWndParent, _U_RECT rect = NULL,

02

    LPCTSTR szWindowName = NULL,

03

    DWORD dwStyle = 0, DWORD dwExStyle = 0,

04

    _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)

05

{

06

    if (T::GetWndClassInfo().m_lpszOrigName == NULL)

07

        T::GetWndClassInfo().m_lpszOrigName = GetWndClassName();

08

    ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);

09

10

    dwStyle = T::GetWndStyle(dwStyle);

11

    dwExStyle = T::GetWndExStyle(dwExStyle);

12

13

    // . . .

14

15

    return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect,

16

        szWindowName, dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam);

17

}

函数一开始通过GetWndClassInfo获取默认的WndClass并注册,GetWndClassInfo通过宏DECLARE_WND_CLASS实现,展开可以发现默认的窗口过程是StartWindowProc,继续看CWindowImplBaseT< TBase,TWinTraits >::Create

01

template

02

HWND CWindowImplBaseT< TBase, TWinTraits >::Create(

03

    HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,

04

    DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID,

05

    ATOM atom, LPVOID lpCreateParam)

06

{

07

    // . . .

08

    // Allocate the thunk structure here, where we can fail gracefully.

09

    result = m_thunk.Init(NULL,NULL);

10

    // . . .

11

    _AtlWinModule.AddCreateWndData(&m_thunk.cd, this);

12

    // . . .

13

14

    HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom),

15

        szWindowName,dwStyle, rect.m_lpRect->left, rect.m_lpRect->top,

16

        rect.m_lpRect->right - rect.m_lpRect->left,

17

        rect.m_lpRect->bottom - rect.m_lpRect->top,

18

        hWndParent, MenuOrID.m_hMenu,

19

        _AtlBaseModule.GetModuleInstance(), lpCreateParam);

20

21

    ATLASSUME(m_hWnd == hWnd);

22

23

    return hWnd;

24

}

我们看到这里开始使用了thunk成员变量m_thunkthunk一般可以理解为转换,在这里是一组ASM指令。每个CWindowImpl实例有自己的m_thunk,这里暂不管thunk的技术实现,只需留意窗口this指针和ThreadId被安全的放进一个_AtlCreateWndData全局链表结构中。下面就来看窗口过程StartWindowProc

01

template

02

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::

03

StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

04

{

05

    // Bruce:通过当前的ThreadId遍历_AtlCreateWndData链表,取得先前保存的窗口this指针

06

    CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();

07

    // . . .

08

    // Bruce:这里保存HWND以作后用,thunking过程中将被覆盖掉

09

    pThis->m_hWnd = hWnd;

10

11

    // Bruce:将静态窗口过程(如果GetWindowProc没被改写,那么就是CWindowImplBaseT::WindowProc)和this指针初始化到thunk中, m_thunk.Init之后,hWnd被替换成this指针

12

    pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);

13

    // Bruce:这里返回的其实是一个_stdcallthunk结构体的首地址,后面会提及

14

    WNDPROC pProc = pThis->m_thunk.GetWNDPROC();

15

    // . . .

16

    // Bruce:进入静态窗口过程

17

    return pProc(hWnd, uMsg, wParam, lParam);

18

}

继续看CWindowImplBaseT::WindowProc

01

template

02

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(

03

    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

04

{

05

    // BrucehWnd变回thisthunk的功劳)

06

    CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;

07

    // . . .

08

09

    // Bruce:转换过程到这里结束,找到thisvirtual成员函数处理窗口消息

10

    BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);

11

    // . . .

12

}

到这里,大致看完了ATL为了解决第1个问题进行的转换过程,总结起来就是:通过thunk的作用,对hWnd做缓存后将Windows提供的hWnd替换成this指针,接着thunk把整个调用栈传递给真正的窗口过程(如果GetWindowProc没被改写,也就是CWindowImplBaseT::WindowProc)。

最后再来看有趣的thunk是怎么实现的。

01

class CWndProcThunk

02

{

03

public:

04

    _AtlCreateWndData cd;

05

    CStdCallThunk thunk;

06

07

    BOOL Init(WNDPROC proc, void* pThis)

08

    {

09

        return thunk.Init((DWORD_PTR)proc, pThis);

10

    }

11

    WNDPROC GetWNDPROC()

12

    {

13

        return (WNDPROC)thunk.GetCodeAddress();

14

    }

15

};

不管内存字节对齐方式,这里假设CStdCallThunk_stdcallthunk,在_M_IX86平台下。

01

#if defined(_M_IX86)

02

PVOID __stdcall __AllocStdCallThunk(VOID);

03

VOID  __stdcall __FreeStdCallThunk(PVOID);

04

05

#pragma pack(push,1)    // Bruce1字节内存对齐方式

06

struct _stdcallthunk

07

{

08

    DWORD   m_mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)

09

    DWORD   m_this;         //

10

    BYTE    m_jmp;          // jmp WndProc

11

    DWORD   m_relproc;      // relative jmp

12

    BOOL Init(DWORD_PTR proc, void* pThis)

13

    {

14

        m_mov = 0x042444C7;  //C7 44 24 0C

15

        m_this = PtrToUlong(pThis);

16

        m_jmp = 0xe9;

17

        m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));

18

        // write block from data cache and

19

        //  flush from instruction cache

20

        FlushInstructionCache(GetCurrentProcess(), this,sizeof(_stdcallthunk));

21

        return TRUE;

22

    }

23

    //some thunks will dynamically allocate the memory for the code

24

    void* GetCodeAddress()

25

    {

26

        return this;

27

    }

28

    void* operator new(size_t)

29

    {

30

        return __AllocStdCallThunk();

31

    }

32

    void operator delete(void* pThunk)

33

    {

34

        __FreeStdCallThunk(pThunk);

35

    }

36

};

37

#pragma pack(pop)

三、其他实现

除了MFCATL中使用的手法,还有另外一种简单的封装方式。且看代码,主要是窗口过程。

01

LRESULT CALLBACK XWindow::WndProc(HWND hWnd, UINT uMsg,

02

    WPARAM wParam, LPARAM lParam)

03

{

04

    XWindow* pThis = NULL;

05

    if (WM_NCCREATE == uMsg)

06

    {

07

        assert(!::IsBadReadPtr((void*)lParam, sizeof(CREATESTRUCT)));

08

        LPCREATESTRUCT lpcs = reinterpret_cast(lParam);

09

        pThis = static_cast(lpcs->lpCreateParams);

10

        pThis->m_hWnd = hWnd;

11

12

        assert(!::IsBadReadPtr(pThis, sizeof(XWindow)));

13

        ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(pThis));

14

    }

15

    else

16

        pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA));

17

18

    if (pThis)

19

        return pThis->MsgProc(hWnd, uMsg, wParam, lParam);

20

    else

21

        return DefWindowProc(hWnd, uMsg, wParam, lParam);

22

}

这种方式通过在收到 WM_NCCREATE消息时,将this指针保存在lParam参数中,lParam事实上是一个LPCREATESTRUCT结构指针。成功保存后,WM_NCCREATE之后的消息就可以取出this指针,调用对应的消息处理函数。一些UI库使用的是这种实现方式。

到此,分析结束,由于MFCATL的设计目标不同(MFC在于简单易用,ATL则是短小精悍),MFCATL采用了不同的实现手法,且各有优劣。MFC采用全局映射表的方式,由于需要查找,损耗一定的时间,并随着窗口数的增多而增长。ATL在效率上占优势,但增加了一些复杂性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值