转载请说明原出处,谢谢~~: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

      

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

  1. [C#]Winform下回车或Tab键自动切换下一个控件焦点

    满足用户体验,在数据录入时,能在输入完一个信息后通过回车或Tab键自动的切换到下一个控件(字段). 在界面控件设计时,默认可以通过设置控件的TabIndex来实现.但在布局调整时或者是对输入的内容有选 ...

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

    转载请说明出处,谢谢~~ 这篇博客已经作废,只是留作记录,新的bug修复博客地址:http://blog.csdn.net/zhuhongshu/article/details/42264673 之前 ...

  3. 2013 duilib入门简明教程 -- 自绘控件 (15)

        在[2013 duilib入门简明教程 -- 复杂控件介绍 (13)]中虽然介绍了界面设计器上的所有控件,但是还有一些控件并没有被放到界面设计器上,还有一些常用控件duilib并没有提供(比如 ...

  4. Duilib学习笔记《03》— 控件使用

    在前面已经对duilib有个一个基本的了解,并且创建了简单的空白窗体.这仅仅只是一个开始,如何去创建一个绚丽多彩的界面呢?这就需要一些控件元素(按钮.文本框.列表框等等)来完善. 一. Duilib控 ...

  5. duilib教程之duilib入门简明教程13.复杂控件介绍

    首先将本节要介绍的控件全部拖到界面上,并调整好位置,如图:  然后将Name属性改成其他名字,         不能是[控件名+UI+数字]这种,因为这是DuiDesigner默认的名字,它不会实际写 ...

  6. WinForm用户控件、动态创建添加控件、timer控件--2016年12月12日

    好文要顶 关注我 收藏该文 徐淳 关注 - 1 粉丝 - 3       0 0     用户控件: 通过布局将多个控件整合为一个控件,根据自己的需要进行修改,可对用户控件内的所有控件及控件属性进行修 ...

  7. DevExpress控件的GridControl控件小结

    DevExpress控件的GridControl控件小结 (由于开始使用DevExpress控件了,所以要点滴的记录一下) 1.DevExpress控件组中的GridControl控件不能使横向滚动条 ...

  8. 【完全开源】百度地图Web service API C#.NET版,带地图显示控件、导航控件、POI查找控件

    目录 概述 功能 如何使用 参考帮助 概述 源代码主要包含三个项目,BMap.NET.BMap.NET.WindowsForm以及BMap.NET.WinformDemo. BMap.NET 对百度地 ...

  9. 如何使用免费PDF控件从PDF文档中提取文本和图片

             如何使用免费PDF控件从PDF文档中提取文本和图片 概要 现在手头的项目有一个需求是从PDF文档中提取文本和图片,我以前也使用过像iTextSharp, PDFBox 这些免费的PD ...

随机推荐

  1. Java实现Mysql数据导入导出

    package com.backup; import java.io.BufferedReader;import java.io.FileInputStream;import java.io.File ...

  2. iOS:UIAlertController和UIAlertAction的详解

    提示框控制器:UIAlertController 提示框按钮:UIAlertAction   功能:用来提示信息,并给出一些可以进行选择的按钮来处理相应的要求.   注意:在Xcode的iOS8 SD ...

  3. 机器学习 —— 概率图模型(Homework: Structure Learning)

    概率图的学习真的要接近尾声了啊,了解的越多越发感受到它的强大.这周的作业本质上是data mining.从数据中学习PGM的结构和参数,完全使用数据驱动 —— No structure, No par ...

  4. 界面上传文件js包【AjaxUpload.js】

    function uploadFile() { new AjaxUpload($("#importFile"), { action: url, type: "POST&q ...

  5. hadoop中的ssh无密码登录配置

    在配置Hadoop集群分布时,要使用SSH免密码登录,假设现在有两台机器hadoop@Master(192.168.1.101),作为Master机,hadoop@Slave(192.168.1.10 ...

  6. python list删除元素 del remove

    L=[5,4,3,2,1,'abc'] del 按照index删除比如: del L[i] del L[i:j] remove按照内容删除 L.remove('abc') L.remove(0)#会报 ...

  7. org.opencv.android.JavaCameraView 摄像机方向的问题

    ——> org.opencv.android.JavaCameraView 摄像机方向的问题 ref: http://www.tuicool.com/articles/q6vUvqB 注意:一般 ...

  8. C# 打印小票 POS

    C# 打印小票 POS 最近在写一个餐饮的收银系统,以前从来没有碰过打印机这玩意.感觉有些无从下手,在前面做报表时,总想找第三方的控件来用用,结果始终不行没搞定.没研究透,催得急没办法还是的动手自己写 ...

  9. Charles是Mac的Fiddler抓包工具

    windows下面我们经常使用 Fiddler 抓包工具进行代理等一系列操作.然而,在 Mac 下的 Fiddler 勉强能运行,但是其挫的都不想说它了.今天看到朋友推荐这款 Charles Mac下 ...

  10. sgu 495. Kids and Prizes (简单概率dp 正推求期望)

    题目链接 495. Kids and Prizes Time limit per test: 0.25 second(s)Memory limit: 262144 kilobytes input: s ...