修复duilib CEditUI控件和CWebBrowserUI控件中按Tab键无法切换焦点的bug

转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/41556615

        在duilib中,按tab键会让焦点在Button一类的控件中切换,但是切换焦点一直存在bug,具体的描述如下:

        1、在主窗体里弹出新的窗体,当新窗体中存在CEditUI控件并且焦点在此CEditUI控件上,那么按tab键将无法切换焦点而一直处于CEditUI中。(只在新窗体中有此bug,主创体中没有,原因会在后面分析)

        2、CWebBrowserUI控件同CEditUI

        之间在群里就看到有人问这个问题,而且也一直没解决。

       这几天在用duilib写一个注册界面时(如图,此页面便是在主窗体上面的一个弹出窗体),上面有多个CEditUI控件,按照我们的习惯,输入完第一个edit的内容后会按tab切换到下一个edit。而由于duilib的bug导致这个焦点无法切换。我自己一般是需要什么功能就摸索什么功能,之前用duilib是没有遇到edit切换焦点的需求,所以就没有考虑过这个bug,今天碰到了这个需求,就得先解决这个bug了。

       

分析过程一:


        很明显可以看出来,这个bug只存在于CEditUI和CWebBrowserUI控件中,而这两个控件与其他控件的区别就在于他们都是用了原生的wini32控件,我这里就只分析CEditUI控件了。

        在CEditUI控件的源码里可以很容易看到,当他的DoEvent函数里收到获取焦点的UIEVENT_SETFOCUS消息或者鼠标按下的UIEVENT_BUTTONDOWN消息后,他就会创建一个子窗体并且维护这个子窗体的相关数据。而这个子窗体会自动通过CreateWindowEx函数创建一个原生的win32的edit控件,当子窗体失去焦点时自动销毁自身,这也就是CEditUI控件的实现原理。

       焦点切换的处理是由CPaintManager类管理的,当我们在界面中按下Tab键打算切换焦点后,CPaintManager会拦截键盘消息然后去管理焦点切换,那么我修复起点就从焦点管理函数开始。焦点管理的函数是PreMessageHandler,原型如下:

bool CPaintManagerUI::PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& /*lRes*/)
{
	for( int i = 0; i < m_aPreMessageFilters.GetSize(); i++ )
	{
		bool bHandled = false;
		LRESULT lResult = static_cast<IMessageFilterUI*>(m_aPreMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
		if( bHandled ) {
			return true;
		}
	}
	switch( uMsg ) {
	case WM_KEYDOWN:
		{
			// Tabbing between controls
			if( wParam == VK_TAB ) {
				if( m_pFocus && m_pFocus->IsVisible() && m_pFocus->IsEnabled() && _tcsstr(m_pFocus->GetClass(), _T("RichEditUI")) != NULL ) {
					if( static_cast<CRichEditUI*>(m_pFocus)->IsWantTab() ) return false;
				}
				SetNextTabControl(::GetKeyState(VK_SHIFT) >= 0);
				return true;

			}
		}
		break;
	//....省略无用代码
}

        可以看到函数里接活VK_TAB按键后,会去调用SetNtextTabControl函数去设置下一个控件获取焦点,然后返回true。而SetNtextTabControl函数的原型如下:

bool CPaintManagerUI::SetNextTabControl(bool bForward)
{
    // If we're in the process of restructuring the layout we can delay the
    // focus calulation until the next repaint.
    if( m_bUpdateNeeded && bForward ) {
        m_bFocusNeeded = true;
        ::InvalidateRect(m_hWndPaint, NULL, FALSE);
        return true;
    }
    // Find next/previous tabbable control
    FINDTABINFO info1 = { 0 };
    info1.pFocus = m_pFocus;
    info1.bForward = bForward;
    CControlUI* pControl = m_pRoot->FindControl(__FindControlFromTab, &info1, UIFIND_VISIBLE | UIFIND_ENABLED | UIFIND_ME_FIRST);
    if( pControl == NULL ) {
        if( bForward ) {
            // Wrap around
            FINDTABINFO info2 = { 0 };
            info2.pFocus = bForward ? NULL : info1.pLast;
            info2.bForward = bForward;
            pControl = m_pRoot->FindControl(__FindControlFromTab, &info2, UIFIND_VISIBLE | UIFIND_ENABLED | UIFIND_ME_FIRST);
        }
        else {
            pControl = info1.pLast;
        }
    }
    if( pControl != NULL ) SetFocus(pControl);
    m_bFocusNeeded = false;
    return true;
}

       函数里调用FindControl函数,根据__FindControlFromTab函数和bForward参数来决定搜索下一个焦点的控件,__FindControlFromTab函数的代码我就不分析了,当找到了下一个应该获取焦点的控件后,调用CPaintManager的SetFocus函数让新控件获取焦点。而SetFocus函数里,首先对旧的获取焦点的控件发送UIEVENT_KILLFOCUS消息让他失去焦点,然后将新的获取焦点的控件指针赋值给m_pFocus变量(CPaintManager中保存当前获取焦点的控件指针的成员变量),并且给新的获取焦点的控件发送UIEVENT_SETFOCUS消息让他获取焦点。

      从代码中看,理论上没有什么问题,我就针对CEditUI来进行修改。在CEditUI的内嵌子窗体类CEditWnd中的HandleMessage函数里加入如下代码,让CEditWnd收到Tab消息后来主动调用CPaintManager的SetNextTabControl函数来切换焦点:

 		else if( uMsg == WM_CHAR ){
 			if(TCHAR(wParam) == VK_TAB)
 			{
 				m_pOwner->GetManager()->SetNextTabControl(::GetKeyState(VK_SHIFT) >= 0);
 			}
 			else
 				bHandled = FALSE;

 		}

      这样修改后还不起作用,原因是PreMessageHandler函数中处理WM_KEYDOWN消息后直接reutrn true导致了消息的截断,从而无法传递到CEditWnd,所以再把return true语句注释掉,这时会惊喜的发现,可以切换焦点了!

分析过程二:


      这样稀里糊涂的修复了bug,并且测试正常。但是我心里很疑惑为什么这样在CEditWnd里面调用SetNextTabControl可以切换焦点但是在CpaintManager的PreMessageHandler里面调用SetNextTabControl函数却失效。而且这也无法解释为什么这个bug只存在于弹出窗体而不是主窗体中,后来才意识到问题的原因根本不在于CEditWnd和PreMessageHandler!

      接着分析过程一之后,我一直调试SetNextTabControl函数和SetFocus函数,下了很多条件断点和数据断点,试图找到在CPaintManager的PreMessageHandler里面调用SetNextTabControl函数失效的原因。最后发现执行PreMessageHandler的CpaintManager类根本不是弹出窗体的CPaintManager,而是主窗体的CPaintManager!主窗体的CPaintManager调用了SetNextTabControl,他是给主窗体的控件切换了焦点!而弹出的子窗体的CPaintManager根本没有执行PreMessageHandler函数,所以他的SetNextTabControl失效了,而我稀里糊涂的在CEditWnd里面调用了SetNextTabControl歪打正着的调用了弹出窗体的SetNextTabControl。这就解析了分析过程一中为什么看上去修复了bug。

      那么现在就要分析一下为什么明明在弹出窗体中按了Tab键,最后调用的却是主窗体的PreMessageHandler函数。

      这要从duilib的最底层消息处理函数说起,他是所以duilib程序消息的起点。duilib的最底层消息处理函数有两个,一个是CWindowWnd类的ShowModal函数,一个是CPaintManager类的MessageLoop函数,这两个函数有一个共同点,共同的代码如下:

        while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) {
        if( msg.message == WM_CLOSE && msg.hwnd == m_hWnd ) {
            nRet = msg.wParam;
            ::EnableWindow(hWndParent, TRUE);
            ::SetFocus(hWndParent);
        }
        if( !CPaintManagerUI::TranslateMessage(&msg) ) {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);
        }
        if( msg.message == WM_QUIT ) break;
    }

      大家都知道win32程序的消息需要先调用GetMessage,然后调用win32的TranslateMessage和DispatchMessage函数来分派消息。而duililb在win32的TranslateMessage之前先调用了CPaintManager中的一个名为TranslateMessage的静态函数来过滤消息。而这个TranslateMessage才是bug的出处!他的代码如下:

<pre name="code" class="cpp">bool CPaintManagerUI::TranslateMessage(const LPMSG pMsg)
{
	// Pretranslate Message takes care of system-wide messages, such as
	// tabbing and shortcut key-combos. We'll look for all messages for
	// each window and any child control attached.
	UINT uStyle = GetWindowStyle(pMsg->hwnd);
	UINT uChildRes = uStyle & WS_CHILD;
	LRESULT lRes = 0;
	if (uChildRes != 0)
	{
		HWND hWndParent = ::GetParent(pMsg->hwnd);

		for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
		{
			CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
			HWND hTempParent = hWndParent;
			while(hTempParent)
			{

				if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
				{
					if (pT->TranslateAccelerator(pMsg))
						return true;

 					if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
 						return true;

 					return false;
				}
				hTempParent = GetParent(hTempParent);
			}

		}
	}
	else
	{
		for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
		{
			int size = m_aPreMessages.GetSize();
			CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
			if(pMsg->hwnd == pT->GetPaintWindow())
			{
				if (pT->TranslateAccelerator(pMsg))
					return true;

				if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
					return true;

				return false;
			}
		}
	}
	return false;
}

       我来分析一下导致bug的原因。首先说一下当窗体中没有CEditUI或者CWebBrowserUI控件的情况。函数进入后调用者两行代码判断发送消息的窗体是不是子窗体

	UINT uStyle = GetWindowStyle(pMsg->hwnd);
	UINT uChildRes = uStyle & WS_CHILD;	

      如果没有CEditUI或者CWebBrowserUI控件,通常情况下就不会有子窗体,那么TranslateMessage往下执行后if (uChildRes != 0)判断就不会成功,也就是会调用else里面的代码。在else里面,会遍历m_aPreMessages数组中的元素(m_aPreMessages是全局变量,里面保存了所有窗体的CPaintManager对象的指针),然后调用每个元素的PreMessageHandler函数,直到消息被处理。

      而如果包含CEditUI或者CWebBrowserUI控件,那么他们内部就会创建win32原生的控件(也就是子窗体),那么if
(uChildRes != 0)判断就会成功,任然是依次遍历m_aPreMessages数组的元素,但是代码有些不同

			CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
			HWND hTempParent = hWndParent;
			while(hTempParent)
			{

				if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
				{
					if (pT->TranslateAccelerator(pMsg))
						return true;

 					if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
 						return true;

 					return false;
				}
				hTempParent = GetParent(hTempParent);
			}

         其中的hTempParent句柄会在while循环中被GetParent函数修改。问题就在这里了!当遍历到m_aPreMessages的的元素,也就是主窗体的CPaintManager时

if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())

           

         这句代码的hTempParent == pT->GetPaintWindow()会被判断为成功,因为win32原生控件句柄多次GetParent后就会得到主窗体的句柄,这时hTempParent的值就和m_aPreMessages的第一个元素,也就是pT->GetPaintWindow()的结构相同。

     

         判断成功后,会调用pT->PreMessageHandler,执行主窗体的PreMessageHandler函数,然后通过PreMessageHandler的代码可以知道,主窗体设置了自己的Tab焦点后,执行了return true。而PreMessageHandler返回true,在这个TranslateMessage里面也就返回了true,这时TranslateMessage就结束了。明显看到,这种情况下,弹出窗体的CPaintManager根本没法执行PreMessageHandler函数,这就解析了为什么子窗体的CEditUI和CWebBrowserUI无法切换焦点而主窗体可以。

          这下子找到了根源,分析过程一的修复代码就是没必要的,这里这样修改代码后,bug就修复了。(注意,最终的bug修复代码只需要修改这一个函数就行了,之前分析过程一的不需要修改了!)

