ATL技术内幕 第五部分 (完结篇)

来源:互联网 发布:如何在淘宝网上卖东西 编辑:程序博客网 时间:2024/05/21 19:36

前言:这一系列博客翻译自 The Code Project 上的文章,作者是Zeeshan amjad。

题目:ATL Under the Hood - Part 4

原文链接:http://www.codeproject.com/Articles/3102/ATL-Under-the-Hood-Part-5

介绍

很多人认为ATL仅仅是用来创造COM组件。实际上你可以利用ATL的窗口类,来创建流畅的基于窗口的应用程序。虽然我们可以将你基于MFC的工程转化为ATL,但ATL中包含的UI组件非常少,所以,你需要写大量的代码。比如,ATL中没有文档/试图结构,所以,你想在ATL中用的话,就必须自己实现它了。这部分中,我们来探讨窗口类。同时也探讨ATL窗口类技术。WTLwindow template library)到现在还没有被微软支持,实际上是向守卫制作图形应用方向走了一步。WTL就是基于ATL的窗口类构建的。

在讨论任何基于ATL的程序之前,我们首先看看经典的 “hello world”程序。这个程序完全利用windows SDK编写。大家应该都比较熟悉:

程序 66

#include <windows.h>LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,                    LPSTR lpCmdLine,  int nCmdShow){    char szAppName[] = "Hello world";    HWND hWnd;    MSG msg;    WNDCLASS wnd;        wnd.cbClsExtra    = NULL;    wnd.cbWndExtra    = NULL;    wnd.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);    wnd.hCursor    = LoadCursor(NULL, IDC_ARROW);    wnd.hIcon        = LoadIcon(NULL, IDI_APPLICATION);    wnd.hInstance    = hInstance;    wnd.lpfnWndProc    = WndProc;    wnd.lpszClassName    = szAppName;    wnd.lpszMenuName    = NULL;    wnd.style        = CS_HREDRAW | CS_VREDRAW;        if (!RegisterClass(&wnd))    {        MessageBox(NULL, "Can not register window class", "Error",                    MB_OK | MB_ICONINFORMATION);        return -1;    }        hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);        ShowWindow(hWnd, nCmdShow);    UpdateWindow(hWnd);        while (GetMessage(&msg, NULL, 0, 0))    {        DispatchMessage(&msg);    }        return msg.wParam;}LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){    HDC hDC;    PAINTSTRUCT ps;    RECT rect;    switch (uMsg)    {    case WM_PAINT:        hDC = BeginPaint(hWnd, &ps);        GetClientRect(hWnd, &rect);        DrawText(hDC, "Hello world", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);        EndPaint(hWnd, &ps);        break;    case WM_DESTROY:        PostQuitMessage(0);        break;    }        return DefWindowProc(hWnd, uMsg, wParam, lParam);}

程序没有什么特别的。只是在其窗口中心显示了hello world字样。

ATL是面向物件的库,意味着你的程序用类来工作。我们来尝试自己实现一些小的类,来简化我们的工作。创造类的标准是什么?换句话说,我们应该创造几个类,他们之间有什么关系,他们分别有什么属性和方法。我这里不讨论面向对象理论和创建高效库的过程。我只是对API进行分类,将相关的API放在一个类中。我将处理窗口的API放入在一个类中,以便在其他窗口中可以重复使用,比如字体,文件,菜单等。所以我做了一个小的类,将所有第一个参数为HWNDAPI放入其中。这个类除了对windows API进行一层包装外,什么也没有做。我取名为ZWindow,你可以改为其他名字:

class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }};

我只放入了目前我们会用到的API。你可以添加任何想添加的API。这个类唯一的有点是,你不再需要为window API传递HWND这个参数了。这个类将自动传递。

目前为止,没什么特别。但我们的回调函数该如何处理呢?别忘了,这个函数的第一个参数也是HWND,所以,按照规则,也应该是这个类的一个成员。现在,我为该类加入回调函数:

class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }    LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)    {        switch (uMsg)        {        case WM_DESTROY:            PostQuitMessage(0);            break;        }        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);    }};

初始化WNDCLASS结果时,你需要将回调函数的地址传递给它,在我们创建了ZWindow类的实例后,你可以这样传递:

    ZWindow zwnd;    WNDCLASS wnd;    wnd.lpfnWndProc = wnd.WndProc;

当程序编译时,得到如下错误:

cannot convert from 'long (__stdcall ZWindow::*)(struct HWND__ *,   unsigned int,unsigned int,long)' to 'long (__stdcall *)(struct HWND__ *,   unsigned int, unsigned int,long)

这是因为,你不可以将一个成员函数作为回调函数。因为编译器会为每一个成员函数自动传递一个参数,这个参数就是this指针,换句话说,就是这个类的实例自己。所以,当你为成员函数传递n个参数时,编译器将传递n+1个,附加的那个就是this指针。编译器显示的错误信息还表明,编译器无法将成员函数转化为全局函数。

