项目组基于开源的duilib构建桌面应用,虽然这个轻量级的桌面库已经老的快要掉牙,但由于免费又好用,所以还是有不少项目在继续使用。闲来把duilib的消息机制梳理了一遍,了解了核心的窗口CWindowWnd和控件CControlUI,更复杂的控件其实都是在此基础之上进行加工而来的。本以为了解了基础,扩展的控件机理还不是手到擒来,没想到看到的第一个日期控件就花费了不少时间。个人感觉这个控件还是有值得学习的地方。下面逐步分析。
功能组成
CDateTimeUI名称中包含了time,其实只有日期选择功能。查看其源码,可以发现CDateTimeUI继承自CLabelUI,并且还有一个辅助的窗口CDateTimeWnd。整个交互也比较简单,正常状态时,就是显示日期。当鼠标浮动到控件之上时,点击右侧按钮就可以在下拉弹出的窗口选择日期了。
正常状态如下:
获得焦点时状态如下:
CDateTimeUI分析
由于本类继承自CLabelUI,所以我们直接拿它当成一个正常的label来处理就行了。duilib控件的事件接收是在DoEvent中:
if( event.Type == UIEVENT_SETFOCUS && IsEnabled() ) //控件获得焦点时,显示CDateTimeWnd窗口。
{
if( m_pWindow ) return;
m_pWindow = new CDateTimeWnd();
ASSERT(m_pWindow);
m_pWindow->Init(this);
m_pWindow->ShowWindow();
}
//...忽略其他的代码
//鼠标点击时,也显示窗口
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK || event.Type == UIEVENT_RBUTTONDOWN)
{
if( IsEnabled() ) {
GetManager()->ReleaseCapture();
if( IsFocused() && m_pWindow == NULL )
{
m_pWindow = new CDateTimeWnd();
ASSERT(m_pWindow);
}
if( m_pWindow != NULL )
{
m_pWindow->Init(this);
m_pWindow->ShowWindow();
}
}
return;
}
可以看到CDateTimeUI的功能并不复杂,只是在收到消息的时候将CDateTimeWnd显示出来,自己其他的就不用处理了。而且本身也只是个label,也就显示下文本。更复杂的东西还是在CDateTimeWnd。
CDateTimeWnd分析
CDateTimeWnd继承自CWindowWnd,重写的几个函数倒是比较常见。值得关注的是它重写了GetSuperClassName。嗯?这个有什么用呢?
LPCTSTR CDateTimeWnd::GetSuperClassName() const
{
return DATETIMEPICK_CLASS;//这个返回值是个而宏定义,查看其原始值为 SysDateTimePick32,是windows系统窗口类
}
GetSuperClassName什么会用到呢?是在CWindowWnd创建窗口的时候。
HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu)
{
if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL;//如果超类名字非空,则注册超类
if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL;//如果类名不空,则注册类
m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
ASSERT(m_hWnd!=NULL);
return m_hWnd;
}
注册超类又做了什么呢?
bool CWindowWnd::RegisterSuperclass()
{
// Get the class information from an existing
// window so we can subclass it later on...
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
if( !::GetClassInfoEx(NULL, GetSuperClassName(), &wc) ) {//获取超类的信息
if( !::GetClassInfoEx(CPaintManagerUI::GetInstance(), GetSuperClassName(), &wc) ) {
ASSERT(!"Unable to locate window class");
return NULL;
}
}
m_OldWndProc = wc.lpfnWndProc;
wc.lpfnWndProc = CWindowWnd::__ControlProc;//重写超类的回调函数
wc.hInstance = CPaintManagerUI::GetInstance();
wc.lpszClassName = GetWindowClassName();
ATOM ret = ::RegisterClassEx(&wc);//注册类
ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}
原来是将超类的回调函数用自己的回调函数代替,同时给自己注册的类换了一个名字。这种做法在windows称之为窗口超类化,与之对应的概念是窗口子类化。
所以CDateTimeWnd本质上是借用windows系统的窗口类SysDateTimePick32来实现日期显示和选择功能。明白了这个关系,后续要处理的就是SysDateTimePick32与我们的label之间的信息传递和交互了。
交互分析
从CDateTimeUI到CDateTimeWnd
我们已经知道当CDateTimeUI获取焦点或者被点击时,将会去显示CDateTimeWnd。同时还需要传递当前选择的日期
void CDateTimeWnd::Init(CDateTimeUI* pOwner)
{
m_pOwner = pOwner;
m_pOwner->m_nDTUpdateFlag = DT_NONE;
if (m_hWnd == NULL)
{
RECT rcPos = CalPos();//计算控件位置,其实就是CDateTimeUI的位置
UINT uStyle = WS_CHILD;
Create(m_pOwner->GetManager()->GetPaintWindow(), NULL, uStyle, 0, rcPos);
SetWindowFont(m_hWnd, m_pOwner->GetManager()->GetFontInfo(m_pOwner->GetFont())->hFont, TRUE);
}
if (m_pOwner->GetText().IsEmpty())
::GetLocalTime(&m_pOwner->m_sysTime);
::SendMessage(m_hWnd, DTM_SETSYSTEMTIME, 0, (LPARAM)&m_pOwner->m_sysTime);//将当前的日期传递给SysDateTimePick32
::ShowWindow(m_hWnd, SW_SHOWNOACTIVATE);//初始化显示的时候,不会激活,这是个比较巧妙的设计
::SetFocus(m_hWnd);
m_bInit = true;
}
从CDateTimeWnd到CDateTimeUI
反之,当CDateTimeWnd失去焦点时,需要将文本写回CDateTimeUI
LRESULT CDateTimeWnd::OnKillFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
LRESULT lRes = ::DefWindowProc(m_hWnd, uMsg, wParam, lParam);
if (m_pOwner->m_nDTUpdateFlag == DT_NONE)
{
::SendMessage(m_hWnd, DTM_GETSYSTEMTIME, 0, (LPARAM)&m_pOwner->m_sysTime);//还是通过发送信息的方式
m_pOwner->m_nDTUpdateFlag = DT_UPDATE;
m_pOwner->UpdateText();//更新文本
}
if ((HWND)wParam != m_pOwner->GetManager()->GetPaintWindow()) {
::SendMessage(m_pOwner->GetManager()->GetPaintWindow(), WM_KILLFOCUS, wParam, lParam);
}
SendMessage(WM_CLOSE);
return lRes;
}
其他注意事项
Last but not least。还有一些需要注意的问题:
焦点处理
CDateTimeWnd在创建的时候,就将自己加入到了CPaintManagerUI的m_aNativeWindow列表中。
if( uMsg == WM_PAINT) {
if (m_pOwner->GetManager()->IsLayered()) {
m_pOwner->GetManager()->AddNativeWindow(m_pOwner, m_hWnd);
}
而CPaintManagerUI处理WM_KILLFOCUS时,如果判断CDateTimeWnd失去了焦点,则设置CDateTimeUI获得焦点。
参考文章
关于windows窗口子类化、超类化,这篇文章的描述比较清楚明了。
眼见为实(2):介绍Windows的窗口、消息、子类化和超类化_wx5b7658e51ef04的技术博客_51CTO博客
关于如何添加窗口数据,实现窗口子类化等场景,这篇文章比较精彩。