bool CPaintManagerUI::TranslateMessage(const LPMSG pMsg)
{
	// Pretranslate Message takes care of system-wide messages, such as
	// tabbing and shortcut key-combos. We'll look for all messages for
	// each window and any child control attached.
	UINT uStyle = GetWindowStyle(pMsg->hwnd);
	UINT uChildRes = uStyle & WS_CHILD;
	LRESULT lRes = 0;
	if (uChildRes != 0)
	{
		HWND hWndParent = ::GetParent(pMsg->hwnd);
//code by redrain 2014.12.3,解决edit和webbrowser按tab无法切换焦点的bug
//		for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
		for( int i = m_aPreMessages.GetSize() - 1; i >= 0 ; --i )
		{
			CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
			HWND hTempParent = hWndParent;
			while(hTempParent)
			{

				if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
				{
					if (pT->TranslateAccelerator(pMsg))
						return true;

					pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes);
// 					if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
// 						return true;
//
// 					return false;
				}
				hTempParent = GetParent(hTempParent);
			}

		}
	}
	else
	{
		for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
		{
			int size = m_aPreMessages.GetSize();
			CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
			if(pMsg->hwnd == pT->GetPaintWindow())
			{
				if (pT->TranslateAccelerator(pMsg))
					return true;

				if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
					return true;

				return false;
			}
		}
	}
    return false;
}

        修复代码很简单,不让他return,而是继续把消息传递下去。附效果图:

        几经波折,前后我分析和调试了4个多小时duilib,最终只要修改三行代码,bug就修复了。

总结:

       实际的修复过程并不是文章描述的这么顺利,期间修改过多次CEditUI的控件代码也实现了焦点切换,还该多其他地方的很多代码,我就不在文章中描述了。而在后续的调试过程中才发现了原来问题的根本在于CPaintManager中的TranslateMessage消息处理。几次周转总算修复了bug。但是我还没有对这个修复的代码进行完整的测试,不知道他会不会引起什么新的问题。所以如果有打算修复这个bug的朋友建议你多做一些测试,如果发现有什么问题,请在博客中留言或者QQ上告诉我一下,谢谢~~

Redrain   2014.11.28


QQ:491646717

      

时间: 2025-01-01 11:25:21

修复duilib CEditUI控件和CWebBrowserUI控件中按Tab键无法切换焦点的bug的相关文章

Silverlight:ScorllViewer随Tab键自动跟随子控件的Focus滚动