如果我们想用成员函数作为回调函数,我们该怎么办呢?如果我们能通过某种方式告诉编译器不要传递this指针,那就可以了。c++中,如果我们将成员函数申明为静态函数,编译器就不会传递this指针了。这也是静态成员函数和非静态成员函数的区别。

所以,我们将WndProc作为ZWindowd的静态成员函数。这个技术在成员函数做线程函数时也用到了。

看下面的例子:

程序 67

#include <windows.h>class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }    static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)    {        switch (uMsg)        {        case WM_DESTROY:            PostQuitMessage(0);            break;        }        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);    }};int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,                     int nCmdShow){    char szAppName[] = "Hello world";    HWND hWnd;    MSG msg;    WNDCLASS wnd;    ZWindow zwnd;        wnd.cbClsExtra    = NULL;    wnd.cbWndExtra    = NULL;    wnd.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);    wnd.hCursor        = LoadCursor(NULL, IDC_ARROW);    wnd.hIcon        = LoadIcon(NULL, IDI_APPLICATION);    wnd.hInstance        = hInstance;    wnd.lpfnWndProc    = ZWindow::WndProc;    wnd.lpszClassName    = szAppName;    wnd.lpszMenuName    = NULL;    wnd.style        = CS_HREDRAW | CS_VREDRAW;        if (!RegisterClass(&wnd))    {        MessageBox(NULL, "Can not register window class", "Error",                    MB_OK | MB_ICONINFORMATION);        return -1;    }        hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);    zwnd.Attach(hWnd);        zwnd.ShowWindow(nCmdShow);    zwnd.UpdateWindow();    while (GetMessage(&msg, NULL, 0, 0))    {        DispatchMessage(&msg);    }        return msg.wParam;}

这个程序显示了ZWindow的用法。实际上,这个类没什么特别。只是包装了windows API。唯一的优点依然是不用传递HWND参数个API了。但当你调用函数时,你还得键入类名。

比如,之前你这样调用:

    ShowWindow(hWnd, nCmdShow);

现在你这样掉:

    zwnd.ShowWindow(nCmdShow);

目前为止没什么到不了的,接下来看看WndProc中处理消息的方式。之前我们只响应一个消息:WM_DESTROY。如果我们想处理更多的消息,我们就需要在消息循环函数中添加更多的case语句。下面的函数添加了WM_PAINT消息响应。

switch (uMsg){case WM_PAINT:    hDC = ::BeginPaint(hWnd, &ps);    ::GetClientRect(hWnd, &rect);    ::DrawText(hDC, "Hello world", -1, &rect, DT_CENTER | DT_VCENTER  DT_SINGLELINE);    ::EndPaint(hWnd, &ps);    break;case WM_DESTROY:    ::PostQuitMessage(0);    break;}

这段代码在窗口中央打印“hello world”,注意观察,为什么要用BeginPaintGetClientRectEndPaint这些API呢?他们第一个参数都是HWND,应该是包含在zwindow中成员变量才对呀?

由于所有这些函数都不是静态函数,而你有不可以在静态的成员函数中调用非静态的成员函数。为什么呢?他们的不同依然是this指针,非静态成员函数含有this指针,而静态成员函数没有。如果我们有办法将这个this指针传递给类的静态成员函数,那我们就可以在其中调用非静态成员函数了。看看下面的例子:

Program 68

#include <iostream>using namespace std;class C {public:    void NonStaticFunc()     {            cout << "NonStaticFun" << endl;    }    static void StaticFun(C* pC)     {        cout << "StaticFun" << endl;        pC->NonStaticFunc();    }};int main(){    C objC;    C::StaticFun(&objC);    return 0;}

程序输出为:

StaticFunNonStaticFun

我们可以用类似的技术,用一个全局变量来存储zwindow对象的地址,在静态成员函数中,就可以利用这个变量来调用非静态成员函数了。下面程序是对前面程序的改进,其中没有直接调用windowAPI

程序 69

