现在的位置: 首页 > 综合 > 正文

[收藏]通过 .NET Compact Framework 优化 Pocket PC 开发

2013年10月11日 ⁄ 综合 ⁄ 共 14232字 ⁄ 字号 评论关闭

通过 .NET Compact Framework 优化 Pocket PC 开发

发布日期: 1/7/2005 | 更新日期: 1/7/2005

Dave EdsonJohn Socha-Leialoha

本文讨论:

能够使在 .NET Compact Framework 环境中编程变得更容易的提示

能够使基于 Pocket PC 的应用程序更快运行的技巧

重要的 .NET Compact Framework 类

.NET Compact Framework 窗口问题

代码下载可从以下位置获得:NETCompactFramework.exe (270KB)

如今,程序员有很多工作都无需去做了。借助于 .NET,最后终于有了一个使应用程序开发真正容易的框架。开发人员几乎不用花费精力来管理内存,以至于我们很容易滥用它。所有优雅的控件、窗体、窗口系统以及 GDI+ 功能都使应用程序开发变得如此容易。是的,很容易 — 直到您必须为 Pocket PC 这个带有慢得令人痛苦的图形(读取:没有加速)、没有硬盘驱动器并且只有 32MB RAM(您实际上只能使用其中的大约 2 或 3 MB)的 200MHz 平台编写程序为止。除了硬件限制以外,当您一头扎入 .NET Compact Framework,却发现自己最喜欢的某些功能并未实现时,还会产生一种沮丧的感觉。突然之间,您所具有的编写巧妙的、令人叹为观止的 C# 应用程序的梦想变成了一个噩梦。

请不要绝望。.NET Compact Framework 确实可以用来编写极好的代码和应用程序。只要您考虑到几个方面并且愿意遵循一两个规则,您就还能够改善性能。我们已经使用用于 Pocket PC 的 C# 编写了很多应用程序,从而得到了既小又快的、完美的、灵巧的应用程序。

本文划分为两个主要部分:技巧和优化。在技巧部分,我们将回顾一些使我们作为 .NET Compact Framework 程序员的工作变得更容易的优雅技巧。在优化部分,我们将讨论能够提高性能、减少加载时间和减小内存足迹的技术。本文将提供示例代码,以帮助您使应用程序变得既小又快。

基本技巧和我们的 Test Bench

当您启动 Visual Studio® .NET 以生成我们的初始应用程序时,您将需要确保在主窗体的构造函数中(恰好在对 InitializeComponent 的调用之后)添加下列代码行:

#if DEBUG

MinimizeBox = false;
#else
MinimizeBox = true;
#endif

这使您可以实际关闭该应用程序,而不是仅仅将其最小化(在调试器外部进行测试时,这可能很有用)。下一个重要任务是发掘您要为其编程的设备的全部能力。

.NET Compact Framework 恰如其名 — 它确实是压缩版。令人欣慰的是,它包含了 Interop 服务,因此您可以调用设备上的任何 Win32®CE API。由于这些 API 中有很多都需要窗口句柄,因此您需要获得一个窗口句柄。在 .NET Compact Framework 的版本 2.0(它重新启用了 Handle 属性)发布之前,请使用下列代码行来获得 hWnd:

control.Capture = true;

IntPtr hwnd = Win32.GetCapture();
control.Capture = false;

有关 Win32 类的信息,请参阅提要栏“Windows CE 命名空间”。

既然 Win32 API 又重新为您所掌握,那么您还可以采用另外一种方式来方便地使用 hWnd。您可以通过用 eMbedded Visual C++® 4.0 编写的 Win32 DLL 来将您自己的窗口子类化,并且使用非常手段。但是,请记住一件事情:Interop 服务不会使 DLL 一直加载在内存中。在 P/Invoke 的内部有一个 LoadLibrary/FreeLibrary 对。如果您希望您的 DLL 子类化您的应用程序(或系统中的任何应用程序),则 DLL 需要一直加载在内存中,直到您准备好让它离开为止。该问题有一个很好的解决方案:只须在开始时调用该 DLL 中的 LoadLibrary,然后在完成工作时调用该 DLL 中的 FreeLibrary。引用计数将突然提高一个等级,并且 P/Invoke 的 LoadLibrary/FreeLibrary 对将变得很温和。

