Codejock的界面库Xtreme Toolkit Pro(XTP)是当前MFC开发中发展得比较成熟,应用也颇为广泛的几个界面库之一,其强大的界面美化功能以及简便的使用步骤深受不少MFC程序员的青睐。应用XTP进行MFC程序的开发能够在极大地减少开发周期的前提下,编写出专业化的windows程序界面。

笔者在实际使用XTP的过程中,发现了一个隐藏得比较深的、一般的应用不会遇到的bug。概括地说,这个bug是与视图的静态拆分 (CXTSplitterWnd)以及标签式视图(CXTTabView)相关的,当混合使用这两个特性时,就有可能遇到这个潜在的bug。

首先让我来描述一下触发这个bug的过程:在一个基于MFC的单文档程序中使用CXTSplitterWnd将主视图拆分为左右两个视图,再在右边的视图(直接从CXTTabView继承)中创建Tab View,如果创建了多个Tab View,当激活第2个(从0开始)以上的Tab View后,此时若调用CXTSplitterWnd的HideColumn将左边的视图(非Tab View所在的主视图)隐藏,就会发生断言失败的错误。断言失败对话框会显示在MFC的源代码winsplit.cpp中的第361行出错,即在这个函数中:

BOOL CSplitterWnd::IsChildPane(CWnd* pWnd, int* pRow, int* pCol)
{
ASSERT_VALID(this);
ASSERT_VALID(pWnd);
UINT nID = _AfxGetDlgCtrlID(pWnd->m_hWnd);
if (IsChild(pWnd) && nID >= AFX_IDW_PANE_FIRST && nID <= AFX_IDW_PANE_LAST)
{
if (pRow != NULL)
*pRow = (nID - AFX_IDW_PANE_FIRST) / 16;
if (pCol != NULL)
*pCol = (nID - AFX_IDW_PANE_FIRST) % 16;
ASSERT(pRow == NULL || *pRow < m_nRows);
ASSERT(pCol == NULL || *pCol < m_nCols);   // 在此行断言失败,行C
return TRUE;
}
else
{
if (pRow != NULL)
*pRow = -1;
if (pCol != NULL)
*pCol = -1;
return FALSE;
}
}

当出现断言失败时,我们通常的调试方法就是看调用堆栈,看到底是哪行代码出的问题。通过查看调用堆栈,我们可以清楚的看到在原因出在CXTSplitterWnd的HideColumn中的GetActivePane那一行。

void CXTSplitterWnd::HideColumn(int nColHide)
{
ASSERT_VALID(this);
if (m_nHiddenCol != -1)
{
// return if the requested one is hidden
if (m_nHiddenCol == nColHide)
{
return;
}
ShowColumn();
}
ASSERT(m_nCols > 1);
ASSERT(nColHide < m_nCols);
ASSERT(m_nHiddenCol == -1);
m_nHiddenCol = nColHide;
// if the column has an active window -- change it
int nActiveRow, nActiveCol;
if (GetActivePane(&nActiveRow, &nActiveCol) != NULL)    // 行A
{
if (nActiveCol == nColHide)
{
if (++nActiveCol >= m_nCols)
nActiveCol = 0;
SetActivePane(nActiveRow, nActiveCol);
}
}
// hide all column panes
/// ...
}

跟踪GetActivePane的调用,看到如下代码:

CWnd* CSplitterWnd::GetActivePane(int* pRow, int* pCol)
// return active view, NULL when no active view
{
ASSERT_VALID(this);
// attempt to use active view of frame window
CWnd* pView = NULL;
CFrameWnd* pFrameWnd = EnsureParentFrame();
pView = pFrameWnd->GetActiveView();
// failing that, use the current focus
if (pView == NULL)
pView = GetFocus();
// make sure the pane is a child pane of the splitter
if (pView != NULL && !IsChildPane(pView, pRow, pCol))    // 行B
pView = NULL;
return pView;
}

从上面的堆栈跟踪可以很清楚地看到程序的执行路径和逻辑:调用HideColumn后,程序会检查当前将要隐藏的pane中是否存在当前的活动视图,通过调用GetActivePane(行A)来获得当前活动视图所在的行号和列号。在函数GetActivePane中会获得pFrameWnd的当前活动视图并调用IsChildPane来检查当前活动视图是否属于拆分器中的子窗格。而就在行B所示的IsChildPane这个函数中失败了,通过在调试状态下的观察,可以看到是因为行C中*pCol < m_nCols的值为false,所以导致的断言失败。

那么*pCol和m_nCols到底代表着什么含义呢,*pCol是由当前窗格的ID与AFX_IDW_PANE_FIRST宏的值计算得出的,那么为什么*pCol < m_nCols的值为false?要想搞清楚这个问题,就必须知道AFX_IDW_PANE_FIRST的含义。

查看MFC的源代码,找到AFX_IDW_PANE_FIRST宏的定义:

#define AFX_IDW_PANE_FIRST              0xE900  // first pane (256 max)

以及与之对应的另一个宏:

#define AFX_IDW_PANE_LAST               0xE9ff