#include <windows.h>class ZWindow;ZWindow* g_pWnd = NULL;class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }    inline HDC BeginPaint(LPPAINTSTRUCT ps)    {  return ::BeginPaint(m_hWnd, ps); }    inline BOOL EndPaint(LPPAINTSTRUCT ps)    {  return ::EndPaint(m_hWnd, ps); }    inline BOOL GetClientRect(LPRECT rect)    {  return ::GetClientRect(m_hWnd, rect); }    BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,                 HWND hWndParent = 0,    DWORD dwStyle = WS_OVERLAPPEDWINDOW,                 DWORD dwExStyle = 0, HMENU hMenu = 0)    {        m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,                                   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,                                   CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);        return m_hWnd != NULL;    }    static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)    {        ZWindow* pThis = g_pWnd;        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        switch (uMsg)        {        case WM_PAINT:            hDC = pThis->BeginPaint(&ps);            pThis->GetClientRect(&rect);            ::DrawText(hDC, "Hello world", -1, &rect,                        DT_CENTER | DT_VCENTER | DT_SINGLELINE);            pThis->EndPaint(&ps);            break;        case WM_DESTROY:            ::PostQuitMessage(0);            break;        }        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);    }};int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,                    LPSTR lpCmdLine,   int nCmdShow){    char szAppName[] = "Hello world";    MSG msg;    WNDCLASS wnd;    ZWindow zwnd;        wnd.cbClsExtra    = NULL;    wnd.cbWndExtra    = NULL;    wnd.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);    wnd.hCursor    = LoadCursor(NULL, IDC_ARROW);    wnd.hIcon        = LoadIcon(NULL, IDI_APPLICATION);    wnd.hInstance    = hInstance;    wnd.lpfnWndProc    = zwnd.WndProc;    wnd.lpszClassName    = szAppName;    wnd.lpszMenuName    = NULL;    wnd.style        = CS_HREDRAW | CS_VREDRAW;        if (!RegisterClass(&wnd))    {        MessageBox(NULL, "Can not register window class", "Error",                    MB_OK | MB_ICONINFORMATION);        return -1;    }    g_pWnd = &zwnd;    zwnd.Create(szAppName, "Hell world", hInstance);    zwnd.ShowWindow(nCmdShow);    zwnd.UpdateWindow();    while (GetMessage(&msg, NULL, 0, 0))    {        DispatchMessage(&msg);    }        return msg.wParam;}

终于,我们有了可以运行的版本。现在让我们利用一下面向对象编程的优势。 如果我们在每个消息中都调用这个函数,我们可以将其写为虚函数,通过继承ZWindow类,我们可以调用者些函数。于是,我们就可以特化zwindow的默认行为了。看看下面的例子:

static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,                                 LPARAM lParam){    ZWindow* pThis = g_pWnd;    switch (uMsg)    {    case WM_CREATE:        pThis->OnCreate(wParam, lParam);        break;    case WM_PAINT:        pThis->OnPaint(wParam, lParam);        break;    case WM_DESTROY:        ::PostQuitMessage(0);        break;    }    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);}

这里的OnCreateOnPaint都是虚函数。如果我们继承ZWindow类,我们可以重写那些想要特化的函数,下面的程序显示了如何在子类中调用OnPaint函数。

程序 70

#include <windows.h>class ZWindow;ZWindow* g_pWnd = NULL;class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }    inline HDC BeginPaint(LPPAINTSTRUCT ps)    {  return ::BeginPaint(m_hWnd, ps); }    inline BOOL EndPaint(LPPAINTSTRUCT ps)    {  return ::EndPaint(m_hWnd, ps); }    inline BOOL GetClientRect(LPRECT rect)    {  return ::GetClientRect(m_hWnd, rect); }    BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,                 HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,                 DWORD dwExStyle = 0, HMENU hMenu = 0)    {        m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,                                   CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,                                   CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);        return m_hWnd != NULL;    }    virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)    {        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        hDC = BeginPaint(&ps);        GetClientRect(&rect);        ::DrawText(hDC, "Hello world", -1, &rect,                    DT_CENTER | DT_VCENTER | DT_SINGLELINE);        EndPaint(&ps);        return 0;    }    virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)    {        return 0;    }    static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,                                     LPARAM lParam)    {        ZWindow* pThis = g_pWnd;        switch (uMsg)        {        case WM_CREATE:            pThis->OnCreate(wParam, lParam);            break;        case WM_PAINT:            pThis->OnPaint(wParam, lParam);            break;        case WM_DESTROY:            ::PostQuitMessage(0);            break;        }        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);    }};class ZDriveWindow : public ZWindow{public:    LRESULT OnPaint(WPARAM wParam, LPARAM lParam)    {        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        hDC = BeginPaint(&ps);        GetClientRect(&rect);        SetBkMode(hDC, TRANSPARENT);        DrawText(hDC, "Hello world From Drive", -1, &rect,                  DT_CENTER | DT_VCENTER | DT_SINGLELINE);        EndPaint(&ps);        return 0;    }};

这个程序的输出是 "Hello world from Drive"。在一个派生类上操作,一切都工作的很好,当我们从ZWindow继承不止一个累时,问题就出现了。所有的消息都跑到了最后一个派生类的消息循环里了。我们看下面的例子:

程序 71