图 1 MSDN Test Bench

本文将讨论很多个不同的主题。我们已经编写了一个 Test Bench 程序(如图 1 所示),它结合使用了本文讨论的很多概念。它合并了提要栏“Windows CE 命名空间”中介绍的 Win32 类,以及本文后面讨论的 BaseControl 类。

笨拙的、令人讨厌的对话框

那些不知道 .NET Compact Framework 中的对话框行为的用户和开发人员在打开模式对话框时,可能会变得不知所措。.NET Compact Framework 中的对话框并不是通常意义上的对话框。相反,它们只是一些出现在现有窗体之上的新窗体,而且在您关闭窗体之前,它们不会将控制返回到调用方。如果您使用如下所示的代码显示对话框

  BadDialog test = new BadDialog();

test.ShowDialog();

则对话框将作为顶级窗体出现。当用户从您的程序中切换出去时,有趣的事情就开始了。最后,他们希望返回到您的应用程序,并且他们转到“System | Settings | Memory | Running Programs”页,在这里他们看到您的主应用程序和对话框都被列出。如果用户单击顶级应用程序而不是对话框,则主应用程序窗口将显示,但是该窗口将是不活动的,因为它正在等待对话框关闭。

要看到这一情况的发生,请单击 Test Bench 应用程序中的“Bad Dialog”按钮。现在,请从“Settings”屏幕转到正在运行的程序的列表,并且激活 Test Bench 应用程序(请参见图 2)。如果您单击任何按钮,则它们只是发出声响,因为该窗口被禁用。您甚至无法关闭它。因此,用户只能采用他们所知道的方式进行响应:一通单击(实际上是在移动设备上一通狂敲)。摆脱这一困境的方式是,返回到正在运行的程序的列表并激活“Bad Dialog”项。更糟糕的是,一旦您关闭该对话框,就可能弹出另外一个应用程序,因为当您在不同的程序之间切换时,窗口的 Z 顺序可能已经更改。

图 2 使人迷惑的信息

为了解决这些问题,以及其他几个与存在多个窗体相关的问题,我们创建了 Dialogs 类 — 该类带有一个名为 ShowDialog 的静态成员。以下是该类完成的工作:

通过将“Running Programs List”中的父窗体的标题设置为空字符串,将该窗体隐藏。

将对话框的标题设置为父窗体所使用的相同名称,从而保留您在“Running Program List”中看到的名称,无论哪个对话框位于最顶层。

捕获新对话框的 Activated 和 Closed 事件,以便可以跟踪哪个窗体是“最顶层”的窗体。

为该对话框调用 ShowDialog。

每当任一“父”窗体被激活时,激活“最顶层”窗体并将其提升至顶层。

这整个过程有时可导致另外一个应用程序在您关闭模式对话框时瞬间可见,但是除了这一点以外,一切都按照用户期望的那样工作。通过单击 Test Bench 应用程序中的“Dialog Fix”按钮,您可以看到 ShowDialog 的这一增强版本是如何工作的。请注意,对话框上的标题与主窗体相同,并且只有一个窗体出现在“Running Programs List”中。

更好地控制上下文菜单

在典型的基于 Pocket PC 的应用程序上单击并保持不动,您将获得那些跳动的点,后面跟着一个上下文菜单。对于用户而言,这非常好。对于程序员而言,这是令人烦恼的,因为没有收到 MouseDown 事件。这意味着没有机会在显示上下文菜单之前执行一些操作,例如,首先禁用该上下文菜单或者更改某个选项。要想绕过这一行为,需要对运行库耍几个花招。通常,只须在用户单击并保持不动时,设置控件的 ContextMenu 属性以便将其与上下文菜单相关联。在我们的解决方案中,我们没有设置控件的 ContextMenu 属性;相反,我们模仿 Control 类中内置的代码来处理 MouseDown 事件并显示菜单。为了使这一点对程序员尽可能地透明,我们开发了一个新的控件基类,并将其聪明地命名为 BaseControl。它封装了我们希望我们的自定义控件具有的很多行为。有关这些功能的简短列表,请参见图 3。消除默认行为并将其替换为我们自己的行为的关键是重写 ContextMenu 属性。这样,Control.ContextMenu 属性仍然为空,而 BaseControl.ContextMenu 则包含我们需要的信息。