当ScrollViewer里包含很多子控件时,默认情况下只能用鼠标手动拖动(或滚轮)滚动条以实现内容的滚动,假如用户是键盘高手,习惯于用Tab键来切换子控件焦点时,即使当前获得焦点的控件在不可见区域,滚动条也不会自动跟随着滚动到相应位置,这个非常不方便,今天在网上看到一个老外的解决办法,代码转贴于此: private void _ScrollViewer_GotFocus(object sender, RoutedEventArgs e) { FrameworkElement element =

duilib List控件,横向滚动时列表项不移动或者移动错位的bug的修复

转载请说明出处,谢谢~~       这篇博客已经作废,只是留作记录,新的bug修复博客地址:http://blog.csdn.net/zhuhongshu/article/details/42264673       之前就在群里挺群友朋友说道,使用List控件,里面加入ListContainElementUI元素,当List出现横向滚动条时,滚动条滑动后元素不跟着滑动或者滑动后位置不正确.       关于List控件的扩展,很早就有人做过了: http://blog.csdn.net/xd

同时添加DataGridView控件与定时器控件后,程序尚未运行时,定时器控件出现红叉

问题描述 别的控件与定时器控件都没问题,只要一添加DataGridView控件,定时器控件的窗口就会出现这样的红叉.VS已经修复过了. 解决方案 解决方案二:而下载的一份代码中,就没有这样的问题,所以,这应该不是VS的问题吧?也按照示例代码中的DataGridView的属性设置过,但是还会有红叉.解决方案三:那就不用datagridview,用listview呗.添加datagridview后再添加timer试试.记住重新创建一个新的项目.解决方案四:将你的timer删了再重新添加试试呢,然后d

[发布] 多选控件和时钟控件

关键字:自定义控件(Custom Control),C++,WIN32 SDK 本文发布的是我在工作中开发的自定义控件.第一个是多选控件,该控件主要启发来自于 ExplorerBar,即资源管理器左侧的 DirectDraw 部分,例如打开文件夹,位于左侧的那个可以扩展收缩的多面板组成的"文件夹任务"等.本控件的开发需求主要是用于在很多个Items中进行快速方便的选择和定位,因此我称它为多选控件.从外观上来看,它是由一些列面板从上到下的方式排列而成(我们把一个Group称为控件的一个C

求方法:web网页开发,想把dropdownlist控件和gridview控件的某一列绑定

问题描述 求方法:web网页开发,想把dropdownlist控件和gridview控件的某一列绑定 先上图: 实现老师查询选择这门课的学生功能: 首先老师可能会教很多课,所以需要选择所教授的课程,比如c语言,然后gridview就自动把选择c语言的学生显示出来. 我不知道是不是用dropdownlist控件,如果不是,烦请大神告诉我设计思路,感激不尽. 解决方案 dropdownlist下拉触发回发,在SelectedIndexChanged中重新根据条件查询绑定gridviewhttp://

file控件和image控件实现图片预览

两种方式:1.用js实现           2.直接在控件的事件处理函数中添加语句(相当于将js的函数代码直接写在此处,如注释处所写)           注意:此处可以用<input type='file'>也可以用<asp:FileUpload>实现,虽然后者没有onchange事件,但是强行使用的时候,虽然提示[validation (asp.net):attribute 'onchange' is not a valid attribute of element 'Fil

ASP.NET 2.0高级控件之FileUpload控件

asp.net|高级|控件 应用程序中经常需要允许用户把文件上传到web服务器.尽管在ASP.NET 1.X也可以完成该功能,但在ASP.NET 2.0中使用FileUpload控件会更简单. 该控件让用户更容易地浏览和选择用于上传的文件,它包含一个浏览按钮和用于输入文件名的文本框.只要用户在文本框中输入了完全限定的文件名,无论是直接输入或通过浏览按钮选择,都可以调用FileUpload的SaveAs方法保存到磁盘上. 除了从WebControl类继承的标准成员,FileUpload控件还公开了

全编辑WebGrid控件LrcGrid(6)——控件呈现

web|控件 全编辑WebGrid控件LrcGrid(6)--控件呈现 创建子控件 重写CreateChildControls()过程,调用创建子控件的方法ReBuild()每当 ASP.NET 页框架需要创建控件树时就会调用CreateChildControls()方法,且该方法调用并不限于控件生命周期的特定阶段.例如,可以在加载页时.在绑定数据过程中或者在呈现过程中调用CreateChildControls protected override void CreateChildControl

Visual Basic 6.0 控件和 .NET 控件的区别

visual|控件|区别 Visual Basic 6.0 控件和 .NET 控件的区别 摘要:本文简单介绍了 Microsoft Visual Basic 6.0 中的标准控件和等效的 Microsoft .NET 控件. 目标 了解哪些 Visual Basic 6.0 ActiveX 控件在 .NET 中具有等效控件. 了解 .NET 控件中哪些属性发生了变化. 了解 .NET 中有哪些新控件. 前提条件 要彻底理解本文内容,需要满足以下条件: 了解什么是 ActiveX 控件. 使用 V