#include <windows.h>class ZWindow;ZWindow* g_pWnd = NULL;class ZWindow{public:    HWND m_hWnd;    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }    inline void Attach(HWND hWnd)    { m_hWnd = hWnd; }    inline BOOL ShowWindow(int nCmdShow)    { return ::ShowWindow(m_hWnd, nCmdShow); }    inline BOOL UpdateWindow()    {  return ::UpdateWindow(m_hWnd); }    inline HDC BeginPaint(LPPAINTSTRUCT ps)    {  return ::BeginPaint(m_hWnd, ps); }    inline BOOL EndPaint(LPPAINTSTRUCT ps)    {  return ::EndPaint(m_hWnd, ps); }    inline BOOL GetClientRect(LPRECT rect)    {  return ::GetClientRect(m_hWnd, rect); }    BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,                 HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,                 DWORD dwExStyle = 0, HMENU hMenu = 0, int x = CW_USEDEFAULT,                 int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT,                 int nHeight = CW_USEDEFAULT)    {        m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,                                    x, y, nWidth, nHeight, hWndParent, hMenu,                                    hInstance, NULL);        return m_hWnd != NULL;    }    virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)    {        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        hDC = BeginPaint(&ps);        GetClientRect(&rect);        ::DrawText(hDC, "Hello world", -1, &rect,                    DT_CENTER | DT_VCENTER | DT_SINGLELINE);        EndPaint(&ps);        return 0;    }    virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)    {        return 0;    }    virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)    {        return 0;    }    virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam)    {        return 0;    }    static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg,                                           WPARAM wParam, LPARAM lParam)    {        ZWindow* pThis = g_pWnd;        if (uMsg == WM_NCDESTROY)            ::PostQuitMessage(0);        switch (uMsg)        {        case WM_CREATE:            pThis->OnCreate(wParam, lParam);            break;        case WM_PAINT:            pThis->OnPaint(wParam, lParam);            break;        case WM_LBUTTONDOWN:            pThis->OnLButtonDown(wParam, lParam);            break;        case WM_KEYDOWN:            pThis->OnKeyDown(wParam, lParam);            break;        case WM_DESTROY:            ::PostQuitMessage(0);            break;        }        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);    }};class ZDriveWindow1 : public ZWindow{public:    LRESULT OnPaint(WPARAM wParam, LPARAM lParam)    {        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        hDC = BeginPaint(&ps);        GetClientRect(&rect);        ::SetBkMode(hDC, TRANSPARENT);        ::DrawText(hDC, "ZDriveWindow1", -1, &rect,                    DT_CENTER | DT_VCENTER | DT_SINGLELINE);        EndPaint(&ps);        return 0;    }    LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)    {        ::MessageBox(NULL, "ZDriveWindow1::OnLButtonDown", "Msg", MB_OK);        return 0;    }};class ZDriveWindow2 : public ZWindow{public:    LRESULT OnPaint(WPARAM wParam, LPARAM lParam)    {        HDC hDC;        PAINTSTRUCT ps;        RECT rect;        hDC = BeginPaint(&ps);        GetClientRect(&rect);        ::SetBkMode(hDC, TRANSPARENT);        ::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);        ::DrawText(hDC, "ZDriveWindow2", -1, &rect,                   DT_CENTER | DT_VCENTER | DT_SINGLELINE);        EndPaint(&ps);        return 0;    }    LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)    {        ::MessageBox(NULL, "ZDriveWindow2::OnLButtonDown", "Msg", MB_OK);        return 0;    }};int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,                     LPSTR lpCmdLine,   int nCmdShow){    char szAppName[] = "Hello world";    MSG msg;    WNDCLASS wnd;    ZDriveWindow1 zwnd1;    ZDriveWindow2 zwnd2;        wnd.cbClsExtra        = NULL;    wnd.cbWndExtra        = NULL;    wnd.hbrBackground        = (HBRUSH)GetStockObject(GRAY_BRUSH);    wnd.hCursor        = LoadCursor(NULL, IDC_ARROW);    wnd.hIcon            = LoadIcon(NULL, IDI_APPLICATION);    wnd.hInstance        = hInstance;    wnd.lpfnWndProc        = ZWindow::StartWndProc;    wnd.lpszClassName        = szAppName;    wnd.lpszMenuName        = NULL;    wnd.style            = CS_HREDRAW | CS_VREDRAW;        if (!RegisterClass(&wnd))    {        MessageBox(NULL, "Can not register window class", "Error",                      MB_OK | MB_ICONINFORMATION);        return -1;    }    g_pWnd = &zwnd1;    zwnd1.Create(szAppName, "Hell world", hInstance);    zwnd1.ShowWindow(nCmdShow);    zwnd1.UpdateWindow();    g_pWnd = &zwnd2;    zwnd2.Create(szAppName, "Hello world", hInstance, zwnd1.m_hWnd,         WS_VISIBLE | WS_CHILD | ES_MULTILINE, NULL, NULL, 0, 0, 150, 150);    while (GetMessage(&msg, NULL, 0, 0))    {        DispatchMessage(&msg);    }        return msg.wParam;}

无论你点击哪个窗口,这个程序输出同样的消息框。这就是说,消息没有被合适的传送给相应的窗体。实际上,每个窗口都有自己的消息过程,来处理相应窗口的消息。但这里的第一个窗体也用了第二个派生类的消息循环函数,从而导致第一个窗口的消息无法执行。

这里的关键问题是:我们必须将特定的消息循环与特定的窗口相联系。也就意味着,HWND必须与特定的派生类相关联,从而使消息走到特定的窗口。为实现这个功能,有很多方法可以选择,我们下面分别给予阐述:

第一种简单的思路是:构造一个全局的结构,该结构存储窗口句柄和对应的派生类地址。但有两个重要问题,首先是当程序中窗口越来越多时,这个结构将越来越大;其次是这种方法涉及到全局搜索的问题,而在该结构非常大的情况下,时间开销是个问题。(我补充了下面的例子,已经在vs2005里编译通过,请看下面的程序:)

#include<windows.h>


class ZWindow;


typedefstruct _Node{

ZWindow * g_pWnd;

HWND m_hwnd;

_Node * next;

}Node;


Node * pheader = NULL;


Node * pcurrent = NULL;


ZWindow * current_wnd = NULL;


class ZWindow

{

public:

HWND m_hWnd;


ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }


inlinevoid Attach(HWND hWnd)

{ m_hWnd = hWnd; }


inline BOOL ShowWindow(int nCmdShow)

{return ::ShowWindow(m_hWnd, nCmdShow); }


inline BOOL UpdateWindow()

{return ::UpdateWindow(m_hWnd); }


inline HDC BeginPaint(LPPAINTSTRUCT ps)

{return ::BeginPaint(m_hWnd, ps); }


inline BOOL EndPaint(LPPAINTSTRUCT ps)

{return ::EndPaint(m_hWnd, ps); }


inline BOOL GetClientRect(LPRECT rect)

{return ::GetClientRect(m_hWnd, rect); }


BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,

HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,

DWORD dwExStyle = 0, HMENU hMenu = 0,int x = CW_USEDEFAULT,

int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT,

int nHeight = CW_USEDEFAULT)

{

m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,

x, y, nWidth, nHeight, hWndParent, hMenu,

hInstance, NULL);


Node* pnode =new Node();

pnode->g_pWnd =this;

pnode->m_hwnd =this->m_hWnd;

pnode->next = NULL;


Node * pNode = pheader;


if(pheader == NULL){

pheader = pnode;

pcurrent = pheader;

}

else{

pcurrent->next = pnode;

pcurrent = pcurrent->next;

}


return m_hWnd != NULL;

}


virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::DrawText(hDC,"Hello world", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);

return 0;

}


virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

return 0;

}


virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)

{

return 0;

}


virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam)

{

return 0;

}


static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg,

WPARAM wParam, LPARAM lParam)

{

ZWindow* pThis = NULL;


Node * pthisnode = pheader;


while(pthisnode != NULL && pthisnode->m_hwnd != hWnd)

pthisnode = pthisnode->next;


if(pthisnode != NULL)

pThis = pthisnode->g_pWnd;


if(pThis == NULL)

pThis = current_wnd;



if (uMsg == WM_NCDESTROY)

::PostQuitMessage(0);


switch (uMsg)

{

case WM_CREATE:

pThis->OnCreate(wParam, lParam);

break;


case WM_PAINT:

pThis->OnPaint(wParam, lParam);

break;


case WM_LBUTTONDOWN:

pThis->OnLButtonDown(wParam, lParam);

break;


case WM_KEYDOWN:

pThis->OnKeyDown(wParam, lParam);

break;


case WM_DESTROY:

::PostQuitMessage(0);

break;

}


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

}

};


class ZDriveWindow1 : public ZWindow

{

public:

LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::SetBkMode(hDC, TRANSPARENT);

::DrawText(hDC,"ZDriveWindow1", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);


return 0;

}


LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

::MessageBox(NULL,"ZDriveWindow1::OnLButtonDown","Msg", MB_OK);

return 0;

}


};


class ZDriveWindow2 : public ZWindow

{

public:

LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::SetBkMode(hDC, TRANSPARENT);

::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);

::DrawText(hDC,"ZDriveWindow2", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);


return 0;

}


LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

::MessageBox(NULL,"ZDriveWindow2::OnLButtonDown","Msg", MB_OK);

return 0;

}


};


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine,int nCmdShow)

{

char szAppName[] = "Hello world";

MSG msg;

WNDCLASS wnd;

ZDriveWindow1 zwnd1;

ZDriveWindow2 zwnd2;

wnd.cbClsExtra = NULL;

wnd.cbWndExtra = NULL;

wnd.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);

wnd.hCursor = LoadCursor(NULL, IDC_ARROW);

wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION);

wnd.hInstance = hInstance;

wnd.lpfnWndProc = ZWindow::StartWndProc;

wnd.lpszClassName = szAppName;

wnd.lpszMenuName = NULL;

wnd.style = CS_HREDRAW | CS_VREDRAW;

if (!RegisterClass(&wnd))

{

MessageBox(NULL,"Can not register window class","Error",

MB_OK | MB_ICONINFORMATION);

return -1;

}


current_wnd = &zwnd1;

zwnd1.Create(szAppName,"Hell world", hInstance);


zwnd1.ShowWindow(nCmdShow);

zwnd1.UpdateWindow();



