DuiLib CDateTimeUI的创建分析
  OX6Uyrmjc2Ik 2023年11月02日 52 0

项目组基于开源的duilib构建桌面应用,虽然这个轻量级的桌面库已经老的快要掉牙,但由于免费又好用,所以还是有不少项目在继续使用。闲来把duilib的消息机制梳理了一遍,了解了核心的窗口CWindowWnd和控件CControlUI,更复杂的控件其实都是在此基础之上进行加工而来的。本以为了解了基础,扩展的控件机理还不是手到擒来,没想到看到的第一个日期控件就花费了不少时间。个人感觉这个控件还是有值得学习的地方。下面逐步分析。

功能组成

CDateTimeUI名称中包含了time,其实只有日期选择功能。查看其源码,可以发现CDateTimeUI继承自CLabelUI,并且还有一个辅助的窗口CDateTimeWnd。整个交互也比较简单,正常状态时,就是显示日期。当鼠标浮动到控件之上时,点击右侧按钮就可以在下拉弹出的窗口选择日期了。

正常状态如下:

DuiLib  CDateTimeUI的创建分析_日期控件

获得焦点时状态如下:

DuiLib  CDateTimeUI的创建分析_duilib_02

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博客

关于如何添加窗口数据,实现窗口子类化等场景,这篇文章比较精彩。

Windows窗口对象的附加数据 - 腾讯云开发者社区-腾讯云 (tencent.com)

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读