《Windows 程序设计(第3版)》——6.3 创建窗口

简介: 我们的框架也提供了一个通用的消息处理函数AfxWndProc。为了响应窗口消息,必须让Windows把窗口的消息处理函数的地址全设为AfxWndProc,在处理消息时由我们自己决定是否调用默认的消息处理函数。改变窗口消息处理函数地址最简单的办法是使用SetWindowLong函数。

本节书摘来自异步社区《Windows 程序设计(第3版)》一书中的第6章,第6.3节,作者:王艳平 , 张铮著,更多章节内容可以访问云栖社区“异步社区”公众号查看

6.3 创建窗口

6.3.1 窗口函数
Windows为每个窗口都提供了默认的消息处理函数,自定义类的窗口的默认消息处理函数是DefWindowProc,各子窗口控件(见7.1节)的类名是Windows预定义的,其窗口函数自然由Windows提供。

我们的框架也提供了一个通用的消息处理函数AfxWndProc。为了响应窗口消息,必须让Windows把窗口的消息处理函数的地址全设为AfxWndProc,在处理消息时由我们自己决定是否调用默认的消息处理函数。改变窗口消息处理函数地址最简单的办法是使用SetWindowLong函数。比如,下面代码会将句柄为hWnd的窗口的窗口函数地址设为AfxWndProc,并将原来的地址保存在oldWndProc变量中。

WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxWndProc);

GWL_WNDPROC标记指示了此次调用的目的是设置窗口函数的地址,新的地址由第3个参数AfxWndProc指明。调用成功后,SetWindowLong返回原来窗口函数的地址。以这个地址为参数调用CallWindowProc函数就相当于对消息做了默认处理。

::CallWindowProc(oldWndProc, hWnd, message, wParam, lParam);
以前在注册窗口类的时候,系统都将一个自定义的函数WndProc的地址传给WNDCLASS或WNDCLASSEX结构,然后在WndProc函数里处理Windows发来的消息。但是,以这种方式创建出来的窗口和标准的子窗口控件有一个明显的区别,就是其窗口函数不是由Windows系统提供的。为了消除这种区别,在注册窗口类时可以直接让API函数DefWindowProc作为窗口函数响应Windows消息,如下面代码所示。

WNDCLASS wndclass;
wndclass.lpfnWndProc = ::DefWindowProc;
...    // 其他代码

这样一来,消息都会被直接发送到默认的消息处理函数,各种窗口处理消息的方式都相同了,我们的框架程序可以使用SetWindowLong和CallWindowProc两个函数对待所有的窗口。

6.3.2 注册窗口类
根据窗口的不同用途,框架程序要为它们注册不同的窗口类,为了进行试验,这里只把它们分成两类(虽然还可以分得更细),子窗口使用的窗口类和框架或视图窗口使用的窗口类。这两种类的类名分别是Wnd和FrameOrView,其类型标志被定义为AFX_WND_REG和AFX_WND FRAMEORVIEW_REG。

// _AFXIMPL.H文件,意思是实现类库所需的文件,而不是提供给用户的。请在COMMON目录下添加此文件
#ifndef __AFXIMPL_H__
#define __AFXIMPL_H__
#include "_afxwin.h"

// 窗口类的类型标志
#define AFX_WND_REG (0x0001)        // 使用第1位
#define AFX_WNDFRAMEORVIEW_REG (0x0002) // 使用第2位。还可继续使用0x0004、0x0008、0x0010等

#define AFX_WND ("Wnd")
#define AFX_WNDFRAMEORVIEW ("FrameOrView")

// 框架程序注册窗口类时使用的类名,这些变量定义在WINCORE.CPP文件
extern const TCHAR _afxWnd[];
extern const TCHAR _afxWndFrameOrView[];

#endif  // __AFXIMPL_H__
在WINCORE.CPP文件的头部有类名的定义:

const TCHAR _afxWnd[] = AFX_WND;        // 当然,文件中有“#include "_afximpl.h"”语句
const TCHAR _afxWndFrameOrView[] = AFX_WNDFRAMEORVIEW;

上面的代码定义了两种类型的窗口类使用的标志和类名,如果想添加新的类型,按这种方式继续添加代码就行了。

