修复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
修复duilib CEditUI控件和CWebBrowserUI控件中按Tab键无法切换焦点的bug的更多相关文章
- [C#]Winform下回车或Tab键自动切换下一个控件焦点
满足用户体验,在数据录入时,能在输入完一个信息后通过回车或Tab键自动的切换到下一个控件(字段). 在界面控件设计时,默认可以通过设置控件的TabIndex来实现.但在布局调整时或者是对输入的内容有选 ...
- duilib List控件,横向滚动时列表项不移动或者移动错位的bug的修复
转载请说明出处,谢谢~~ 这篇博客已经作废,只是留作记录,新的bug修复博客地址:http://blog.csdn.net/zhuhongshu/article/details/42264673 之前 ...
- 2013 duilib入门简明教程 -- 自绘控件 (15)
在[2013 duilib入门简明教程 -- 复杂控件介绍 (13)]中虽然介绍了界面设计器上的所有控件,但是还有一些控件并没有被放到界面设计器上,还有一些常用控件duilib并没有提供(比如 ...
- Duilib学习笔记《03》— 控件使用
在前面已经对duilib有个一个基本的了解,并且创建了简单的空白窗体.这仅仅只是一个开始,如何去创建一个绚丽多彩的界面呢?这就需要一些控件元素(按钮.文本框.列表框等等)来完善. 一. Duilib控 ...
- duilib教程之duilib入门简明教程13.复杂控件介绍
首先将本节要介绍的控件全部拖到界面上,并调整好位置,如图: 然后将Name属性改成其他名字, 不能是[控件名+UI+数字]这种,因为这是DuiDesigner默认的名字,它不会实际写 ...
- WinForm用户控件、动态创建添加控件、timer控件--2016年12月12日
好文要顶 关注我 收藏该文 徐淳 关注 - 1 粉丝 - 3 0 0 用户控件: 通过布局将多个控件整合为一个控件,根据自己的需要进行修改,可对用户控件内的所有控件及控件属性进行修 ...
- DevExpress控件的GridControl控件小结
DevExpress控件的GridControl控件小结 (由于开始使用DevExpress控件了,所以要点滴的记录一下) 1.DevExpress控件组中的GridControl控件不能使横向滚动条 ...
- 【完全开源】百度地图Web service API C#.NET版,带地图显示控件、导航控件、POI查找控件
目录 概述 功能 如何使用 参考帮助 概述 源代码主要包含三个项目,BMap.NET.BMap.NET.WindowsForm以及BMap.NET.WinformDemo. BMap.NET 对百度地 ...
- 如何使用免费PDF控件从PDF文档中提取文本和图片
如何使用免费PDF控件从PDF文档中提取文本和图片 概要 现在手头的项目有一个需求是从PDF文档中提取文本和图片,我以前也使用过像iTextSharp, PDFBox 这些免费的PD ...
随机推荐
- Java实现Mysql数据导入导出
package com.backup; import java.io.BufferedReader;import java.io.FileInputStream;import java.io.File ...
- iOS:UIAlertController和UIAlertAction的详解
提示框控制器:UIAlertController 提示框按钮:UIAlertAction 功能:用来提示信息,并给出一些可以进行选择的按钮来处理相应的要求. 注意:在Xcode的iOS8 SD ...
- 机器学习 —— 概率图模型(Homework: Structure Learning)
概率图的学习真的要接近尾声了啊,了解的越多越发感受到它的强大.这周的作业本质上是data mining.从数据中学习PGM的结构和参数,完全使用数据驱动 —— No structure, No par ...
- 界面上传文件js包【AjaxUpload.js】
function uploadFile() { new AjaxUpload($("#importFile"), { action: url, type: "POST&q ...
- hadoop中的ssh无密码登录配置
在配置Hadoop集群分布时,要使用SSH免密码登录,假设现在有两台机器hadoop@Master(192.168.1.101),作为Master机,hadoop@Slave(192.168.1.10 ...
- 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)#会报 ...
- org.opencv.android.JavaCameraView 摄像机方向的问题
——> org.opencv.android.JavaCameraView 摄像机方向的问题 ref: http://www.tuicool.com/articles/q6vUvqB 注意:一般 ...
- C# 打印小票 POS
C# 打印小票 POS 最近在写一个餐饮的收银系统,以前从来没有碰过打印机这玩意.感觉有些无从下手,在前面做报表时,总想找第三方的控件来用用,结果始终不行没搞定.没研究透,催得急没办法还是的动手自己写 ...
- Charles是Mac的Fiddler抓包工具
windows下面我们经常使用 Fiddler 抓包工具进行代理等一系列操作.然而,在 Mac 下的 Fiddler 勉强能运行,但是其挫的都不想说它了.今天看到朋友推荐这款 Charles Mac下 ...
- sgu 495. Kids and Prizes (简单概率dp 正推求期望)
题目链接 495. Kids and Prizes Time limit per test: 0.25 second(s)Memory limit: 262144 kilobytes input: s ...