图 4 显示了 BaseControl 的完成这一工作的四个方法:OnMouseDown、CheckContext、ShowContext 和 RecognizeGesture。如果您需要更改选项或者执行其他某种操作,则您自己的自定义控件可以重写 OnMouseDown 方法。CheckContext 方法调用 OnBeforeContextPopup,后者接着激发 BeforeContextPopup 事件。然后,事件处理程序就有机会更改或取消该上下文菜单,或者完成其他特定于应用程序的工作。RecognizeGesture 方法完成使跳动的点出现的实际工作。ShowContext 显示该上下文菜单。

图 5 上下文菜单技巧

在 Test Bench 程序上,单击“Context Menu”按钮,您将看到图 5 中显示的屏幕。顶部的灰框控件是一个正常创建的控件。单击它和它下面的列表框可以显示相关事件。正如前面说明的那样,如果您单击并保持不动,则不会发生鼠标按下事件。该屏幕的下半部分使用了一个控件,该控件利用了我们的技巧。该控件的行为就像正常的控件一样,不同之处在于,您在用户触摸屏幕后立即得到那个重要的鼠标按下事件。

可设计的自定义控件

为 .NET Compact Framework 设计自定义控件不像为桌面设计自定义控件那样容易。如果能够使用设计器创建一个自定义控件,然后在设计应用程序时使用该新控件,那会很不错。在桌面上,这一切完全是无缝地工作,因为桌面 System.Windows.Form.Control 对象背后的代码内置了所有设计器支持代码。Microsoft.WindowsCE.Forms.Control 不具有该代码,因为该代码会消耗内存。

但是,您可以使用一个小技巧使这一情况发生。在设计新的自定义控件时,从窗体而不是控件开始。现在,您可以使用所有设计器工具来生成自定义控件。当然,需要为此付出代价,但是不太高:在对应用程序(不是控件)使用设计器时,您无法向设计器的工具箱中添加自定义控件,因为您所创建的是窗体,而不是控件。因此,您将需要使用如下所示的代码创建“控件”的实例:

MyControl = new MyCustomControl();

Controls.Add(MyControl);
MyControl.Location = new Point(x, y);
MyControl.Size = new Size(dx, dy);
MyControl.Visible = true;

请注意,我们同时设置了位置和大小。当您使用窗体作为控件时,.NET Compact Framework 将自动根据使用全屏的需要设置它的大小。要绕过这一行为,您必须自己设置大小。

但是,有一个小问题。在某些情况下,添加实际上是窗体的自定义控件将导致屏幕顶部的标题中出现额外的闪烁,因为它首先更改为显示新窗体中的标题,然后又更改回来。绕过该行为需要撤消我们的技巧。我们需要将基类重新更改为控件!要让该控件可以根据它是希望成为窗体还是控件进行翻转,您可以添加一个条件编译常量,并且为您基于该窗体技巧创建的每个自定义控件修改类定义。

图 6 DESIGN 条件常量

图 6 显示了名为 DESIGN 的条件常量,它只在编译的调试版本中设置。修改自定义控件(窗体)的类定义,以使其看起来如下所示:

   // DESIGN is only set in Debug builds

#if DESIGN
public class MyCustomControl :
System.Windows.Forms.Form
#else
public class MyCustomControl :
System.Windows.Forms.Control
#endif

图 7 自定义控件

如果 DESIGN 常量未定义(对于发布模式不应当如此),则自定义控件将作为控件而不是窗体编译。对于调试模式,您可以使 DESIGN 常量保持定义,并且在该模式下进行测试。但是,如果您愿意的话,可以总是移除它以便测试代码,然后重新添加它以便可以重新设计窗体。如果您先是将其移除,然后又添加了它,则您可能需要重新编译应用程序,然后 Visual Studio 才能重新意识到它可以设计该窗体。