current_wnd = &zwnd2;

zwnd2.Create(szAppName,"Hello world", hInstance, zwnd1.m_hWnd,

WS_VISIBLE | WS_CHILD | ES_MULTILINE, NULL, NULL, 0, 0, 150, 150);


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

{

DispatchMessage(&msg);

}

return msg.wParam;

}


该程序与前一个程序相比较,有以下不同:

首先,构造了全局的节点,用来存储派生类和窗口的组合,然后构造一个链表,记录其头部和当前位置。同时,还需要一个全局的派生类指针来引导窗口过程进入相应的类,来初始化全局节点。

ypedefstruct _Node{

ZWindow * g_pWnd;

HWND m_hwnd;

_Node * next;

}Node;


Node * pheader = NULL;


Node * pcurrent = NULL;


ZWindow * current_wnd = NULL;


(由上面程序可以看出,我们在刚刚创建窗口时,是利用全局的派生类指针作为标识,进行窗口过程和派生类的关联;而在基类的Create函数中(注意该函数不是虚函数,所以子类不可以重写,但子类将继承该函数)进行全局链表的初始化。之后,所有的窗口消息将有这个链表中类和窗口的关联性来连接。)

ATL的主要目标是让代码尽可能少,执行速度尽可能快。但上述的方法却连其中任何一点都做不到。上述的方法不仅很慢,而且在窗口很多的情况下会消耗大量内存。

另一种可能的方案是,利用WNDCLASS或者 WNDCLASSEX结构中的cbWndExtra字段。同时,我们要注意一个细节,为何没有用cbClsExtra而用cbWndExtra答案很简单,cbClsExtra为每个类存储额外字节,而cbWndExtra为类中每个窗口存储附加字节。所以,你无法用cbClsExtra区分统一各类的多个窗口。我们只需要在cbWndExtra中存储适当的窗口类信息。(我补充了下面的例子,已经在vs2005里编译通过,请看下面的程序:)

#include“windows.h”

class ZWindow

{

public:

HWND m_hWnd;


ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }


inlinevoid Attach(HWND hWnd)

{ m_hWnd = hWnd; }


inline BOOL ShowWindow(int nCmdShow)

{return ::ShowWindow(m_hWnd, nCmdShow); }


inline BOOL UpdateWindow()

{return ::UpdateWindow(m_hWnd); }


inline HDC BeginPaint(LPPAINTSTRUCT ps)

{return ::BeginPaint(m_hWnd, ps); }


inline BOOL EndPaint(LPPAINTSTRUCT ps)

{return ::EndPaint(m_hWnd, ps); }


inline BOOL GetClientRect(LPRECT rect)

{return ::GetClientRect(m_hWnd, rect); }


BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,

HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,

DWORD dwExStyle = 0, HMENU hMenu = 0,int x = CW_USEDEFAULT,

int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT,

int nHeight = CW_USEDEFAULT)

{

m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,

x, y, nWidth, nHeight, hWndParent, hMenu,

hInstance, NULL);


::SetWindowLong(m_hWnd,GWL_USERDATA,(LONG)this);


return m_hWnd != NULL;

}


virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::DrawText(hDC,"Hello world", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);

return 0;

}


virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

return 0;

}


virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)

{

return 0;

}


virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam)

{

return 0;

}


static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg,

WPARAM wParam, LPARAM lParam)

{

ZWindow* pThis = NULL;


pThis = (ZWindow*)::GetWindowLong(hWnd,GWL_USERDATA);


if (uMsg == WM_NCDESTROY)

::PostQuitMessage(0);


switch (uMsg)

{

case WM_CREATE:

pThis->OnCreate(wParam, lParam);

break;


case WM_PAINT:

pThis->OnPaint(wParam, lParam);

break;


case WM_LBUTTONDOWN:

pThis->OnLButtonDown(wParam, lParam);

break;


case WM_KEYDOWN:

pThis->OnKeyDown(wParam, lParam);

break;


case WM_DESTROY:

::PostQuitMessage(0);

break;

}


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

}

};


class ZDriveWindow1 : public ZWindow

{

public:

LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::SetBkMode(hDC, TRANSPARENT);

::DrawText(hDC,"ZDriveWindow1", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);


return 0;

}


LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

::MessageBox(NULL,"ZDriveWindow1::OnLButtonDown","Msg", MB_OK);

return 0;

}


};


class ZDriveWindow2 : public ZWindow

{

public:

LRESULT OnPaint(WPARAM wParam, LPARAM lParam)

{

HDC hDC;

PAINTSTRUCT ps;

RECT rect;


hDC = BeginPaint(&ps);

GetClientRect(&rect);

::SetBkMode(hDC, TRANSPARENT);

::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);

::DrawText(hDC,"ZDriveWindow2", -1, &rect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(&ps);


return 0;

}


LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)

{

::MessageBox(NULL,"ZDriveWindow2::OnLButtonDown","Msg", MB_OK);

return 0;

}


};


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine,int nCmdShow)