自定义函数AfxEndDeferRegisterClass实现了为框架程序注册窗口类的功能,函数唯一的参数是类型标志,指明要注册什么类型的窗口类,具体的代码如下。

// 这两个函数的声明代码在_AFXWIN.H文件中(CWnd类下面),实现代码在WINCORE.CPP文件中
BOOL AfxEndDeferRegisterClass(LONG fToRegister)
{
  WNDCLASS wndclass;
  memset(&wndclass, 0, sizeof(wndclass));
  wndclass.lpfnWndProc = ::DefWindowProc;
  wndclass.hInstance = AfxGetModuleState()->m_hCurrentInstanceHandle;
  wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);

  BOOL bResult = FALSE;
  if(fToRegister & AFX_WND_REG)
  {
    // 子窗口——没有背景刷子,没有图标,最安全的风格
    wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    wndclass.lpszClassName = _afxWnd;
    bResult = AfxRegisterClass(&wndclass);
  }
  else if(fToRegister & AFX_WNDFRAMEORVIEW_REG)
  {
    // 框架或视图窗口——普通的颜色
    wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wndclass.lpszClassName = _afxWndFrameOrView;
    bResult = AfxRegisterClass(&wndclass);
  }
  return bResult;
}

BOOL AfxRegisterClass(WNDCLASS* lpWndClass)
{
  WNDCLASS wndclass;
  if (GetClassInfo(lpWndClass->hInstance, lpWndClass->lpszClassName,
    &wndclass))
  {
    // 已经注册了该类
    return TRUE;
  }
  if (!::RegisterClass(lpWndClass))
  {
    TRACE("Can't register window class named %s\n", lpWndClass->lpszClassName);
    return FALSE;
  }
  return TRUE;
}

由AfxEndDeferRegisterClass函数注册的窗口类使用的窗口函数都是默认的消息处理函数DefWindowProc,两种不同类型的窗口类使用的类的风格、类名或背景刷子等参数不完全相同。最终的注册工作由AfxRegisterClass函数来完成。AfxEndDeferRegisterClass函数的用法十分简单,比如下面语句为创建框架窗口注册了窗口类。

VERIFY(AfxEndDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));  // 类名为_afxWndFrameOrView

AfxRegisterClass函数是对API函数RegisterClass的扩展。它先调用GetClassInfo函数试图查看要注册的类的信息,如果查看成功就不注册了,仅返回TRUE。

AfxEndDeferRegisterClass函数只能作为类库内部调用的一个函数来使用,下面再提供一个更通用的注册窗口类的函数。

LPCTSTR AfxRegisterWndClass(UINT nClassStyle, HCURSOR hCursor,
          HBRUSH hbrBackground, HICON hIcon)
{
  // 使用线程局部存储中的缓冲区存放临时类名
  LPTSTR lpszName = AfxGetThreadState()->m_szTempClassName;

  HINSTANCE hInst = AfxGetModuleState()->m_hCurrentInstanceHandle;
  if(hCursor == NULL && hbrBackground == NULL && hIcon == NULL)
    wsprintf(lpszName, "Afx:%d:%d", (int)hInst, nClassStyle);
  else
    wsprintf(lpszName, "Afx:%d:%d:%d:%d", (int)hInst, nClassStyle, 
          (int)hCursor, (int)hbrBackground, (int)hIcon);

  WNDCLASS wc = { 0 };
  if(::GetClassInfo(hInst, lpszName, &wc))
  {
    ASSERT(wc.style == nClassStyle);
    return lpszName;
  }
  
  wc.hInstance = hInst;
  wc.style = nClassStyle;
  wc.hCursor = hCursor;
  wc.hbrBackground = hbrBackground;
  wc.hIcon = hIcon;
  wc.lpszClassName = lpszName;
  wc.lpfnWndProc = ::DefWindowProc;
  if(!AfxRegisterClass(&wc))
  {
    TRACE("Can't register window class named %s\n", lpszName);            
    return NULL;
  }
  return lpszName;
}

_AFX_THREAD_STATE结构的成员m_szTempClassName[96]的作用是保存当前线程注册的窗口类。后面的例子程序基本都要使用这个函数注册窗口类。在_AFXWIN.H文件中有如下这样几个函数的声明。

