原文出处:MSDN
Magazine September 2004(C++ Q&A)
原代码下载:CQA0409.exe
(248KB)
- 性能监视
- 托管扩展
- 锁定工具栏
在
MSDN® Magazine 2004 六月
的专栏中,我描述了一个称为
ShowTime 的类,你可以用它来为你的应用程序执行一些简单的性能监视。ShowTime 用它的构造函数/析构函数来记录它存在的开始/结束时间,因此你可以在代码块中这样来实例化:
{
ShowTime st(_T("Total time is:"));
// some lengthy operation
} 你便会获得 TRACE
消息或日志文件,它会记录这样一条日志:“共耗时是:nnn 毫秒”,这个nnn 是从实例在栈上创建开始之时到析构函数调用之时的毫秒数。
ShowTime 使用了 CRT::clock 函数以获得自从进程启动后的毫秒数。正如我指出的,对于精度计时来说,::clock
并不是最精确 的函数。我从没有建议过其它可使用的计时函数,因为当时::clock足以充分说明我所回答的问题。但是读者
Robert Bocquier 指出这里有一个更好的函数: timeGetTime。
Figure 1 时间输出
Robert 说这是他编写音乐应用程序的经验:根据你运行的 Windows® 平台/版本,::clock
的粒度可能会高达 10 毫秒 ;而 timeGetTime 更精确。PerfTest.cpp 是一个简单的程序,它可以从
MSDN杂志代码中下载获得,它举例说明了这个问题(感谢Robert提供了 此测试思路)。PerfTest
展示只要时间一改变,它便会显示一条消息,先用::clock,然后用 timeGetTime。正如你从 Figure
1 中 所看到的范例输出那样,在我运行 Windows XP 的机器上,::clock 的粒度为10毫秒;而
timeGetTime 的粒度为 1 毫秒。唯一的差别是你必须首先调用 timeBeginPeriod
来设置你所想要的粒度,并在结束时调用 timeEndPeriod。
timeBeginPeriod(1);
// ... run performance testtimeEndPeriod(1);
每次调用 timeBeginPeriod 后都必须以相同的粒度值配对调用 timeEndPeriod。在我的机器上,粒度的默认值为10毫秒(和
::clock 一样),所以如果 你不想出什么差错,最好不要忘记调用timeBeginPeriod。timeXxx
函数是 Windows 多媒体支持的一部分,因此你必须 #include mmsystem.h 并链接
winmm.lib 库。这里有一个技巧,你可以用它来告诉连接器从源代码模块中包含某个特定的库——而不是向工程设置中添加库。它就是
使用 #pragma 声明,像下面这样:
// tell linker to use winmm.lib
#pragma comment( lib, "winmm" )
#pragma 声明有多种其有用的选项,你应该根据自己的情况选择使用;lib 选项填入一个库搜索记录到你的模块对象文件中。我写了一个新版本的
ShowTime,它使用 timeGetTime 和 pragma 来连接 winmm.lib。ShowTime
构造函数现在 有一个粒度参数,ShowTime 将该参数传递给 timeBeginPeriod (默认值=1)。(PerfTest
用 ShowTime 来测算分配 大批量字符串的时间。)除了这些简单的修订以外,ShowTime 与 2004
年六月刊中的一样棒,在此我就不赘述了。具体细节自己下载源代码看吧。
我有一个用 C++ 写的库并且我 正在尝试用托管扩展将我的某些类暴露给 Microsoft .NET 框架。这时编译器报错
C3828:“placement arguments not allowed while creating
instances of managed classes”。我必须要在我的库中插入下面的代码 才能解决这个问题:
#pragma push_macro("new")
#undef new
// managed stuff here
#pragma pop_macro("new")
但我始终不明白为什么要这样做,因为我的其它模块不需要它们。此外,敲入 pragmas 相当不方便。有没有什么更好的方法使我不必总是要敲入
push_macro 和 pop_macro?
Jordie Marslan
这个问题与 C++ 无关,它与 MFC
有关。C++ 的一个 比较晦涩难懂的特点是你可以重载 new 操作符,并且你甚至可以给它附加参数。通常,操作符
new 只接受拟分配对象的大小:
void* operator new(size_t nAlloc)
{
return malloc(nAlloc);
}
但你也可以随心所欲附加参数来重载 new 操作符,只要在调用 new 时候提供这些参数即可。在各种应用程序向导(App
Wizards)中,这 是 MFC 所做的事情。一个典型的 MFC 程序(.cpp)文件顶部都有下面这样的代码行,通常都由应用程序向导生成:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
MFC 将 new 重定义为 DEBUG_NEW。但 DEBUG_NEW 是什么? afx.h 道出了原委:
// (simplified)
#ifdef _DEBUG
# define DEBUG_NEW new(THIS_FILE, __LINE__)
#else
# define DEBUG_NEW new
#endif
在 debug 生成模式中,MFC 重载了操作符 new 以获取两个额外的参数,比如:
void* operator new(size_t nSize,
LPCSTR lpszFileName, int nLine);
重载的版本与普通的 new 同样都有表示对象大小的 size 参数,但还增加了两个参数:源文件名称和行数。因此,无论何时,只要你写:
pfoo = new CFoo(..);
预处理程序便会将它转变为:
pfoo = new (sizeof(CFoo), THIS_FILE, __LINE__) CFoo(...);
__FILE__(用来初始化 THIS_FILE)和 __LINE__ 是专用的预处理符号,它保存当前被编译的模块文件名称和行数。
其主要用途是当你的应用程序泄漏时,MFC 能显示一个消息。如:
Shame on you! You didn''t free the CFoo object in foo.cpp, line 127!
这对于调试来说,是个巨大的福音,但是当你使用托管C++时,它会导致混乱,因为公共语言运行时(CLR)用于托管对象的
new 操作符并不能理解 这些额外的参数(placement arguments)。但是 MFC 用 #define
重定义了 new,这导致预处理程序做了一个直接的词汇替换。当托管扩展看到了 额外的参数,它们便会歇斯底里。这就是为什么你必须用
#pragmas push_macro/pop_macro 来临时反定义 MFC 已经定义的东西, 然后再次重定义它以恢复到
MFC 中。
对于这个问题,我能想到的唯一的解决办法就是删除 MFC 对 new 的重定义,同时用某个称为mfcnew
的东西来替代它,如 Figure 2 所示。 其缺点是当分配常规 C++ 对象时,你必须记住使用 mfcnew
类型,而分配托管对象时使用常规的 new 声明,如下所示:
pfoo = mfcnew CFoo(...);
CManagedClass *pmc = new CManagedClass();
如果你忘记使用 mfcnew,你便无法获得自动的内存泄漏报告。如果你认为这样是相当麻烦的(我就是这么觉得),那么你会很高兴知道这些问题将在
Visual C++® 2005中得到修正。这正相反:它有一个 gcnew 操作符,你必须用它来分配托管对象。这样就不可能再与普通操作符
new 相冲突了。 以我之见,这是一个更好的方案,因为强制程序员了解何时分配与本地堆对象相对的托管对象是有好处的。此外,它也使得将来在托管堆上分配本地对象成为可能,反之亦然,通过
自动创建相应的使用 GCHandle 或其它必需的代理类。
你可能想知道为什么这个额外的参数被称为“placement arguments”,这是因为最初的目的是允许你在内存的特定位置分配对象。例如,你可能创建一个操作符
new(size_t size, void* p),它只返回被传递的指针
void* operator new(size_t size, void* p)
{
return p;
}
这时你可以调用
new(p) CFoo ;
如果你想要在 p 位置创建 CFoo 对象,而不是让 malloc 为你分配内存,就可以使用一个指针
p。
无论如何,你是对的,使用 push/pop_macro 是个痛苦,而且它还使你的代码看起来丑陋。眼下并没有太多的解决办法——尽管另外一个更加简单的方案
是将所有的托管代码和本地代码隔离开来,放到单独的源文件中去并从托管模块中删除 DEBUG_NEW 部分。如果你的代码已经紧密混合时——换句话说,如果你从相同的函数或代码
块创建托管和非托管对象时,这个方法将不再可行。
我发现不少有关如何停靠工具栏,并持续化其状态之类的文章,并且我的应用程序可以完全做到。现在我正 在尝试实现与
Microsoft Internet Explorer 以及许多其它应用软件中相同的“锁定工具栏”特性。但现在似乎一调用
CToolBar 的 EnableDocking 成员和 CMainFrame 的 EnableDocking成员就没有办法回头去除工具栏的“可停靠性(dockability)”。
我如何才能锁定我的 MFC 工具栏,请给我一些建议?
Sandro Franchi
你是正确的,MFC 中没有任何可以让你锁定/解锁工具栏的代码,而且也没有
函数使你能在启用以后再禁用停靠功能。不过不要害怕,Windows 中总会有办法的!我写了一个小类,CLockBar,它可以让你锁定你的工具栏,还有一个测试程序,LBTest,
示范了这个类的使用。
CLockBar 的使用方法很容易。你只要在主框架类中实例化一个 CLockBar,一个 CLockBar
对应一个你想要锁定的工具栏,如下代码所示:
class CMainFrame : public CFrameWnd
{
…
protected:
CToolBar m_wndToolBar;
CLockBar m_lockToolbar; // toolbar lock
};
接着,你必须安装 CLockBar。最好是在主框架的 OnCreate 函数中:
int CMainFrame::OnCreate(...)
{
// create toolbar as normal
m_lockToolbar.Install(&m_wndToolBar);
return 0;
}
安装完这个锁之后,你便可以在任何时间用 TRUE 或 FALSE 调用 CLockBar::SetLocked
来锁定/解锁工具栏。LBTest有一个 View | Lock 工具栏命令,它转换锁定状态。Figure
3 展示了 LBTest 中主框架类的源文件,它是 LBTest 使用 CLockBar 的地方。
我已经向你展示了如何使用 CLockBar,但是它是如何工作的呢?当我刚开始实现 CLockBar
时,我在 MFC 中做了各种努力 来试图禁用停靠功能——即,撤销 EnableDocking 所做的事情。事实证明这相当困难。停靠工具栏的
MFC 代码高深莫测,即使看源代码也很难理解。我可以肯定从此方法入手最后一定会成功——但是 如果有更简单的方法,那为什么要自讨苦吃呢?
为了阻止用户拖拽工具栏,你唯一要做的就是阻塞(block)任何可能触发拖拽的鼠标消息。这是 CLockBar
类所做的事:如果工具栏被锁定,它就阻塞 将要拖拽工具栏的鼠标消息。如果用户按下其中一个按钮时,你不想阻塞这些鼠标消息,只是当用户试图通过工具栏夹子(gripper)或非客户区拖拽工具栏时才
去阻塞鼠标消息。
查一查文档,我们知道 MFC 已经有了一个函数来确定何时鼠标是处于工具栏的“可拖动”区域:OnToolHitTest。如果你深入到
MFC 的 CControlBar::OnLButtonDown(为 CToolBar 基类和所有控制栏)的源代码,你将会看到
MFC 在什么地方初始化拖 拽操作:
void CControlBar::OnLButtonDown(...)
{
// only start dragging if clicked in "void" space
if (m_pDockBar != NULL && OnToolHitTest(pt, NULL) == -1) {
// start the drag
} else {
// ignore—pass to base class CWnd
CWnd::OnLButtonDown(...);
}
}
OnToolHitTest 是 MFC 用来确定是否显示以及什么时候显示工具提示的一个函数。对于工具栏来说,当鼠标在按钮上时,正好做正常处理。
如果鼠标不在某个按钮上,它便在用户可以拖拽工具栏的“死亡”地带,如工具栏的夹子上或边缘区域。因此你只要吃掉
OnToolHitTest 返回 -1 时的鼠标消息便可以防止拖拽。这正好是 CLockBar 所做的事情,如下面代码所示:
// in CLockBar::WindowProc
if ((msg==WM_LBUTTONDOWN ||
msg==WM_LBUTTONDBLCLK) && m_bLocked) {
CPoint pt(lp);
if (m_pBar->OnToolHitTest(pt, NULL) == -1)
return 0; // eat it
}
CLockBar 派生于 CSubclassWnd,这是一个我在专栏里经常用来截获发送到其它窗口的窗口消息的类。当你安装这个锁,CLockBar
便会以老式 Windows 感应其自身窗口过程安装的方法那样子类化工具栏。然后 CLockBar 首先破解所有发送到工具栏的消息。CSubclassWnd
管理 着这个子类化机制, 并且 CLockBar::WindowProc 是 CSubclassWnd
改写过的,它处理指定的消息——在本例中就是 WM_LBUTTONDOWN 和 WM_LBUTTONDBLCLK。(
你是否知道 MFC 浮动工具栏使用的两个消息吗?双击工具栏可以在停靠/非停靠状态之间来回转换?)结果是当设置锁定时,CLockBar
便防止工具栏看到 导致 MFC 进入拖拽模式的鼠标消息。Figure 4 展示了所有 CLockBar
的代码,你应该注意到只有在工具栏已不再停靠状态时,SetLock(TRUE) 才调用 DockControlBar
来 停靠工具栏。 |