{

char szAppName[] = "Hello world";

MSG msg;

WNDCLASS wnd;

ZDriveWindow1 zwnd1;

ZDriveWindow2 zwnd2;

wnd.cbClsExtra = NULL;

wnd.cbWndExtra =sizeof(int);

wnd.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);

wnd.hCursor = LoadCursor(NULL, IDC_ARROW);

wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION);

wnd.hInstance = hInstance;

wnd.lpfnWndProc = ZWindow::StartWndProc;

wnd.lpszClassName = szAppName;

wnd.lpszMenuName = NULL;

wnd.style = CS_HREDRAW | CS_VREDRAW;

if (!RegisterClass(&wnd))

{

MessageBox(NULL,"Can not register window class","Error",

MB_OK | MB_ICONINFORMATION);

return -1;

}


zwnd1.Create(szAppName,"Hell world", hInstance);


zwnd1.ShowWindow(nCmdShow);

zwnd1.UpdateWindow();



zwnd2.Create(szAppName,"Hello world", hInstance, zwnd1.m_hWnd,

WS_VISIBLE | WS_CHILD | ES_MULTILINE, NULL, NULL, 0, 0, 150, 150);


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

{

DispatchMessage(&msg);

}

return msg.wParam;

}



该程序与程序71相比,仅仅是在基类的Create函数中,添加了下面语句:

::SetWindowLong(m_hWnd,GWL_USERDATA,(LONG)this);

同时,在消息循环中通过下面语句获取当前派生类:

pThis = (Zwindow*)::GetWindowLong(hWnd,GWL_USERDATA);


这种方案比第一种要好一些,但任然有两个问题。第一,如果用户想利用cbWndExtra字段,很可能将导致消息循环的混乱,所以用这种技术的人必须小心谨慎,别丢到这个字段的内容。就算你决定这么做,并且写好文档告诉别人你的库不支持修改cbWndExtra字段,但还有第二个问题,与ATL使用的方法相比,这种方法不够快。

ATL既没有用第一种方法,也没有用第二种方法,而使用了一种叫做“Thunk”的方法。Thunk是执行特定工作的一小段代码,这个术语在一些不同的情况下,你可能听过两种Thunk技术

通用ThunkUniversal Thunking

通用ThunkUniversal Thunking)可以实现16位代码调用32位函数。在Win9xWinNT/2000/XP均可用。这种Thunk又被较为泛型ThunkGeneric Thunking.

一般性ThunkGeneral Thunking

一般性Thunk可以使16位函数调用32位的代码,仅仅在Win 9x上使用,因为Win NT/2000 / XP是真正的32位操作系统,所以没有理由用16位代码调用32为程序,这种Thunk也被叫做平面ThunkFlat Thunking)。

ATL没有使用这两种里面的任意一种,因为我们在ATL中不涉及16位和32位的混合编程,而只是插入一小段代码来调用合理的窗口过程。

在看ATLthunking技术之前,我们先看看基本概念。看看下面简单例子:

程序 72

#include <iostream>using namespace std;struct S{    char ch;    int i;};int main(){    cout << "Size of character = " << sizeof(char) << endl;    cout << "Size of integer = " << sizeof(int) << endl;    cout << "Size of structure = " << sizeof(S) << endl;    return 0;}

程序输出如下:

Size of character = 1Size of integer = 4Size of structure = 8

成员变量的大小之和应该是5而不是8,下面我们再添加一个成员变量,看看如下程序:

程序 73

#include <iostream>using namespace std;struct S{    char ch1;    char ch2;    int i;};int main(){    cout << "Size of character = " << sizeof(char) << endl;    cout << "Size of integer = " << sizeof(int) << endl;    cout << "Size of structure = " << sizeof(S) << endl;    return 0;}

程序输出结果与前面的一样,那到底是怎么回事呢?我们对前述程序稍作改变,看看内部发生了什么。

程序 74

#include <iostream>using namespace std;struct S{    char ch1;    char ch2;    int i;}s;int main(){    cout << "Address of ch1 = " << (int)&s.ch1 << endl;    cout << "Address of ch2 = " << (int)&s.ch2 << endl;    cout << "Address of int = " << (int)&s.i << endl;    return 0;}

程序输出结果为:

Address of ch1 = 4683576Address of ch2 = 4683577Address of int = 4683580

这是由于结构和联合的成员按字对齐导致的。如果你仔细观察,你会发现在结构外面的变量都存储在可以被4整出的地址处。这是为了提高CPU的性能。所以,这里的结构体存在了4的倍数对应的一个地址4683576 处,成员变量ch1当然也是同样的地址.成员变量 ch2紧挨着这个存储单元存储,而int型的成员变量I存放在地址为4683580,为什么不紧挨着放在4683578处呢,因为这个地址是不可以被4整除的。一个自然的问题:在4683578 and 4683579中放置的内容是什么呢?如果变量是局部的,那存放的就是垃圾数据,如果是全局的或者静态的,就是0。看下面程序有助于理解这里所讲的内容:

