[ATL/WTL]_[初级]_[关于自定义容器窗口和调用CreateWindowEx创建窗口的区别]

场景

  1. 在开发WTL程序时,有时候会需要一些简单的容器窗口来存放子控件(窗口),有了容器窗口,可以通过管理容器窗口的大小,显示隐藏等来统一管理它的所有子窗口,那么WTL有提供容器窗口吗?能否不新建一个子类通过组合的方式来管理子控件?

说明

  1. 目前WTL没有发现可以直接使用的容器窗口,但是可以通过CreateWindowExCWindow::Create来创建容器窗口。
// 通过CreateWindowEx或者是CWindow::Create创建
	const wchar_t CLASS_NAME[]  = L"Sample Window Class";
	WNDCLASS wc = {0};
	wc.lpfnWndProc   = ::DefWindowProc;
	wc.hInstance     = _AtlBaseModule.GetModuleInstance();
	wc.lpszClassName = CLASS_NAME;
	wc.hbrBackground = ::AtlGetStockBrush(GRAY_BRUSH);
	RegisterClass(&wc);
	CWindow w;
	w.Create(CLASS_NAME,m_hWnd,CRect(100,210,250,310),
		NULL,WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
	HWND Create(
		_In_opt_z_ LPCTSTR lpstrWndClass,
		_In_opt_ HWND hWndParent,
		_In_ _U_RECT rect = NULL,
		_In_opt_z_ LPCTSTR szWindowName = NULL,
		_In_ DWORD dwStyle = 0,
		_In_ DWORD dwExStyle = 0,
		_In_ _U_MENUorID MenuOrID = 0U,
		_In_opt_ LPVOID lpCreateParam = NULL) throw()
	{
		ATLASSUME(m_hWnd == NULL);
		if(rect.m_lpRect == NULL)
			rect.m_lpRect = &rcDefault;
		m_hWnd = ::CreateWindowEx(dwExStyle, lpstrWndClass, szWindowName,
			dwStyle, rect.m_lpRect->left, rect.m_lpRect->top, rect.m_lpRect->right - rect.m_lpRect->left,
			rect.m_lpRect->bottom - rect.m_lpRect->top, hWndParent, MenuOrID.m_hMenu,
			_AtlBaseModule.GetModuleInstance(), lpCreateParam);
		return m_hWnd;
	}

问题

  1. 这种通过原始的Win32函数创建窗口的方式是有限制的。比如,如果我们有一个自定义的按钮,并处理类似WM_NOTIFY消息的通知BCN_HOTITEMCHANGE,这个消息是需要父窗口通过REFLECT_NOTIFICATIONS来转发到子窗口的.不然的话子窗口接收不到这个通知.
class DhContainerDelegate : public CWindowImpl<DhContainerDelegate>
{
public:
	~DhContainerDelegate(){}
	BEGIN_MSG_MAP_EX(DhContainerDelegate)
		REFLECT_NOTIFICATIONS()
	END_MSG_MAP()
};
  1. 这个BCN_HOTITEMCHANGE通知触发的流程是以下。
子CButton接受到鼠标消息->子窗口把WM_NOTIFY消息转发给父窗口->父窗口转换为OCM__BASE+uMsg再转发给子窗口

在类里的声明 ATL::CWindowImplRoot<TBase>。

template <class TBase>
LRESULT CWindowImplRoot< TBase >::ReflectNotifications(
	_In_ UINT uMsg,
	_In_ WPARAM wParam,
	_In_ LPARAM lParam,
	_Out_ BOOL& bHandled)
{
	HWND hWndChild = NULL;

	switch(uMsg)
	{
	case WM_COMMAND:
		if(lParam != NULL)	// not from a menu
			hWndChild = (HWND)lParam;
		break;
	case WM_NOTIFY:
		hWndChild = ((LPNMHDR)lParam)->hwndFrom;
		break;
	case WM_PARENTNOTIFY:
		switch(LOWORD(wParam))
		{
		case WM_CREATE:
		case WM_DESTROY:
			hWndChild = (HWND)lParam;
			break;
		default:
			hWndChild = GetDlgItem(HIWORD(wParam));
			break;
		}
		break;
	case WM_DRAWITEM:
		if(wParam)	// not from a menu
			hWndChild = ((LPDRAWITEMSTRUCT)lParam)->hwndItem;
		break;
	case WM_MEASUREITEM:
		if(wParam)	// not from a menu
			hWndChild = GetDlgItem(((LPMEASUREITEMSTRUCT)lParam)->CtlID);
		break;
	case WM_COMPAREITEM:
		if(wParam)	// not from a menu
			hWndChild =  ((LPCOMPAREITEMSTRUCT)lParam)->hwndItem;
		break;
	case WM_DELETEITEM:
		if(wParam)	// not from a menu
			hWndChild =  ((LPDELETEITEMSTRUCT)lParam)->hwndItem;

		break;
	case WM_VKEYTOITEM:
	case WM_CHARTOITEM:
	case WM_HSCROLL:
	case WM_VSCROLL:
		hWndChild = (HWND)lParam;
		break;
	case WM_CTLCOLORBTN:
	case WM_CTLCOLORDLG:
	case WM_CTLCOLOREDIT:
	case WM_CTLCOLORLISTBOX:
	case WM_CTLCOLORMSGBOX:
	case WM_CTLCOLORSCROLLBAR:
	case WM_CTLCOLORSTATIC:
		hWndChild = (HWND)lParam;
		break;
	default:
		break;
	}

	if(hWndChild == NULL)
	{
		bHandled = FALSE;
		return 1;
	}

	ATLASSERT(::IsWindow(hWndChild));
	return ::SendMessage(hWndChild, OCM__BASE + uMsg, wParam, lParam);
}
  1. 因此要进行通知转发,那么必须使用<atlwin.h>里声明的宏REFLECT_NOTIFICATIONS,而这个宏的实现只在实现类ATL::CWindowImplRoot里,CWindow类是没有处理的。也就是说我们通过CWindow或者直接CreateWindowEx创建的容器窗口不会处理通知转发,只能直接通过自定义窗口处理函数来解决,也就是需要自定义lpfnWndProc窗口处理函数。如果要写这类代码无疑会浪费时间,因此CWindow::CreateCreateWindowEx方式虽然不需要新建类文件,但是不能处理通知,用处不大。

方案

  1. 要处理通知转发,只能新建一个容器子类,并添加通知转发来解决以上的问题。

dh_container_delegate.h


#ifndef __DH_CONTAINER_DELEGATE
#define __DH_CONTAINER_DELEGATE

#include <Windows.h>
#include "atlcrack.h"

class DhContainerDelegate : public CWindowImpl<DhContainerDelegate>
{
public:
	~DhContainerDelegate(){}
	BEGIN_MSG_MAP_EX(DhContainerDelegate)
		REFLECT_NOTIFICATIONS()
	END_MSG_MAP()
};

#endif

dh_container_delegate.cpp

#include "stdafx.h"
#include "dh_container_delegate.h"

调用方式

auto contentWindow = new DhContainerDelegate();
	// 1.设置白色背景.不过这个范围是类静态存储区的,因为它的 CWndClassInfo 是局部静态变量.
contentWindow->GetWndClassInfo().m_wc.hbrBackground = ::AtlGetStockBrush(WHITE_BRUSH);
auto u = contentWindow->Create(m_hWnd,CRect(100,100,250,200),
	NULL,WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN | WS_CLIPSIBLINGS);

例子

  1. 以下例子是分别使用DhContainerDelegate容器窗口和CreateWindowEx来说明自定义按钮的响应鼠标问题。按钮例子调用了[实用的自定义按钮][1]

View.h

// View.h : interface of the CView class
//
/

#pragma once

typedef enum CViewWindowId1{
	kCViewButtonMinId = 5000,
	kCViewButtonMaxId = 5010
}CViewWindowId;

class CView : public CWindowImpl<CView>
{
public:
	DECLARE_WND_CLASS(NULL)

	BOOL PreTranslateMessage(MSG* pMsg);

	BEGIN_MSG_MAP_EX(CView)
		MSG_WM_CREATE(OnCreate)
		MESSAGE_HANDLER(WM_PAINT, OnPaint)
		COMMAND_RANGE_HANDLER_EX(kCViewButtonMinId,kCViewButtonMaxId,OnCommandIDHandlerEX)
		REFLECT_NOTIFICATIONS()
	END_MSG_MAP()

// Handler prototypes (uncomment arguments if needed):
//	LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
//	LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
//	LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)

protected:
	int OnCreate(LPCREATESTRUCT lpCreateStruct);
	LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
	void OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl);
private:

};