这两个宏定义了静态拆分窗口ID的区间,即从AFX_IDW_PANE_FIRST(59648)到AFX_IDW_PANE_LAST(59903)的这样一个左闭右开的区间。所有的静态拆分窗格pane的ID均在这两个宏定义的数字之间,一共256个可用的ID号,亦即一共能够拆分为256个pane。同时我们也能够了解到由于窗口拆分的规则性,所以静态拆分器能够拆分的窗格是一个16×16的矩阵。

明白了上述原理后,回头再看看*pCol和m_nCols以及行C附近的代码,不难明白*pCol指的是当前列的列号,由当前的ID除以16后的余数得到(即求“模”),而m_nCols则表达了当前的列的个数,可以从MFC源码中的CreateStatic、SplitColumn、DeleteColumn等函数中被赋值的过程推断得出。正常情况下*pCol的值肯定是要小于m_nCols的(由于列号从0开始),当出现*pCol的值大于或者等于m_nCols时,就会导致断言失败。

上述的分析似乎很显而易见,逻辑上也很明了,但大家不禁会问:“为什么当前列号会大于等于列数呢?”这得看CXTTabView中的tab在创建的时候到底做了什么,来看源码,见XTP源码中的XTTabBase.cpp文件:

CWnd* CXTTabExBase::CreateTabView(CRuntimeClass* pViewClass,
CDocument* pDocument, CCreateContext* pContext)
{
/// ...
int nTab = (int)m_tcbItems.GetCount();
// Create with the right size (wrong position)
CRect rect(0, 0, 0, 0);
if (!pWnd->Create(NULL, NULL, dwStyle,
rect, m_pTabCtrl, (AFX_IDW_PANE_FIRST + nTab), pContext))
{
TRACE0("Warning: couldn't create client tab for view./n");
// pWnd will be cleaned up by PostNcDestroy
return NULL;
}
if (pWnd->m_hWnd == NULL)
return NULL;
ASSERT((int)_AfxGetDlgCtrlID(pWnd->m_hWnd)
== (AFX_IDW_PANE_FIRST + nTab));
/// ...
}

可以看出,XTP的源码实现中竟然将在TabView中嵌入的子窗口视图的ID号从AFX_IDW_PANE_FIRST开始编号?!这样的实现在TabView未作为SplitterWnd的pane时是不会出问题的,但一旦在诸如文章一开始提到的类似情境时,便会出现问题。那么,问题是怎么发生的呢?让我们来剖析剖析。

当在Mainfrm中用CreateStatic将主视图被拆分为左右两个视图时,左边的视图其ID被赋值为AFX_IDW_PANE_FIRST即59648,右边的则为59649(当非嵌套的splitterwnd时,pane的ID一般都是从AFX_IDW_PANE_FIRST开始的)。接下来在右边的pane中创建了5个tab view,分别嵌入了5个tabctrl。从上面的源码可以看出,这5个tab view的ID从59648到59652,此时,当激活第二个tabctrl(从0开始)后,此时pFrameWnd的当前活动视图即为第二个 tabctrl内嵌的视图,即59650号窗口。这样当执行前面列出的IsChildPane函数时得到的列号*pCol即为(59650-59648)%16=2,而m_nCols为2,所以此时*pCol < m_nCols不成立!!从而导致断言失败。这就是问题发生的整个过程和缘由。

从上面的分析归纳出来一句话,这是一个由于误用AFX_IDW_PANE_FIRST宏而导致的在特定情境下才会发生的潜在的bug,其根本原因是创建tabview的时候用到的ID段号占用了静态拆分器预留的ID段号,要纠正这个bug,最彻底的做法是将创建tabview时用到的ID段号与静态拆分器预留的ID段号彻底分开。幸好XTP的源码是可以由我们自己修改的,可以通过如下方法解决此bug。

重新定义一个宏XTP_IDW_TAB_FIRST,表明tabview窗格的ID起始号,并且其数值区间的选取要避开常用的一些预留ID号的区间(这些预留段号可以参见afxres.h头文件)。然后修改XTTabBase.cpp文件中的CreateTabView函数,如下:

CWnd* CXTTabExBase::CreateTabView(CRuntimeClass* pViewClass,
CDocument* pDocument, CCreateContext* pContext)
{
/// ...
int nTab = (int)m_tcbItems.GetCount();
// Create with the right size (wrong position)
CRect rect(0, 0, 0, 0);
if (!pWnd->Create(NULL, NULL, dwStyle,
rect, m_pTabCtrl,
(XTP_IDW_TAB_FIRST + nTab), pContext))
// 修改AFX_IDW_PANE_FIRST为XTP_IDW_TAB_FIRST,D行
{
TRACE0("Warning: couldn't create client tab for view./n");
// pWnd will be cleaned up by PostNcDestroy
return NULL;
}
if (pWnd->m_hWnd == NULL)
return NULL;
ASSERT((int)_AfxGetDlgCtrlID(pWnd->m_hWnd)
== (XTP_IDW_TAB_FIRST + nTab));  // 同D行
/// ...
}

重新编译XTP的DLL文件,再次在文章开始的情景中使用HideColumn时,一切正常。