// 注册窗口类的辅助函数
LPCTSTR AfxRegisterWndClass(UINT nClassStyle,
          HCURSOR hCursor = 0, HBRUSH hbrBackground = 0, HICON hIcon = 0);
BOOL AfxRegisterClass(WNDCLASS* lpWndClass);
BOOL AfxEndDeferRegisterClass(LONG fToRegister);

6.3.3 消息钩子
现在,框架程序创建的窗口的窗口函数都是Windows提供的默认的消息处理函数,不管在创建的过程中使用的是自定义的窗口类,还是使用系统预定义的窗口类,为了使框架程序提供的函数AfxWndProc获得消息的处理权,必须调用SetWindowLong将窗口函数的地址设为AfxWndProc函数的地址。可是应该在什么时候调用此函数呢?

这个问题并不像想象的那么简单。调用CreateWindowEx的时候,窗口函数就开始接受消息。也就是说,在CreateWindowEx返回窗口句柄之前窗口函数已经开始处理消息了,这些消息有WM_GETMINMAXINFO、WM_NCCREATE和WM_CREATE等。所以等到CreateWindowEx返回的时候再调用SetWindowLong函数就已经晚了,漏掉了许多的消息。

那么,有没有办法让系统在正要创建窗口的时候通知应用程序呢?这样的话,就可以在窗口函数接受到任何消息之前有机会改变窗口函数的地址。使用钩子函数能够实现这一设想。

在Windows的消息处理机制中,应用程序可以通过安装钩子函数监视系统中消息的传输。在特定的消息到达目的窗口之前,钩子函数就可以将它们截获。这种机制的实现原理第9章有专门介绍。但钩子函数的使用方法是比较简单的,例如,下面一条语句就给当前线程安装了一个类型为WH_CBT的钩子,其钩子函数的地址为HookProc。

HHOOK hHook = ::SetWindowsHookEx(WH_CBT, HookProc, NULL, ::GetCurrentThreadId());

系统在发生下列事件之前激活WH_CBT类型的钩子,调用自定义钩子函数HookProc通知应用程序:

创建、销毁、激活、最大化、最小化、移动或者改变窗口的大小;
完成系统命令;
将鼠标或键盘消息移出消息队列;
设置输入输出焦点;
同步系统消息队列。
HookProc是一个自定义的回调函数,和窗口函数WndProc一样,其函数名称可以是任意的。

LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam);
nCode参数指示了钩子函数应该如何处理这条消息,如果它的值小于0,钩子函数必须将消息传给CallNextHookEx函数。此参数的取值可以是HCBT_CREATEWND、HCBT_ACTIVATE等,从字面也可以看出,它们分别对应着窗口的创建、窗口的激活等消息。当窗口将要被创建的时候,nCode的取值是HCBT_CREATEWND,此时,wParam参数指定了新建窗口的句柄,lParam参数是CBT_CREATEWND类型的指针,包含了新建窗口的坐标位置和大小等信息。

SetWindowsHookEx函数的返回值是钩子句柄hHook。CallNextHookEx函数的第一个参数就是此钩子句柄。此函数的作用是调用钩子队列中的下一个钩子。