View.cpp

// View.cpp : implementation of the CView class
//
/

#include "stdafx.h"
#include "resource.h"
#include <string>
#include <GdiPlus.h>
#include "View.h"
#include "dh_container_delegate.h"
#include "atlmisc.h"
#include "tool/utils.h"
#include "view/dh_img_button.h"

using namespace std;
using namespace Gdiplus;


wstring GetDllModuleDir(HMODULE module)
{
	static wchar_t szbuf[MAX_PATH];  
	::GetModuleFileNameW(module,szbuf,MAX_PATH);  
	::PathRemoveFileSpecW(szbuf);
	int length = lstrlen(szbuf);
	szbuf[length] = L'\\';
	szbuf[length+1] = 0;
	return szbuf;
}

void CView::OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	wchar_t buf[64] = {0};
	wsprintf(buf,L"Click Window ID %d",nID);
	MessageBox(buf,L"OnCommandIDHandlerEX");
}

int CView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	auto contentWindow = new DhContainerDelegate();
	// 1.设置白色背景.不过这个范围是类静态存储区的,因为它的 CWndClassInfo 是局部静态变量.
	contentWindow->GetWndClassInfo().m_wc.hbrBackground = ::AtlGetStockBrush(WHITE_BRUSH);
	auto u = contentWindow->Create(m_hWnd,CRect(100,100,250,200),
		NULL,WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN | WS_CLIPSIBLINGS);

	// 2.通过CreateWindowEx或者是CWindow::Create创建
	const wchar_t CLASS_NAME[]  = L"Sample Window Class";
	WNDCLASS wc = {0};
	wc.lpfnWndProc   = ::DefWindowProc;
	wc.hInstance     = _AtlBaseModule.GetModuleInstance();
	wc.lpszClassName = CLASS_NAME;
	wc.hbrBackground = ::AtlGetStockBrush(GRAY_BRUSH);
	RegisterClass(&wc);
	CWindow w;
	w.Create(CLASS_NAME,m_hWnd,CRect(100,210,250,310),
		NULL,WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN | WS_CLIPSIBLINGS);

	// 3. 在容器上添加按钮.
	Font* font = Utils::GetFont(GetDC(),16,1,true);
	auto binDir = GetDllModuleDir(NULL);
	auto imageNormal = new Bitmap((binDir+L"home.png").c_str());

	auto funcAddButton = [this,&lpCreateStruct,&font](HWND parent,Bitmap* normal,Bitmap* pressed,
		int _id,const wchar_t* desc,CRect rect)->DhImgButton*{
		auto button = new DhImgButton();
		button->Create(parent,rect,NULL,WS_VISIBLE|WS_CHILD,0,_id);
		button->SetFont(font);
		button->SetImages(normal,pressed,pressed,normal);
		button->SetColorOfNormalStatus(RGB(91,155,213),RGB(255,255,255),RGB(91,155,213));
		button->SetColorOfPressedStatus(RGB(41,90,135),RGB(128,198,244),RGB(41,90,135));
		button->SetColorOfHoverStatus(RGB(41,90,135),RGB(128,198,244),RGB(41,90,135));
		
		button->SetImageAndTextGap(6);
		button->Center(true);
		::SetWindowText(*button,desc);
		return button;
	};

	auto color = GetSysColor(COLOR_WINDOW);

	int _id = kCViewButtonMinId;
	auto button = funcAddButton(*contentWindow,imageNormal,imageNormal,_id++,L"Open",CRect(10,10,100,40));
	button->SetHMargin(4);
	button->SetVMargin(4);
	button->SetFixWidth(true);
	button->SetImgAndTextLayoutType(kImgButtonImgAndTextLayoutHorizontal);
	button->Load(true);

	button = funcAddButton(w,imageNormal,imageNormal,_id++,L"Open",CRect(10,10,100,40));
	button->SetHMargin(4);
	button->SetVMargin(4);
	button->SetFixWidth(true);
	button->SetImgAndTextLayoutType(kImgButtonImgAndTextLayoutHorizontal);
	button->Load(true);

	return 0;
}

BOOL CView::PreTranslateMessage(MSG* pMsg)
{
	pMsg;
	return FALSE;
}

LRESULT CView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
	CPaintDC dc(m_hWnd);

	//TODO: Add your drawing code here

	return 0;
}

图示

[ATL/WTL]_[初级]_[关于自定义容器窗口和调用CreateWindowEx创建窗口的区别]
[ATL/WTL]_[初级]_[关于自定义容器窗口和调用CreateWindowEx创建窗口的区别]
主线程的调用堆栈可看出消息的转发过程。
[ATL/WTL]_[初级]_[关于自定义容器窗口和调用CreateWindowEx创建窗口的区别]

下载

  1. 使用VS2010以上版本打开,项目自带了WTL10库.
    https://download.csdn.net/download/infoworld/15451353

参考

  1. 实用的自定义按钮
  2. creating-a-window
上一篇:wm_concat与pivot的区别


下一篇:c#中将WM_CLOSE消息发送到没有窗口的进程的方法