Button是我们遇到的最多的控件之一,因此以它来作为入手控件还是很合适的。
通常我们见到的Button都是有窗口控件,当然也有很多优秀的库为了提高效率做成无窗口控件。所有有窗口和无窗口就是是否控件和一个HWND句柄相关联。我们还是以常规的有窗口控件来讲述下吧。
在CreateWindow函数的参数中,我们注意到还有几个的参数我们没用过,其中一个就是hWndParent。在创建普通窗口时该参数我们都设成了NULL,但现在我们需要用到了。从名字我们就看出hWndParent指的就是父窗口句柄。按钮控件作为一个窗口不能独立存在,它是依附于在别的窗口上的,也就是其父窗口。因此,我们在hWndParent参数需要指定。
另外,还有dwStyle,其是DWORD类型。查看msdn可以看出有一种类型值WS_CHILD,解释中也是告诉Windows创建一个子窗口。
注:这里还有一个概念,windows窗口有owned-owner和child-parent两种关系,我们普通的按钮和窗口就是后者,而模态对话框和调用显示其的窗口就是owned-owner的关系。具体二者有什么区别可额外查询下资料。当仅指定hParent时就是owned-owner关系;当同时指定hParent和WS_CHILD时就是child-parent的关系。
有了这两个参数后创建按钮控件算是完成了第一步。下面就还有一个问题:注册一个什么样的WNDCLASS呢?不然怎么创建呢?
Windows为用户预定义并注册了一些控件的WndClass,其中包括一些常用的按钮(BUTTON)、ListBox等等。因此我们也就没必要在重新声明注册按钮控件窗口类型了。
到这里可能你会发现还有一个问题没解决,WNDCLASS都会指定一个窗口过程函数地址,但windows注册的窗口怎么能知道我们所需要的函数地址呢?怎么才能让其执行到我们的控件窗口过程函数中去呢?这里就引入了ATL中的SUPERCLASS。
ATL会有这么一个宏:DECLARE_WND_SUPERCLASS。看了其定义就发现其有两个参数,一个是我们想注册的控件名称(比如叫“XButton”),另外一个参数就是其所希望继承的窗口类型。SuperClass的意思就是在注册窗口时能够继承一个已经注册过的窗口类型(也就是克隆技术),同时窗口过程函数可以使用我们自定义的。这样我们的问题也就解决了,剩下就是利用windows的api来怎么实现了。
题外话: 需要注意的是,superclass是在注册窗口类时就改变了窗口的行为。WTL中还有subclass(子类化),其是在窗口创建完毕后,通过修改窗口函数的地址等改变一个窗口的行为的。 |
先看看我们定义的控件基类吧,它和XWindowImpl差不多,区别就是在注册的时候不一样了。
template<class T>
class XWindowComponentImpl: public XWindow
{
XThunk* m_pThunk;
public:
CString m_strTitle; //多了点成员变量
RECT m_rect;
HWND m_hParent;
public:
XWindowComponentImpl()
{
CWindowImpl
}
HWND Create(
LPCTSTR szSuperClass,
LPCTSTR szTitle,
RECT* pRect = nullptr,
HWND hParent = nullptr,
DWORD dwStyle = WS_OVERLAPPEDWINDOW,
DWORD dwStyleEx = 0)
{
m_strTitle = CString(szTitle);
m_rect = *pRect;
m_hParent = hParent;
WNDCLASSEX wc;
if (!::GetClassInfoEx(_XModule.GetInstance(), szSuperClass, &wc))
{
ATLASSERT(FALSE);
}
wc.lpszClassName = T::GetXWndClassName(); // 创建wndclass也不一样
wc.lpfnWndProc = &StartXWndProc;
wc.cbSize = sizeof(wc);
ATOM ato = ::RegisterClassEx(&wc);
RECT rc;
if (pRect == nullptr)
rc = rcDefault;
else
rc = *pRect;
m_pThunk = (XThunk *)VirtualAlloc(NULL, sizeof(XThunk), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
_XModule.m_pCurWnd = (void*)this;
HWND hWnd = ::CreateWindow(T::GetXWndClassName(), szTitle, wc.style | WS_CHILD,
rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top,
hParent, nullptr, _XModule.GetInstance(), nullptr);
ATLASSUME(m_hWnd == hWnd);
return m_hWnd;
}
protected:
static LRESULT CALLBACK StartXWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
ATLASSERT(_XModule.m_pCurWnd != nullptr);
XWindowComponentImpl<T>* pThis = (XWindowComponentImpl<T>*)_XModule.m_pCurWnd;
WNDPROC pWndProc = (WNDPROC)pThis->m_pThunk;
pThis->m_pThunk->Init((DWORD)pThis, (DWORD)&XWindowComponentImpl<T>::RealXWndProc);
pThis->m_hWnd = hWnd;
;
SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);
return pWndProc(hWnd, uMsg, wParam, lParam);
}
static LRESULT CALLBACK RealXWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
XWindowComponentImpl<T>* pThis = (XWindowComponentImpl<T>*)hWnd;
return pThis->ProcessXWndMessage(uMsg, wParam, lParam);
}
public:
virtual LRESULT ProcessXWndMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return ::DefWindowProc(this->m_hWnd, uMsg, wParam, lParam);
}
};
代码有点多,我们就把核心部分挑出来说明下吧!
首先就是注册控件窗口:
WNDCLASSEX wc;
if (!::GetClassInfoEx(_XModule.GetInstance(), szSuperClass, &wc))
{
ATLASSERT(FALSE);
}
wc.lpszClassName = T::GetXWndClassName();
wc.lpfnWndProc = &StartXWndProc;
wc.cbSize = sizeof(wc);
ATOM ato = ::RegisterClassEx(&wc);
这里面用到了函数GetClassInfoEx(),整个过程就是先获取已经注册了的superclass窗口的WNDCLASS信息,然后将名字和过程函数替换成我们的,再注册就ok了。Atl中会有一堆代码封装的比较完善,但流程也就这个意思,理解这个再看ATL会很easy了。
然后再Create的时候Style参数要设成wc.style | WS_CHILD就ok了。意思就是首先保持注册窗口的style再加上子窗口类别。
其他代码就不用再解释了,前面章节已经说得比较详细了。
起航,创建一个按钮控件
有了基类就不怕了!上代码
class XButton : public XWindowComponentImpl<XButton>
{
public:
XButton()
{
}
static LPCTSTR GetXWndClassName()
{
return _T("XButton");
}
public:
LRESULT ProcessXWndMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
switch(uMsg)
{
case WM_LBUTTONDOWN:
::SendMessage(m_hParent, WM_COMMAND, 2013,0);
break;
case WM_PAINT:
hdc = BeginPaint(m_hWnd, &ps);
Rectangle(hdc,m_rect.left, m_rect.top, m_rect.right,m_rect.bottom);
TextOut(hdc,20,20,(LPCTSTR)m_strTitle, m_strTitle.GetLength());
EndPaint(m_hWnd, &ps);
break;
case WM_CLOSE:
DestroyWindow();
break;
default:
return ::DefWindowProc(this->m_hWnd, uMsg, wParam, lParam);
}
return 0;
}
};
很简单吧,也就ProcessXWndMessage中多了一个WM_PAINT消息的处理,他就是绘制我们的按钮,为了简单我们就只画个框和文字了。具体代码就不再解释了。
另外,还有一个消息WM_LBUTTONDOWN需要处理,不然上层的SimpleWindow怎么知道我们点了按钮呢?该消息工作就是通知父窗口并发送一个WM_COMMAND消息。WM_COMMAND消息的两个参数内容可以查看msdn。为了简化,我们就直接把WPARAM的值传入button的ID(整型值)。这个ID应该不陌生了,MFC中都会定义resource.h,里面就是放了这种数值。
好了,下面就把这个按钮放到我们的窗口中试试:
首先定义一个按钮成员XButton m_btn;
然后在哪创建呢?就在WM_CREATE中吧。
最后就是接收按钮的按下消息并响应了,这个就是在WM_COMMAND消息中,并判断下WPARAM的ID。具体代码如下:
LRESULT ProcessXWndMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rc = {10,10,125,40};
switch(uMsg)
{
case WM_COMMAND:
if (wParam == 2013)
{
HDC dc = ::GetDC(m_hWnd);
CString strInfo(L"You have pushed the button");
TextOut(dc,20,100,strInfo, strInfo.GetLength());
}
case WM_CREATE:
m_btn.Create(_T("BUTTON"),L" I am Button", &rc, m_hWnd );
m_btn.ShowWindow(SW_SHOW);
m_btn.UpdateWindow();
break;
case WM_CLOSE:
DestroyWindow();
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return ::DefWindowProc(this->m_hWnd, uMsg, wParam, lParam);
}
return 0;
}
最后,编译运行。我们很高兴按钮出来了,点击后会发现下面多出了一行文字。
最后,还要说一点:
我们使用MFC、ATL中窗口的处理各种消息,包括按钮等时经常会用到宏,也就是我们常说的消息映射,类似如下:
BEGIN_MESSAGE_MAP(class)
COMMAND_MESSAGE(ID1,FUNC1)
COMMAND_MESSAGE(ID2,FUNC2)
WINDOW_MESSAGE(WM_XXXX, FUNC3)
… …
END_MESSAGE_MAP()
假如我们展开宏就会发现:
BEGIN_MESSAGE_MAP也就是定义窗口成员函数ProcessXWndMessage;
COMMAND_MESSAGE就是处理WM_COMMAND,内部会有一堆的cass,并调用FUNCx;
WINDOW_MESSAGE就是处理其他WM_XXX消息,然后调用指定的FUNCi;
至此,窗口封装就算有所小成了。