在 Test Bench 应用程序中,单击“Custom Control”按钮将呈现一个带有该自定义控件的对话框(请参见图 7)。如果您转到 Test Bench 的源代码,则您将看到 Tests 文件夹中的 MyCustomControl 类,您能够从设计器中编辑该类(请参见图 8)。

图 8 MyCustomControl

优化

编写既小又快的代码总是一个好主意,但是在为 .NET Compact Framework 编写代码时,这一点却是关键性的。您确实没有很多的内存和很高的速度可用。在设计时,您需要考虑将要执行什么代码,以及将如何使用您的数据结构。由于内存非常宝贵,因此避免制作数据副本很重要。而最为重要的是,代码只应在需要数据时才创建它,并且在完成工作时丢弃它。

另一个需要考虑的问题是,何时使用 .NET 代码以及何时使用 Win32 代码。有时,使用 Win32 代码要快速和容易得多(的确如此!)。您将很高兴地知道,大多数时候,使用 .NET 代码要更容易。尽管如此,请注意,我们不是鼓励您“回到远古时代”,只是为了使应用程序提高一点儿性能。

简言之,.NET Compact Framework 对于内存管理、事件处理、面向对象的编程、用户输入、国际化和文件 I/O 很擅长。.NET Compact Framework 面临最大挑战的领域是图形、窗口管理和资源管理。通过放弃 Graphics 对象并返回到 Win32 GDI,可以获得速度和功能方面的巨大改善。对于窗口管理,可以调整 .NET Compact Framework 代码以显著改善性能。但是创建窗口需要花费很长时间。如果您具有一个选项卡控件,并且该控件带有跨很多个页的很多个控件,则该选项卡控件的初始化可能需要相当长的时间。这是因为每个页上的每个控件都是在加载时创建的。稍后,我们将介绍选项卡控件类的替代物,它主要是带有少量 Win32 调用的 .NET 代码,该代码会产生一个能够飞快加载并动态加载或放弃不可见页的选项卡控件。

衡量应用程序性能的一种方式是使用 Environment.TickCount 属性,如下所示:

int t = Environment.TickCount;

//
t = Environment.TickCount - t;

当然,没有什么事情会如此容易。由于 .NET 可执行文件实际上是由 Microsoft® 中间语言 (MSIL) 代码组成的,因此涉及到即时 (JIT) 编译器。第一次调用您的函数时,所花费的时间可能比随后的调用多。在运行测试时,请记住这一点。还要注意,模拟器和实际设备可能就完成任务所需要的时间量给出极为不同的结果。因此,您将需要在实际设备上运行所有基准测试,并且不能通过调试器。

我们的第一个示例需要回溯到 Microsoft Systems Journal 1995 年 5 月刊中的一篇题为“Grab Bag of Gotchas and Goodies for Programming in Windows”的文章。在经过这么多年以后,ExtTextOut 仍然是在屏幕上显示文本的最快速的方式。它还可以用透明的背景进行绘制,而 .NET Compact Framework 中不支持所绘制的标签(只须在 PictureBox 上方放置一个标签,您就可以了解这一点)。

图 9 GDI 速度测试

在 Test Bench 程序上,运行 GDI 速度测试。当您第一次运行该测试时,JIT 编译器将污染结果。在我们的设备上,常规的、使用 DrawString 的 .NET 方法需要 51 毫秒,而使用 GDI 方法 ExtTextOut 只需要 22 毫秒。如果您花费时间再次运行该测试,您将看到速度明显提高,如图 9 所示。

让资源变得丰富

当需要向应用程序中添加图像和文本时,使用资源而不是对文本进行手动编码或者使用外部图像文件是一个好主意。Visual Studio .NET 2003 具有创建字符串资源所需的所有工具,但是它缺少内置的对管理图像资源的支持。因此,您需要求助于 Visual Studio .NET 随附的 ResEditor 示例应用程序,或者求助于第三方应用程序。我们最喜欢的是一个名为 Resourcer for .NET 的免费程序,您可以从 www.aisto.com/roeder/dotnet 获得它。它的用法很简单,并且附有源代码,因此,如果您愿意,可以对其进行更改。