程序 75

#include <iostream>using namespace std;struct S{    char ch1;    char ch2;    int i;};int main(){    S s = { 'A', 'B', 10};    void* pVoid = (void*)&s;    char* pChar = (char*)pVoid;    cout << (char)*(pChar + 0) << endl;    cout << (char)*(pChar + 1) << endl;    cout << (char)*(pChar + 2) << endl;    cout << (char)*(pChar + 3) << endl;    cout << (int)*(pChar + 4) << endl;    return 0;}

程序输出为:

AB¦¦10

程序结果显示,在其中存放了垃圾数据。

我们如何才可以避免这种空间浪费呢。有两种方法,一是使用编译器开关/Zp,二是在申明结构体的前面添加语句#pragma

程序 76

#include <iostream>using namespace std;#pragma pack(push, 1)struct S{    char ch;    int i;};#pragma pack(pop)int main(){    cout << "Size of structure = " << sizeof(S) << endl;    return 0;}

程序输出结果为:

Size of structure = 5

这意味着,没有了因字对齐导致的空闲空间存在。实际上,ATL用这种技术来制作thunkATL用一个不进行字节对齐的结构体来存储微处理器的直接机器码。

#pragma pack(push,1)// structure to store the machine codestruct Thunk{    BYTE    m_jmp;          // op code of jmp instruction    DWORD   m_relproc;      // relative jmp};#pragma pack(pop)

这种结构可以存储thunk代码,这些代码执行的速度非常快。我们看看用thunk执行函数的例子。

Program 77

#include <iostream>#include <windows.h>using namespace std;class C;C* g_pC = NULL;typedef void(*pFUN)();#pragma pack(push,1)// structure to store the machine codestruct Thunk{    BYTE    m_jmp;          // op code of jmp instruction    DWORD   m_relproc;      // relative jmp};#pragma pack(pop)class C{public:    Thunk    m_thunk;    void Init(pFUN pFun, void* pThis)    {        // op code of jump instruction        m_thunk.m_jmp = 0xe9;        // address of the appripriate function        m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));        FlushInstructionCache(GetCurrentProcess(),                                 &m_thunk, sizeof(m_thunk));    }    // this is cour call back function    static void CallBackFun()    {        C* pC = g_pC;        // initilize the thunk        pC->Init(StaticFun, pC);        // get the address of thunk code        pFUN pFun = (pFUN)&(pC->m_thunk);        // start executing thunk code which will call StaticFun        pFun();        cout << "C::CallBackFun" << endl;    }    static void StaticFun()    {        cout << "C::StaticFun" << endl;    }};int main(){    C objC;    g_pC = &objC;    C::CallBackFun();    return 0;}

程序输出结果为:

C::StaticFunC::CallBackFun

StaticFun通过在thunk中初始化,并且通过其调用。程序执行的思路大致是:

  • CallBackFun

  • Init (to initialize the thunk)

  • Get the address of thunk

  • Execute thunk

  • Thunk code call StaticFun



ATL用了同样的技术实现回调函数的调用,但在调之前还做了一件事。ZWindow又加了一个虚函数:ProcessWindowMessage,这个函数在基类中不做任何事情。但每个派生类都要通过重写这个函数来处理其消息。过程是一样的:我们将ZWindow派生类的地址保存在一个指针中,用这个指针来调用派生类的虚函数,但窗口函数由WindowProc变为StartWndProcATL用该技术将HWND参数用this指针来替代,这样,this指针里包含了相应的HWND,所以,thisHWND信息都将得到保存。

为了实现这个,ATL用了比之前稍微大的结构:

#pragma pack(push,1)struct _WndProcThunk{    DWORD   m_mov;    // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)    DWORD   m_this;    BYTE    m_jmp;    // jmp WndProc    DWORD   m_relproc;    // relative jmp};#pragma pack(pop)

在初始化时,写入汇编代码: "mov dword ptr [esp +4], pThis"。大概是这样的:

void Init(WNDPROC proc, void* pThis){    thunk.m_mov = 0x042444C7;  //C7 44 24 04    thunk.m_this = (DWORD)pThis;    thunk.m_jmp = 0xe9;    thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));    FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));}

初始化thunk代码后,获取thunk地址,并且将新的窗口过程地址设为thunk地址,之后,thunk代码将调用callWindowProc函数,而此时的第一个参数由HWND变成了this指针。所以,我们可以安全地将其转化为ZWindow*类型,然后调用ProcessWindowMessage函数。

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,                                    WPARAM wParam, LPARAM lParam){    ZWindow* pThis = (ZWindow*)hWnd;    if (uMsg == WM_NCDESTROY)        PostQuitMessage(0);    if (!pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam))        return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam);    else        return 0;}现在,每个窗口都会别适当的窗口过程处理。

-------------------------------------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------华丽的分割线--本系列结束----------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------

原创粉丝点击