`LRESULT CallNextHookEx(HHOOK hHook, int nCode, WPARAM wParam, LPARAM lParam);
在不使用钩子的时候还应该以此句柄为参数,调用UnhookWindowsHookEx函数将钩子释放掉。

有了这些知识,我们很容易会想到,在创建窗口之前先安装一个WH_CBT类型的钩子就有机会改变窗口函数的地址了。下面介绍这一过程的具体实现。

在改变窗口函数地址的时候,必须将此窗口原来的窗口函数的地址保存下来以便对消息做默认处理。窗口函数的地址是窗口的一个属性,所以再在CWnd类中添加一个WNDPROC类型的成员变量m_pfnSuper,并添加一个虚函数GetSuperWndProcAddr返回默认的消息处理函数的地址。默认处理时,只要以m_pfnSuper或GetSuperWndProcAddr函数返回的指针所指向的函数为参数调用CallWindowProc函数即可,下面是相关的代码。

class CWnd : public CCmdTarget      // _AFXWIN.H文件
{
...    // 其他成员
protected:

  // 默认消息处理函数的地址
  WNDPROC m_pfnSuper;
  virtual WNDPROC* GetSuperWndProcAddr();

  // 对消息进行默认处理
  LRESULT Default();  
  virtual LRESULT DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam);
  
  // 挂钩消息的实现
  friend LRESULT __stdcall _AfxCbtFilterHook(int, WPARAM, LPARAM);
}

函数的实现代码在WINCORE.CPP文件中。

WNDPROC* CWnd::GetSuperWndProcAddr()    // WINCORE.CPP文件
{
  return &m_pfnSuper;
}

LRESULT CWnd::Default()
{
  // 以最近接收到的一个消息为参数调用DefWindowProc函数
  _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
  return DefWindowProc(pThreadState->m_lastSendMsg.message, 
      pThreadState->m_lastSendMsg.wParam, pThreadState->m_lastSendMsg.lParam);
}

LRESULT CWnd::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  if(m_pfnSuper != NULL)
    return ::CallWindowProc(m_pfnSuper, m_hWnd, message, wParam, lParam);

  WNDPROC pfnWndProc;
  if((pfnWndProc = *GetSuperWndProcAddr()) == NULL) 
    return ::DefWindowProc(m_hWnd, message, wParam, lParam);
  else
    return ::CallWindowProc(pfnWndProc, m_hWnd, message, wParam, lParam);   
}

在类的构造函数中应该把成员m_pfnSuper的值初始化为NULL。CWnd的派生类有可能重载虚函数GetSuperWndProcAddr,所以成员函数DefWindowProc发现m_pfnSuper是NULL后还会去检查GetSuperWndProcAddr的返回值,如果能够得到一个有效的函数地址就将消息传给此函数,否则调用API函数DefWindowProc。

类的友元函数_AfxCbtFilterHook就是要安装的过滤消息的钩子函数。框架程序要在这个函数里改变窗口函数的地址。它将原来窗口函数的地址保存在CWnd类的m_pfnSuper成员中。而GetSuperWndProcAddr成员的保护类型是“protected”,所以要将_AfxCbtFilterHook声明为CWnd类的友元函数。

假设CWnd类提供的创建窗口的函数的名称为CreateEx,现在模拟用户创建窗口的过程。创建窗口的代码如下。

CWnd myWnd; 
myWnd.CreateEx(...);  // 创建窗口

CreateEx函数先安装WH_CBT类型的钩子,然后调用API函数CreateWindowEx创建窗口。

HHOOK hHook = ::SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
:: CreateWindowEx(...);

但是,在写_AfxCbtFilterHook函数的实现代码的时候会遇到如下两个问题:

(1)如何获得调用CallNextHookEx函数时所需的钩子句柄。

(2)在改变窗口函数的地址之前,必须首先让此窗口的窗口句柄hWnd与myWnd对象关联起来,即执行代码“myWnd. Attach(hWnd)”。只有这样,框架程序的窗口函数AfxWndProc才能将接收到的消息传给正确的CWnd对象。可是,在_AfxCbtFilterHook函数中,如何知道正在创建的窗口的CWnd对象的地址呢?

这都是关于传递变量的值的问题,一个是钩子句柄的值,另一个是正在初始化的CWnd对象的指针的值。因为这些变量是线程局部有效的,所以只要在表示线程状态的类中添加相关变量即可。

class _AFX_THREAD_STATE : public CNoTrackObject
{
  ...    // 其他成员
  CWnd* m_pWndInit;      // 正在初始化的CWnd对象的指针
  HHOOK m_hHookOldCbtFilter;  // 钩子句柄
};

安装钩子的时候设置这两个成员的值,在钩子函数中再访问它们就行了。下面是框架程序为创建窗口提供的安装钩子和卸载钩子的函数。

void AfxHookWindowCreate(CWnd* pWnd)
{
  _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
  if(pThreadState->m_pWndInit == pWnd)
    return;
  if(pThreadState->m_hHookOldCbtFilter == NULL)
    pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, 
          _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());

  ASSERT(pWnd != NULL);
  ASSERT(pWnd->m_hWnd == NULL); // 仅挂钩一次

  ASSERT(pThreadState->m_pWndInit == NULL);
  pThreadState->m_pWndInit = pWnd;
}

BOOL AfxUnhookWindowCreate()
{
  _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
  if(pThreadState->m_hHookOldCbtFilter != NULL)
  {
    ::UnhookWindowsHookEx(pThreadState->m_hHookOldCbtFilter);
    pThreadState->m_hHookOldCbtFilter = NULL;
  }

  if(pThreadState->m_pWndInit != NULL)
  {
    pThreadState->m_pWndInit = NULL;
    return FALSE;  // 钩子没有被成功地安装
  }
  return TRUE;
}

因为钩子函数在改变窗口函数的地址以后会将pThreadState->m_pWndInit的值初始化为NULL,所以通过检查此成员的值就可以知道钩子是否被正确安装。下面是实现钩子函数_AfxCbtFilterHook所需的代码。

WNDPROC AfxGetAfxWndProc()
{
  return &AfxWndProc;
}

LRESULT __stdcall _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
  _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
  if(code != HCBT_CREATEWND)
  {
    // 只对HCBT_CREATEWND通知事件感兴趣
    return ::CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);
  }

  // 得到正在初始化的窗口的窗口句柄和CWnd对象的指针
  HWND hWnd = (HWND)wParam;
  CWnd* pWndInit = pThreadState->m_pWndInit;

  // 将hWnd关联到pWndInit指向的CWnd对象中,并设置窗口的窗口函数的地址
  if(pWndInit != NULL)
  {
    // hWnd不应该在永久句柄映射中
    ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);

    // 附加窗口句柄
    pWndInit->Attach(hWnd);
  
    // 允许其他子类化窗口的事件首先发生
    // 请在CWnd类中添加一个什么也不做的PreSubclassWindow虚函数,参数和返回值类型都为void
    pWndInit->PreSubclassWindow();

    // 要在pOldWndProc指向的变量中保存原来的窗口函数
    WNDPROC* pOldWndProc = pWndInit->GetSuperWndProcAddr();
    ASSERT(pOldWndProc != NULL);

    // 子类化此窗口(改变窗口函数的地址)
    WNDPROC afxWndProc = AfxGetAfxWndProc();
    WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, 
                      GWL_WNDPROC, (DWORD)afxWndProc);
    ASSERT(oldWndProc != NULL);
    if(oldWndProc != afxWndProc) // 如果确实改变了
      *pOldWndProc = oldWndProc;

    pThreadState->m_pWndInit = NULL;
  }
  return ::CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);
}

下面是CreateEx函数最基本的实现代码。

AfxHookWindowCreate(this);
::CreateWindowEx(...);  // 此函数执行的时候,钩子函数_AfxCbtFilterHook会收到HCBT_CREATEWND通知
AfxUnhookWindowCreate();

因为程序为当前线程安装了WH_CBT类型的钩子,所以在有任何Windows消息发送到窗口函数前,钩子函数会首先接收到HCBT_CREATEWND通知。在这个时候将窗口函数的地址设为AfxWndProc最合适了。在保存原来的窗口函数的过程中,程序没有直接访问m_pfnSuper变量,而是通过语句“pWndInit->GetSuperWndProcAddr”得到此变量的地址,然后将原来的窗口函数的地址保存到此变量中。

*pOldWndProc = oldWndProc;
GetSuperWndProcAddr返回m_pfnSuper变量的地址仅仅是CWnd类的默认实现,如果CWnd类的派生类重载了虚函数GetSuperWndProcAddr,结果就可能不一样了。

6.3.4 最终实现
至此,完全可以写出CreateEx函数完整的实现代码了。注册窗口类、安装钩子、创建窗口、子类化窗口等全都会出现在这个函数里,下面是在_AFXWIN.H文件中添加的代码。

class CWnd : public CCmdTarget      // _AFXWIN.H文件
{
...    // 其他成员
public:
  // 为创建各种子窗口设置
  virtual BOOL Create(LPCTSTR lpszClassName,
    LPCTSTR lpszWindowName, DWORD dwStyle,
    const RECT& rect,
    CWnd* pParentWnd, UINT nID,
    LPVOID lpParam = NULL);

  // 最终创建窗口的代码
  BOOL CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
    LPCTSTR lpszWindowName, DWORD dwStyle,
    int x, int y, int nWidth, int nHeight,
    HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam = NULL);

  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
  virtual void PostNcDestroy();
  virtual void PreSubclassWindow();
}

这些函数的实现代码如下。

BOOL CWnd::Create(LPCTSTR lpszClassName,
  LPCTSTR lpszWindowName, DWORD dwStyle,
  const RECT& rect,
  CWnd* pParentWnd, UINT nID,
  LPVOID lpParam)
{
  // 只允许创建非弹出式的子窗口
  ASSERT(pParentWnd != NULL);
  ASSERT((dwStyle & WS_POPUP) == 0);

  return CreateEx(0, lpszClassName, lpszWindowName,
    dwStyle | WS_CHILD,
    rect.left, rect.top,
    rect.right - rect.left, rect.bottom - rect.top,
    pParentWnd->GetSafeHwnd(), (HMENU)nID, (LPVOID)lpParam);
}

BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
  LPCTSTR lpszWindowName, DWORD dwStyle,
  int x, int y, int nWidth, int nHeight,
  HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
  CREATESTRUCT cs;
  cs.dwExStyle = dwExStyle;
  cs.lpszClass = lpszClassName;
  cs.lpszName = lpszWindowName;
  cs.style = dwStyle;
  cs.x = x;
  cs.y = y;
  cs.cx = nWidth;
  cs.cy = nHeight;
  cs.hwndParent = hWndParent;
  cs.hMenu = nIDorHMenu;
  cs.hInstance = AfxGetModuleState()->m_hCurrentInstanceHandle;
  cs.lpCreateParams = lpParam;

  // 调用虚函数PreCreateWindow,执行注册窗口类的代码
  if(!PreCreateWindow(cs))
  {
    // 调用虚函数PostNcDestroy,通知用户窗口没有被创建
    PostNcDestroy();
    return FALSE;
  }

  // 创建窗口
  AfxHookWindowCreate(this);
  HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
      cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
      cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
  if(!AfxUnhookWindowCreate())
    PostNcDestroy();    // CreateWindowEx调用失败,通知用户


  if(hWnd == NULL)
    return FALSE;
  ASSERT(hWnd == m_hWnd); // 至此,新窗口的句柄应该已经附加到当前CWnd对象
  return TRUE;
}

BOOL CWnd::PreCreateWindow(CREATESTRUCT& cs)
{
  if(cs.lpszClass == NULL)
  {
    // 默认情况下,创建的是子窗口
    VERIFY(AfxEndDeferRegisterClass(AFX_WND_REG));
    ASSERT(cs.style & WS_CHILD);
    cs.lpszClass = _afxWnd;
  }
  return TRUE;
}

void CWnd::PostNcDestroy()
{
  // 默认情况下什么也不做
}

void CWnd::PreSubclassWindow()
{
  // 默认情况下什么也不做
}

CWnd类提供了Create和CreateEx两个创建窗口的函数。前一个是虚函数,这说明CWnd类的派生类可以重载此函数以创建不同的窗口;后一个函数CreateEx实现了实际创建窗口的代码。CWnd类默认的行为是创建不具有WS_POPUP风格的子窗口。

在创建窗口前,框架程序首先调用虚函数PreCreateWindow,给用户修改创建参数的机会。此函数默认的实现仅仅对窗口类的类名cs.lpszClass感兴趣,发现这个值为NULL后会调用函数AfxEndDeferRegisterClass进行注册。CWnd类的派生类往往重载此函数注册合适自己的窗口类,也可以改变cs对象中其他成员的值,比如窗口风格等。

CreateEx在不能完成创建任务的时候会调用虚函数PostNcDestroy。另外在窗口销毁的时候,框架程序会再次调用此函数,所以用户可以重载这个函数做一些清理工作,如销毁CWnd对象等。

总之,创建窗口的时候只要先实例化一个CWnd类(或其派生类)的对象,然后调用成员函数Create或CreateEx即可。一般从CWnd派生的类都会重载虚函数Create以创建特定类型的窗口,比如以后要讲述的CEdit类、CDialog类等。

6.3.5 创建窗口的例子
本小节将把上述知识放在一起,使用框架程序创建第一个窗口。例子代码在配套光盘的06CreateExample工程下。

新建一个Win32 Application类型的工程06CreateExample,应用程序的种类选择An empty project。工程创建完毕以后,将COMMON目录下所有的文件都添加到工程中。新建两个文件Example.h和Example.cpp,其中Example.h文件包含了两个派生类的定义,Example.cpp文件包含了这两个类的实现代码。

//------------------------ Example.cpp文件---------------------------------//
#include "..\COMMON\_AFXWIN.H"

class CMyApp : public CWinApp 
{
public:
  virtual BOOL InitInstance();
};

class CMyWnd : public CWnd 
{ 
public:
  CMyWnd();
  virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
};

//-----------------------Example.cpp文件--------------------------------//
#include "Example.h"
CMyApp theApp;

///////////////////////////////////////////////
// CMyApp成员函数的实现代码
BOOL CMyApp::InitInstance()
{
  m_pMainWnd = new CMyWnd;
  ::ShowWindow(*m_pMainWnd, this->m_nCmdShow);
  ::UpdateWindow(*m_pMainWnd);
  return TRUE; // 返回TRUE进入消息循环
}

///////////////////////////////////////////////
// CMyWnd成员函数的实现代码
CMyWnd::CMyWnd()
{
  LPCTSTR lpszClassName = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW, 
    ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_3DFACE+1));

  CreateEx(WS_EX_CLIENTEDGE, lpszClassName, 
    "框架程序创建的窗口", WS_OVERLAPPEDWINDOW, 
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL);
}

LRESULT CMyWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  if(message == WM_NCDESTROY)
  {
    ::PostQuitMessage(0);
    delete this;
    return 0;  // CMyWnd对象已经不存在了,必须在这里返回,不能再访问任何非静态成员了
  }
  return Default();
}

运行上面的代码,一个典型的窗口出现了,如图6.3所示。

screenshot

CMyWnd是CWnd的派生类,它重载了虚函数WindowProc以处理AfxWndProc发送给本CMyWnd对象的消息。在窗口的整个生命周期,必须保证CMyWnd对象没有被销毁,否则有关该窗口的消息谁来处理?所以,直到接收到最后一个消息WM_NCDESTROY才可以删除CMyWnd对象。

写这个小例子仅仅是为演示框架程序创建窗口的过程。创建CMyWnd对象时发生的事情有:注册窗口类、安装钩子、创建窗口、子类化窗口、卸载钩子。这些事件完成以后,初始化窗口的工作也就完成了,接着InitInstance函数调用ShowWindow和UpdateWindow函数显示更新窗口。

相关文章
|
6月前
|
消息中间件 编译器 API
Windows窗口程序
Windows窗口程序
|
25天前
|
API Windows
Windows之窗口原理
这篇文章主要介绍了Windows窗口原理和如何使用Windows API创建和管理窗口。
44 0
|
5月前
|
Windows
windows系统vbs脚本 恶搞关不掉的窗口 以及解决办法
windows系统vbs脚本 恶搞关不掉的窗口 以及解决办法
118 2
|
6月前
|
API Python Windows
python3应用windows api对后台程序窗口及桌面截图并保存的方法
python3应用windows api对后台程序窗口及桌面截图并保存的方法
493 1
|
5月前
|
C++ UED 开发者
逆向学习 MFC 篇:视图分割和在 C++ 的 Windows 窗口程序中添加图标的方法
逆向学习 MFC 篇:视图分割和在 C++ 的 Windows 窗口程序中添加图标的方法
71 0
|
6月前
|
Windows
(查看,和保存)windows下通过cmd命令符窗口查看、保存文件目录结构
(查看,和保存)windows下通过cmd命令符窗口查看、保存文件目录结构
182 0
WGCLOUD日常使用 - windows启动server,dos窗口显示乱码的问题处理
首先,这个乱码没有影响,忽略即可 这个是windows窗口编码导致的,不会影响程序运行,server/log下日志文件没有出现乱码,我们主要看日志文件
WGCLOUD日常使用 - windows启动server,dos窗口显示乱码的问题处理
|
API C++ Windows
Windows入门篇一之MSDN手册的使用和第一个窗口程序
Windows入门篇一之MSDN手册的使用和第一个窗口程序
Windows入门篇一之MSDN手册的使用和第一个窗口程序
|
Windows
Windows DOS窗口查看历史执行过的命令的三种方式
Windows DOS窗口查看历史执行过的命令的三种方式
Windows编程基础,第一个Windows程序,注册窗口,创建窗口(下)
Windows编程基础,第一个Windows程序,注册窗口,创建窗口