当您单击 Resources 按钮时,Test Bench 程序会将一个位图资源加载到窗体中。Test Bench 包含一个名为 MyResources 的类,该类使得这些资源的使用变得容易得多。以下是您为加载图像而需要完成的所有工作:

m_image = MyResources.Icons.LoadBitmap("resTest");

仅仅通过使用“Add New Item”对话框添加新的程序集资源文件,就可以创建字符串资源文件。为了将资源组合到一个位置,我们在名为 Resources 的文件夹中创建了一个名为 strings.resx 的文件。双击 strings.resx 文件,您将看到一个屏幕,如图 10 所示。要加载字符串,只须完成以下工作:

string s = MyResources.Strings["id"];

正如我们在前面提到的那样,使用图像资源要求使用外部资源编辑器。如果您使用的是 Resourcer,则可以从“Add”菜单中选择“Add File”,输入要添加的图像资源的名称,然后选择文件。如果您运行示例应用程序并单击“Resource Test”按钮,则您将看到从资源文件中同时加载了一个字符串和一个图像。

图 10 添加字符串资源文件

如果您要编写商业应用程序,则有很多理由将资源放到单独的 DLL 中。最重要的理由是您可以提供各种皮肤。如果您可以为用户提供多种应用程序外观,则可以增加潜在的客户群。我们将不会详细讨论如何编写可换肤的应用程序,但是我们将假设字符串、位图和位置信息都需要放到某个 DLL 中,并且该 DLL 可以容易地与另一个包含不同皮肤的 DLL 进行交换。

用 .NET Compact Framework 动态加载 .NET DLL 并且从中获得资源并不是一件令人愉快的事情。即使是的话,您也会在您的资源被加载之前很快睡过去。对于 .NET 资源,还有一个恼人的行为:如果您从 .NET 资源中加载图像,然后调用 Dispose,则该图像会永久消失。如果您尝试从 .NET 资源中重新加载它,则资源加载程序将向您返回一个有效的、但不会绘制自身的 Image 对象。使该图像重新缓存自身的唯一方式是,创建资源加载程序的新实例(这要花费时间)。因此,让我们完全避开这些资源,并且从头开始。

在我们讨论加载位图以前,我们需要讨论如何呈现它们。在涉及到速度时,只要您使用的是设备相关位图 (DDB),Win32 BitBlt 函数就仍然能够满足需要。如果您尝试 Blt 到设备无关位图 (DIB),则会在速度方面遭受巨大打击。

在 Pocket PC 设备的系统目录内部,有一个可以解压缩下列所有种类的图像格式的 DLL:JPG、PNG 和 GIF(这里只举数例)。我们在我们编写的一个 C++ DLL 中利用了该功能,以便可以从 DLL 中加载图像资源,并且将该信息作为 DDB 返回给我们。该 DLL 具有两个公开的函数:

HBITMAP LoadBitmapFromFilename (HDC hdc, LPCTSTR szFileName);

HBITMAP LoadBitmapFromResource (
HDC hdc, DWORD dwResourceID, LPCTSTR pcszClass, HMODULE hModule);

这两个函数的第一个参数是一个设备上下文句柄 (HDC)。我们将使用我们的具有相应名称的 Win32 类获得当前窗体的 HDC,并将其传递到这些函数中。然后,这些函数使用传入的 HDC 来判断如何将 DDB 返回到您的应用程序中。第一个函数非常容易。第二个函数可以用一些代码片段说明。以下是 C++ 资源 DLL 的 RC 文件中的代码:

1001    PNG    DISCARDABLE    "msdn.png"

以下则是能够将该资源加载到应用程序中的代码:

[DllImport("ImageLoader.dll", CharSet=CharSet.Unicode)]