由AFX_IDW_PANE_FIRST宏的含义分析界面库XTP的一个bug相关推荐

  1. 以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)

    三年前,准备将金山界面库做一个全面的剖析.后来由于种种原因,这个系列被中断而一直没有更新.时过境迁,现在在windows上从事开发的人员越来越少,关注这块的技术的朋友也很少了.本以为这系列也随着技术的 ...

  2. 以金山界面库(openkui)为例思考和分析界面库的设计和实现——资源读取模块分析

    按照软件的执行流程,我们首先遇到<以金山界面库(openkui)为例思考和分析界面库的设计和实现--问题>中提出的最后一个问题:界面描述文件的放置位置.我们曾提出一种方案:将界面描述文件打 ...

  3. 以金山界面库(openkui)为例思考和分析界面库的设计和实现——问题

    随着物质生活的丰富,人们的精神生活也越来越丰富.人们闲暇的时间也相对变多,于是很多人就开始寻找打发时间的方法.其中电视便是其中一种非常重要的消遣方式.假如我们打开电视机,看到了一个电视台正在播一部我们 ...

  4. 记numpy高速封装库bottleneck的一个bug

    博主在使用bottleneck的移动平均函数时,对0的平均得到了很接近0的非零值,这是一个官方已知的bug,但是还未修复. 这些值的产生可能是因为bottleneck在底层用C编写函数的时候,数据类型 ...

  5. ST USB Host库USBH_HandleEnum()的一个bug

    USBH_HandleEnum()里定义了一个64字节的局部数组Local_Buffer[ ],并在获取厂商描述符.产品描述符和序列号的时候使用.但是使用的时候却认为最大可以用到0xff个字节.如果U ...

  6. C++ 100款开源界面库——内容细节(现在有变动)不必深究,普及就好

    C++ 100款开源界面库 (10) from:http://www.cnblogs.com/Alberl/p/3375162.html (声明:Alberl以后说到开源库,一般都是指著名的.或者不著 ...

  7. 同花顺python_专题研究|量化交易怎么少得了GUI!手把手教你用 Python 打造股票行情分析界面...

    开场 Python 的出现可以帮助我们快速解决实际的问题,提高工作效率. 如果给Python 脚本加上一个GUI 的话,不仅可以进一步提升使用效率(不用每次停止运行去修改参数),而且还能把自己程序分享 ...

  8. 仿迅雷播放器教程 -- C++ 100款开源界面库 (10)

    (声明:Alberl以后说到开源库,一般都是指著名的.或者不著名但维护至少3年以上的.那些把代码一扔就没下文的,Alberl不称之为开源库,只称为开源代码.这里并不是贬低,像Alberl前面那个系列的 ...

  9. [解决方案]excel2010分析工具库无法运行“ FUNCRES.NLAM! Showatpdialog”宏

    问题描述: 2010版excel32位,加载过分析工具库后,运行出现如下异常 无法运行" FUNCRES.NLAM! Showatpdialog""宏.可能是因为该宏在此 ...

最新文章

  1. H5跟ios、android交互跟数据对接
  2. Redis分布式锁的正确实现方式(Java版)
  3. 面试让HR都能听懂的MySQL锁机制,欢声笑语中搞懂MySQL锁
  4. 在windows系统上word转pdf
  5. Ubuntu 8.04 Hardy LTS 软件源设置
  6. 使用Win2D在UWP程序中2D绘图(二)
  7. 北京54坐标转WGS84坐标
  8. 零基础学python电子书-《零基础入门学习Python》电子书PDF+笔记+课后题及答案免费下载...
  9. GD32F450替换STM32F429
  10. 小程序分享至群群消息小结(包括分享到App)
  11. LabWindows/CVI与Matlab混合编程的一种实现方法
  12. 自走棋突然显示服务器无法定位,刀塔自走棋服务器无法定位游戏会话_刀塔自走棋服务器无法定位游戏会话怎么回事_玩游戏网...
  13. 魔兽世界怀旧服显示没有可用服务器,魔兽世界怀旧服选哪个服务器
  14. DSP28335入门教程:ADC的使用
  15. MS8413光纤同轴解码芯片
  16. 企业微信手机端可以退出吗?会影响电脑端企业微信吗?
  17. 国密SSL证书保障网站安全
  18. 使用fiddler获取ios手机接口
  19. 湿度传感器 DHT11
  20. 推荐 5 个好玩的 ChatGPT 开源应用

热门文章

  1. 在网页上实现大华视频监控摄像头在线
  2. HTML页面格式化(CSS)
  3. 正在获取服务器信息,正在获取远程列表服务器信息
  4. rm ,rm -rf , rm -f 以及rm 命令的其他参数命令
  5. Python性能分析利器pyinstrument讲解
  6. 工件SSMwar exploded 部署工件时出错。请参阅服务器日志了解详细信息
  7. 基础正则表达式及常用正则表达式
  8. SAP ABAP EXCEL导出多个SHEET页签
  9. Softing参加ASAM中国区域大会暨C-ASAM技术论坛会议
  10. 2022南理工824专考研经验