分类: C/C++
2008-03-10 20:02:11
我在 MDI 程序中打算通过 CMainFrame 中的定时器事件来更新所有的子窗口。 视图用于显示许多图表。用如下的代码只能更新当前活动窗口: GetActiveWindow()->GetActiveView()->GetDocument() 是否有其它的方法从 CMDIFrame 类中获得所有的子窗口或者所有的文档? Makarand 你的情况并不罕见。许多采集实时数据的程序需要定时更新屏幕。即使你的程序不是采集实时数据,当用户的操作改变文档时,你一样要更新视图。MFC中doc/view模型(包括所有的object/view模型)的基本思想是数据与显示的分离。用户或者 实际事件改变了底层对象、数据或者文档,就会通过某些视图立即更新事件传递给显示机制。对于同一个文档如果有数个视图,MFC已经有了一种机制一步到位地更新所有视图。这个函数就是CDocument::UpdateAllViews,它对打开文档的每 一个视图调用 CView::OnUpdate。你可以传递应用程序专用的,描述要执行哪一种更新操作的“提示”。例如,如果你知道仅仅是文档的标题改变了,你就可以定义一个枚举值 CHANGED_TITLE,将它作为 提示代码进行传递。如果你的文档包含了图片和文字,你可以定义枚举值 CHANGED_TEXT 和 CHANGED_GRAPHICS。这些 提示代码的目的是提高性能。通过“提示”来告诉视图什么东西改变了,这样就可以更智能地只重绘那些真正需要刷新的屏幕区域,从而避免潜在的耗时的绘制操作或屏幕闪烁。 UpdateAllViews 更新所有与某个文档关联的视图,但是如何更新所有的文档呢?MFC中没有UpdateAllDocuments 这样的函数,因此你需要自己列举所有的文档。这 就要求实现一个对文档模板和相关文档的循环操作,如下所示: for (/* each CDocTemplate in app */) { for (/* each CDocument in CDocTemplate */) { // do something } } 既然列举文档是如此的有用,我写了一个很小的类 CDocEnumerator,隐藏了MFC中所有的模板和位置的机制。 实际上,这个类是我早在1995年9月写的——呵呵,这都几乎是十年前的事了。代码如 Figure 1 所示。 使用 CDocEnumerator 很容易在程序中列举所有打开的文档。 CDocEnumerator it; CDocument* pdoc; while ((pdoc=it.Next())!=NULL) { // do something } 还有什么比这个更容易?为了在实际的例子中示范这个类的用法,我写了一个小程序 UpdView,该程序将模拟实时数据采集程序。UpdView 中每个文档对其打开的秒数进行计数。Figure 2 显示了工作中的 UpdView。如果下载、生成并运行 UpdView,你便能看到每个视图每秒更新显示文档打开的秒数。在 Figure 2 中,名字为 file2.dat 的文档有两个视图,它们都显示同一个底层文档。每个文档维持自己的自打开后的时间数(数据),视图只是进行显示(表现)。在你自己的程序中,UpdView通过主框架的定时器设置工作。这个定时器处理事件使用 CDocEnumerator 告诉每一个文档收集更多的数据,如下面所示: void CMainFrame::OnTimer(UINT_PTR nIDEvent) { CDocEnumerator it; CDocument* pdoc; while ((pdoc=it.Next())!=NULL) { ((CMyDoc*)pdoc)->CollectMoreData(); } } Figure 2 运行中的UpdView CMyDoc::CollectMoreData 增加了一个简单计数器的值。在一个实时程序中,CMyDoc::CollectMoreData 将获得最新的数据,如下载最近的火星图片,或者读取 Bill Gates 浴缸的温度。重要的采集数据后,CMyDoc 通知它的视图自动更新: void CMyDoc::CollectMoreData() { iData++; // time waits for no man... UpdateAllViews(NULL, 0, NULL); }现在,MFC 调用每个视图的 OnUpdate 方法,再调用 Invalidate/UpdateWindow 刷新视图。既然UpdView 是如此的简单,就没有必要提示了。在一个实际的程序中,你 可能需要传递提示信息来帮助视图更有效地重绘它的窗口。 我在 Microsoft® .NET Framework 中用 C# 写了一个程序。我知道事件是如何工作的,但有时很难知道什么时候发生的是哪个事件。当我使用 C++ 编程时,我总是可以运行 Spy++ 来查看窗口消息是何时发送的。即使没有Spy++,我也可以写一个窗口过程将消息显示到 TRACE 输出。例如: LRESULT MyWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { TRACE(_T("Message: %04x, wp=%04x, lp=%04x\n"), msg, wp, lp); ... }我试图象这样来跟踪 C# 中的事件,但是我无法实现,因为没有中枢事件处理例程。是否有其它方法来观察这些事件,或者 有没有在 .NET 中使用的第三方的 Spy 工具? Francine Wiggins 我没有注意到 .NET Framework 中有事件跟踪工具,但是写一些代码来跟踪事件并不难。注意我说的是并不太难,因为它 比处理消息的 WndProc 例子难那么一点点。 为了使大家都能明白,我先强调一下 Windows 消息和 Framework 事件的区别。在 Windows® 中, 当某件事情发生的时候,操作系统与程序的通讯是通过将消息发送到窗口过程(WndProc)来实现 的,因为所有的消息都要通过单一的回调过程,即窗口过程。所以很容易跟踪消息。无论使用 C/C++ 编程还是.NET Framework 都是如此。在.NET中,你可以通过 改写 Control.WndProc 来实现对消息的跟踪,与C的例子一样。但是,虽然窗口消息从概念上将是一种事件(许多程序员都把它们称为事件),并且 虽然许多 Framework 事件都对应于窗口消息(例如,OnClick 和 WM_COMMAND/BN_CLICKED 一样),但 Framework 使用完全不同的机制, 一种基于委托(delegates)机制而不是全局回调过程。然而每个消息通过相同的 WndProc,每个 FrameWork 有其自己的委托。 那么在没有中枢处理器的情况下,如何跟踪事件?甚至是如何知道处理的是哪个事件呢? 找到这些事件相对容易。.NET Framework 一个最强大的特性之一就是反射,这是一种奇特的方式,我们把它叫做.NET的自我意识。例如,为找出 Form 类中的所有的 public 事件,可以这样写: EventInfo[] allEvents = typeof(Form).GetEvents(); 这获得了一个 EventInfo 对象数组,每个元素对应于一个 Form 类中定义的事件。每个 EventInfo 对象包括描述 该事件的信息。EventInfo.Name 是事件的名字(例如,Click 或Closing)。EventInfo.EventHandlerType 是 需要用来处理该事件的处理器的类型(delegate)。你甚至可以调用 EventInfo.AddEventHandler 增加另一个处理器。这会使你猜想一个跟踪事件的方法 是写一个通用的处理器,把它挂到类种你想要跟踪的每一个事件上。这是一种正确的途径,但是细节有点复杂。如何写出这个通用处理器?署名是什么?答案显然应该是: // generic event handler-for any event public void OnAnyEvent(Object sender, EventArgs e); 然而,并不是所有的处理器都使用 EventArgs。例如,Form.Closing 事件使用一个要求CancelEventArgs 的处理 器。 void OnClosing(Object sender, CancelEventArgs e); 当然,CancelEventArgs 从 EventArgs 派生而来,所以将 CancelEventArgs 传递给某个期望 EventArgs 的方法是完全合法的——但是.NET 1.1版本中委托不是这样工作的。当使用 += 或者 EventInfo.AddEventHandler 添加事件处理器时,你必须提供一个委托,其类型必须与该事件处理器的类型完全匹配。这意味着你不能用单一的通用处理器来处理所有事件。每一个事件委托署名至少需要一个处理 器。 // spy on myself spy = new EventSpy("MySpy", this); spy.SpyEvent += new SpyEventHandler(OnEventSpy); 当目标对象(spyee)产生任何一种事件,EventSpy就产生一个SpyEvent事件。此时,SpyEventArgs.EventName 就是事件的名字,SpyEventArgs.EventArgs 包 含原始的事件参数。如何处理这些事件是你自己的事。SpyTest 将该事件报告给诊断流。 // in main form private void OnEventSpy(Object sender, SpyEventArgs e) { Trace.WriteLine(String.Format("{0}: On{1}: {2}", sender.GetType(), e.EventName, e.EventArgs)); } Figure 3 示范了一个典型的运行结果。先去下载代码,自己试着运行一下——它 确实能运行!EventSpy 还有一个 DumpEvents 函数,列出所有你的目标类暴露的事件。 AssemblyName an = new AssemblyName(); an.Name = "EventSpyHelper"; AssemblyBuilder asm = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run); 创建模块更简单。创建这个类、字段和方法很象给每个东西取一个名字和标记那样直截了当。最难的部分是何时生成实际代码——换句话说, 也就是每个事件的容器和事件处理器。关键的类叫ILGenerator。Figure 4 演示 EventSpy 如何编写这个事件处理器, // create a new method OnEventXxx MethodBuilder mthd = helperClass.DefineMethod(...); // generate its code ILGenerator il = mthd.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld,fld); il.Emit(OpCodes.Ldstr,ev.Name); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Callvirt, miReportEvent); il.Emit(OpCodes.Ret); 对 ILGenerator.Emit 的每一个调用产生一个简单的 MSIL 指令。但是我如何知道哪个指令被生成 了呢?你是否真的认为我知道在 MSIL 中如何编程?当然不是。你不必一定要使用 MSIL 来生成代码。你只要编写拟在 C#中使用的代码(或者任何其它面向.NET的语言) 即可,编译它,并用 ILDASM 来检查生成的 MSIL。这就是我要做的;Figure 5 演示了部分 Figure 4 代码 dump 出的 ILDASM。正如你看到的,MSIL和以前的片断显示的一模一样。 有任意问题或建议情发邮件给 Paul,邮箱是 。 |
作者信息 Paul DiLascia 是一个自由撰稿人、顾问和 Web/UI 高级设计师。他是《Windows++: Writing Reusable Windows Code in C++》(Addison-Wesley, 1992)一书的作者。你可以通过与他联系。 |
译者信息 肖进:南京中萃食品有限公司资讯部(210061) |
本文来自,。可以从附近的报亭获得,或者是 。 |