public static extern IntPtr LoadBitmapFromResource(
IntPtr hdc, uint dwResourceID, string pcszClass, IntPtr hModule);
...
hInstance = Win32.LoadLibary ( stringDLLName );
if (hInstance != IntPtr.Zero)
{
IntPtr hdc = Win32.GetDC ( this.Handle );
hbmp = LoadBitmapFromResource ( hdc, 1001,
"PNG", hInstance );
Win32.ReleaseDC ( hwnd, hdc );
Win32.FreeLibrary ( stringDLLName );
}

您会发现,该代码非常快速地加载位图。并且,由于 JPG、PNG 和 GIF 格式受到支持,因此资源 DLL 可以非常小。除了图像以外,您还可以使用 Win32.LoadString 方法来填充特定于语言的信息,甚至是字符串形式的位置信息。

图 11 资源测试

在我们的 Test Bench 应用程序中,单击“Resource Test”按钮以查看比较(请参见图 11)。在“Resource Test”屏幕中,单击“Load”按钮从资源 DLL 中加载位图(使用您刚刚看到的代码,并使用标准的 .NET 资源调用)。每一部分代码都会加载资源,绘制它,然后丢弃它。当您第一次运行测试时,您将会看到在速度方面存在令人吃惊的差异。随后的运行不像这样令人印象深刻,但仍然有差异。例如,我们执行的下一次运行对于 .NET Compact Framework 产生了 30 毫秒的加载时间,对于我们的 DLL 产生了 7 毫秒的加载时间。

只加载需要的信息;扔掉不需要的信息

良好的优化始于良好的内存管理。我们最喜欢的 .NET 编程方法是 Dispose。由于我们不是生活在拥有无穷内存的极乐世界中,因此在编写代码时考虑它所使用的资源是很重要的。对于每个窗体,您都需要在激活和停用时为其添加代码,如下所示:

protected override void OnActivated(EventArgs e)

{
base.OnActivated (e);
}
protected override void OnDeactivate(EventArgs e)
{
base.OnDeactivate (e);
}

在这些函数中,必须确保加载并丢弃对于应用程序而言不是非常关键的任何信息。这里是我们加载并丢弃我们的位图的地方。

同样的思想也适用于您创建和使用的对象。当您不再使用对象并且可以释放这些对象的任何时候,您都应当释放它们。并且,所有对象都应当即时创建,除非对于应用程序的正常运行绝对必要。这包括窗体本身。窗体和控件本身占用内存。当您关闭窗体时,也应该处置该窗体。

C# 中的 using 关键字能够很好地帮助您进行处置。请看以下示例:

using ( MyClass thing = new MyClass ())

{
// Do something interesting here
}

在大括号内部,thing 对象有效。一旦程序控制离开大括号,thing 的 Dispose 方法就被自动调用,并且 MyClass 的实例使用的任何资源都被释放(尽管垃圾回收器仍将负责释放 thing 本身所占用的内存)。当然,MyClass 对象必须支持 IDisposable 接口以使该机制工作。

可靠的应用程序体系结构使该方案更加容易实现。一种编程范例称为 Mediator Model。简而言之,按照 Mediator Model 编写的应用程序被进行了适当的安排,以便所有对象除了知道那些直接位于其下方的对象以外,不再知道任何对象。对象不知道它们的父对象,也不知道它们的同辈。与父对象之间的通讯是完全通过事件实现的,与直接后继的通讯是使用方法实现的。通过遵守该模型,可以使在对象树的整个部分不再使用时动态修剪它们变得非常容易,并且使内存管理变得更加容易。例如,让我们假设您的应用程序创建了对象 A,而该对象本身又创建并使用了对象 B,后者又创建并使用了一个名为 C 对象。

当应用程序的这一部分不再需要时,顶层对象 A 只是调用 B.Dispose。然后,B.Dispose 调用 C 对象的 Dispose 方法。因此,从 A 往下的所有资源都被删除。如果您最后又在其他应用程序中重用了该代码,或者您在当前应用程序中四处移动它,则只要您坚持 Mediator Model 的规则,这些对象的初始化和清理工作就将很可靠。

除了使总体内存足迹保持较小以外,只加载所需信息的另外一个理由是减少应用程序加载时间。所加载并缓存的每个不需要的窗体、位图或数据库都会导致更长的加载时间。由于 Windows®CE 是即时启动类型的操作系统,因此用户将期望他们的应用程序具有相同的特征。

线程可以帮助您减少加载时间。可以使用它们建立不重要的结构,而让主应用程序完成必需的活动,例如,检查注册、登录、显示启动画面或者让用户浏览主屏幕。如果程序具有很多可从工具栏或菜单使用的功能,则可以在线程中加载可选的功能,并且增长或启用菜单或工具栏,以便随着后台线程完成加载而显示更多的功能。

将所有概念结合在一起 — TabControlEx 类

现在,应该将我们已经讨论的很多概念结合在一起了。.NET Compact Framework 中的 TabControl 充满了问题。最大的问题是所有页和它们的控件都在启动时加载。这意味着在选项卡控件呈现之前存在较长的加载时间,并且会使用很多内存。我们已经编写了一个新类 — 我们聪明地将其称为 TabControlEx。它是一个从头生成的选项卡控件,它使页可以仅当用户单击该选项卡时才加载。TabControlEx 还使页可以在用户单击另外一个选项卡时被丢弃。如果 Input Panel 出现或消失,这些页将自动调整大小。还有其他一些很好的技巧,例如,对可见性进行更好的控制。有关与 TabControlEx 类相关联的属性和方法的列表,请参见图 12

使用 TagControlEx 时,最大的区别是,您不再创建页并将它们添加到集合中。相反,您将页的类型添加到该集合中:

m_tab.TabPages.Add("Tab Text", typeof(MyPage));

TabControlEx 类具有一个名为 LoadPage 的私有方法,它使用该类型信息创建控件实例。LoadPage 方法的内部有负责创建实际对象的代码:

private Control LoadPage(Type type, TabPageEx tab) {

...
ConstructorInfo constructor = type.GetConstructor(new Type[] {});
Control page = (Control) constructor.Invoke(null);
...
return page;
}

使用反射获得对默认构造函数的访问并调用它,是一种通过一个代码片段即时创建不同种类对象的优雅方式。

LoadPage 方法包含一些代码,这些代码试图从控件中获得一个 IPage 接口(请参见图 13),然后调用 InitPage 方法:

IPage p = page as IPage;

if (p != null) p.InitPage(this);

您实现 IPage 接口的原因有两个。首先,您可能希望在加载需要与应用程序的其余部分通讯的控件之后,立即执行一些初始化工作。InitPage 方法具有单个参数 — 一个指向 TabControlEx 对象的指针。如果页需要与选项卡上的其他页通讯,则可以使用 TabPages 集合来查找另一个控件(如果它被加载的话)。您还可以注册以接收 PageLoadedEventHandler 事件,以便您可以在其他控件加载之后与它们交互。最后,您可以创建 TabControlEx 的子类,并使其具有一些额外的属性或方法,以便您的页用来与应用程序的其余部分交互。

其次,并非所有页都希望在输入面板可见时调整大小。如果是这种情况,则您可以让 IPage 接口的 CanResize 属性返回 false。如果您为该属性的实现返回 true,则将发生默认行为 — 调整页的大小。

有时,您可能希望知道页何时变为活动或非活动状态,以便您可以完成某些工作,例如,基于应用程序中的其他更改更新值。.NET Compact Framework 中的 TabControl 没有提供获得这样的通知的容易方式。通过实现只包含下列两个方法的 IPageActivate 接口(请参见图 13),可以非常容易地在页控件中获得该通知:Activated 和 Deactivated。这些方法仅当控件的特定实例被激活或停用时才调用。使用 TabControlEx 的 SelectedIndexChanged 事件,可以为所有页(而不是仅仅一个页)获得相同的信息。通常,当页需要知道它何时变为活动或非活动状态时,使用 IPageActivate 更为容易。但是,当您希望监视应用程序中的所有页时,SelectedIndexChanged 无疑更好。

在我们的 Test Bench 应用程序中,单击“Tab Control”按钮可显示选项卡控件测试对话框。TabTest 对象的构造函数显示在图 14 中。

向 TabControlEx 中添加工具栏按钮

选项卡页的最后一个可选接口提供了某种确实有用的功能,但是却难以使用 .NET Compact Framework 中的选项卡控件实现。让我们假设您具有一个菜单栏,并且您希望具有一些特定于选项卡控件的每个页的工具栏。使用我们的 TabControlEx,这确实很简单。您将需要向您的窗体而不是 Toolbar 中添加 MyToolbar 的实例。MyToolbar 是 Toolbar 的子类,它支持工具栏节的概念,即可以作为组显示或隐藏的工具栏按钮。图 13 中的 IPageToolbar 接口具有单个方法,该方法返回工具栏节的列表。图 15 显示了我们的 Test Bench 示例应用程序中用于在屏幕上设置几个工具栏图标的代码。

IPageToolbar.Sections 方法中的代码在该方法第一次调用时创建一个新的工具栏节。然后,我们向该节中添加了两个图标并将其返回。LoadIcon 只是一个 Helper 函数,如下所示:

private Icon LoadIcon(string name)

{
string path = Path.Combine(Utility.HomePath, name);
usin(Stream stream = File.OpenRead(path))
{
return new Icon(stream);
}
}

LoadIcon 获得 Utility.HomePath 属性,该属性是用于包装下面这团难以记忆的字符的单行方式:

public static string HomePath

{
get
{
return Path.GetDirectoryName(
Assembly.GetExecutingAssembly().GetName().CodeBase);
}
}

运行 Test Bench 示例并选择“Toolbar Test”按钮。您可以在这两个选项卡之间前后跳转,并且查看工具栏按钮如何只为 Test 页显示。

您可能已经注意到了,对于这一带有工具栏的测试,以及未使用工具栏的 Custom Control 测试,我们使用了相同的控件 — MyCustomControl。如果 MyCustomControl 的 UseToolbar 属性为 false(这是默认值),则 IPageToolbar.Sections 方法返回 null。为了针对工具栏测试将该属性设置为 true,我们使用了前面提到的用于在页首次加载时与其进行通讯的技术。ToolbarTest 窗体的构造函数与 TabTest 窗体几乎完全相同,不同之处在于在构造函数的结尾添加的额外代码:

public ToolbarTest()

{
...
tab.PageLoaded += new
TabControlEx.PageLoadedEventHandler(tab_PageLoaded);

// Create an instance of our toolbar needed to allow
// each page to have its own toolbar buttons.
MyToolbar toolbar = new MyToolbar();
Controls.Add(toolbar);
}

首先,我们进行注册以便接收选项卡控件的 PageLoadedEventHandler 事件,该事件使我们可以在控件加载之后立即设置控件的属性。我们将使用该事件将 UserToolbar 属性设置为 true:

private void tab_PageLoaded(object sender, PageLoadedEventArgs e)

{
MyCustomControl control = e.Page as MyCustomControl;
if (control != null) control.UseToolbar = true;
}

在创建事件处理程序之后,我们向窗体中添加了 MyToolbar 的一个实例,它是 Toolbar 类的支持工具栏节的子类。

小结

为 .NET Compact Framework 编写应用程序是一种奇妙的经历。这些应用程序足够小,以至于共享软件开发人员可以再一次自行编写某种东西,并且以联机方式销售它来赚一点儿钱。但是,移动设备的简单性和较小的容量为程序员带来了新的挑战。您没有令人敬畏的权力,却具有令人恐惧的责任。请努力从一开始就设计快速、紧凑并且具有极高可处置性的代码。

Dave Edson 当前是 Software Architect for Potala Software, LLC 的按摩治疗专家、弹子机大王和软件架构师。他与自己的妻子和两个孩子一起生活在西雅图。现在,他把所有可能的余暇时间都花在滑雪和水上滑板运动上面。

John Socha-Leialoha 是 Potala Software, LLC 的首席软件架构师。他过去的成就包括编写 Norton Commander。他与自己的妻子和儿子一起生活在西雅图。他把所有可能的余暇时间都花在为他的铁路模型爱好/业务建造小得可笑却又极其精确的仿真模型上面。

转到原英文页面

抱歉!评论已关闭.