65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows Mobile 应用开发第四部分:添加自定义控件并利用 GPS 硬件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (9投票s)

2009 年 10 月 30 日

Ms-PL

20分钟阅读

viewsIcon

62266

学习如何在应用中添加自定义控件并利用 GPS 硬件。

引言

开发 Windows Mobile 设备的应用与开发桌面应用类似,尤其是如果您使用 Visual Basic .NET 或 Visual C#。您可以使用相同的开发工具来开发您的应用,尽管开发 Windows Mobile 设备应用与开发桌面应用之间存在差异。设备屏幕更小,资源有限,极其便携,并且通常由电池供电。

本文提供有关如何为 Windows Mobile 设备开发 Windows Forms 应用的信息。在本文中,您将学习如何向自己的应用添加自定义控件,以及自定义控件如何获得完整的可视化设计器支持。您还将学习如何在自己的应用中使用 GPS 硬件,以及如何在多线程应用中更新用户界面控件。

向您的应用添加自定义控件

如果您正在为 Windows Mobile 设备开发应用,您可能有时需要 .NET Compact Framework 的通用控件集中未提供的特定用户界面控件。在这种情况下,您有多种选择:

  • 创建仅在您当前正在开发的应用程序中使用的用户界面控件
  • 创建可在多个应用程序中使用的用户界面控件
  • 从第三方购买具有所需功能的 UI 控件

在本文中,我们将介绍三种不同类型的控件,您可以从头开始创建它们,或者通过重用和组合现有控件来创建它们。

用户控件

这些控件通常用于组合几个现有控件以创建新控件。在 Visual Studio 2008 中,提供的 Visual Studio 2008 项目类型完全支持用户控件的创建。创建用户控件后,您将获得一个可视化设计器表面,该表面充当容纳其他控件的容器。使用用户控件,您可以创建复合控件,例如,它包含多个现有控件,并在应用程序中使用时表现得像一个单独的控件。在图 1 中,您可以看到 Visual Studio 2008 中的用户控件。该用户控件包含两个 Label 和两个 TextBox,可用于输入用户凭据,例如登录数据库。项目构建后,此用户控件将存储在其自己的程序集中,任何智能设备项目都可以重用它。在这个特定的用户控件示例中,将现有自定义控件拖放到其中后,几乎不需要额外的代码。此用户控件中唯一的额外代码是声明两个公共属性,用于检索应用程序中的密码和用户名,以及设置用户名。Password 属性是只读的,以强制应用程序用户至少提供一个有效的密码。密码验证将在应用程序内完成,而不是在用户控件内完成,以使其尽可能灵活和准备好重用。

MOB4DEVS04/mob04fig1.jpg

图 1 - 用于输入凭据的用户控件

要在应用程序中使用用户凭据控件,只需将其从工具箱中拖放即可。用户凭据控件不会自动出现在工具箱中。但是,可以通过右键单击工具箱的**“通用”**区域,然后从显示的弹出菜单中选择**“选择项”**,并导航到存储用户凭据控件的程序集来将控件添加到工具箱。在图 2 中,您可以看到“用户凭据”控件如何在应用程序中显示。如果您查看工具箱,您会在**“通用”**区域中看到“UserCredentials”控件,您可以从中将其拖放到窗体上。您还可以看到在**“属性”**窗口中定义了一个 UserName 属性,该属性可以从设计器设置,也可以在应用程序内部以编程方式设置。您还可以看到 Password 属性在“属性”窗口中为灰色,这意味着它是一个只读属性。它无法设置,但一旦用户输入了密码,应用程序就可以检索该属性。

MOB4DEVS04/mob04fig2.jpg

图 2 - 应用程序中的用户控件

继承控件

有时您可能想使用一个控件,比如说 .NET Compact Framework 中提供的某个通用控件。但也许您希望该控件的行为略有不同。例如,如果您想使用 TextBox,但要求 TextBox 只能接受数字。当然,您可以让用户在 TextBox 中输入任何内容,并在应用程序中验证输入的数据。但是,您也可以创建自己的控件,要么在应用程序内部,要么作为一个单独的控件,该控件派生自 TextBox,但仅限于字符输入。这样,您的应用程序需要编写的代码就更少了,因为验证是由**继承的 Textbox** 完成的。尽管此类 Textbox 的功能与通用控件相比有限,但派生的 Textbox 仍然具有出色的可视化设计器支持,并公开了为原始 Textbox 定义的所有**属性**和**事件**。此外,如果您为它创建一个单独的控件,您也可以在其他应用程序中重用该控件,这会提高您的工作效率。为了创建一个单独的**数字 Textbox** 控件,您可以创建一个新的**智能设备项目**并选择**类库**。将 Textbox 转换为 Numeric Textbox 所需的代码量有限,尤其是在此示例中,因为它省略了小数点的使用,只允许输入数字和退格键。

public class NumericTextBox : TextBox 
{ 
     protected override void OnKeyPress(KeyPressEventArgs e) 
     { 
         if ((e.KeyChar < '0' || e.KeyChar > '9') && e.KeyChar != (char)Keys.Back) 
         { 
              SystemSounds.Beep.Play(); 
              e.Handled = true; 
         } 



         base.OnKeyPress(e);
     }
}

编写重写代码时,确定基类方法的作用很重要,以便决定是删除它还是调用它,以及在后一种情况下何时调用它。只需查看文档,其中包含“**继承者的注意事项**:在派生类中重写 OnKeyPress 时,请务必调用基类的 OnKeyPress 方法,以便已注册的委托能够接收事件。”。由于 TextChanged 事件的侦听器也只期望数字数据,因此在验证了输入的字符后,您会调用基类方法。使用 Numeric Textbox 应用程序的使用方法与本文上一节中看到的“UserCredentials”控件一样简单。

MOB4DEVS04/mob04fig3.jpg

图 3 - 用户凭据和 NumericTextBox 在设计器模式下以及在设备模拟器上执行

自定义控件

如果您不打算重用现有的 UI 控件,但需要全新的控件,您可以创建自己的自定义控件。通常,这些类型的控件是从头开始编写的,尽管您可以,例如,让它们继承自 System.Windows.Forms.Control 类,以至少利用控件共有的共享功能。它们也经常被称为**所有者绘制控件**。创建自定义控件没有现成的 Visual Studio 2008 项目模板。最简单的方法是在 Visual Studio 中创建一个新的用户控件。创建项目后,您可以删除用户控件并从 Visual Studio 2008 中手动添加一个新的自定义控件。

public partial class CustomControl1 : Control 
{ 
    public CustomControl1() 
    { 
        InitializeComponent();
    } 

    protected override void OnPaint(PaintEventArgs pe) 
    { 
         // TODO: Add custom paint code here
         // Calling the base class OnPaint 
         base.OnPaint(pe); 
    }
}

从头开始创建自定义控件意味着您有责任自己绘制控件。您可以为此重写 OnPaint 方法,当您让 Visual Studio 2008 创建新的自定义控件时,该方法已添加到您的源文件中。由于控件的绘制可能经常发生,因此重要的是要使您的实现高效,无论是性能还是资源使用。由于您通常在 OnPaint 方法中使用 Graphics 类,并且 Graphics 类经常依赖于底层原生代码,因此在绘制完成后清理资源非常重要。在 C# 中,使用实现了 Dispose 方法的 Graphics 类并结合 using 语句并不是一个坏主意。using 语句可确保即使在调用对象上的方法时发生异常,也会调用 Dispose,这意味着您无需担心异常处理。例如,您想显示一些文本;您可以按照以下代码片段所示进行操作,并从 OnPaint 方法内部调用它,传递作为 PaintEventArgs 参数一部分的 Graphics 对象。

private void DisplayLabelInfo(Graphics g, string labelText, int xPos, int yPos)
{ 
    using (Font labelFont = new Font(FontFamily.GenericSerif, 10.0F, FontStyle.Regular))
    { 
        using (SolidBrush labelBrush = new SolidBrush(LabelForeground))
        { 
           g.DrawString(labelText, 
                        labelFont, 
                        labelBrush, 
                        xPos, yPos);
        }
    }
}

尽管这只是一个简单的显示控件内文本的方法,但您已经可以看到需要两个不同的 Graphics 对象、一个 Font 和一个 SolidBrush,它们将在每次调用 DisplayLabelInfo 方法时创建。由于您将这些对象嵌入 using 语句中,因此在对象超出范围时会调用 Dispose 方法,从而使对象的清理工作高效,从而减少了垃圾回收期间的活动,因为无需执行 Finalize 方法。这在处理 Graphics 类时尤其有价值,因为它们依赖于有限的原生资源,这些资源应尽快释放。

如果您直接在屏幕上使用 Graphics 类型的实例进行绘制,则屏幕更新可能会导致闪烁,尤其是在 OnPaint 方法中执行大量代码时。在这些情况下,使用缓冲区可能会有所帮助。这意味着您首先在处理内存的另一个 Graphics 实例上绘制所有内容,然后在绘制完成后,将内存中的 Graphics 对象的所有内容传输到绘制在屏幕上的 Graphics 对象。

假设您想显示图像,并在其之上写一些文本。要使用缓冲区,您将执行以下操作:

protected override void OnPaint(PaintEventArgs pe)
{
    // Initialize a Graphics object with the bitmap we just created 
    // and make sure that the bitmap is empty. 
    Graphics memoryGraphics = Graphics.FromImage(memoryBitmap); 
    memoryGraphics.Clear(this.BackColor); 
    // Draw the control image in memory 
    memoryGraphics.DrawImage(someImage, 
                             destRect, 
                             imageRect, 
                             GraphicsUnit.Pixel); 
    // Display some text 
    DisplayLabelInfo(memoryGraphics, “Some text”, 0, 0); 
    // Draw the memory bitmap on the display 
    pe.Graphics.DrawImage(memoryBitmap, 0, 0); 
    // Calling the base class OnPaint 
    base.OnPaint(pe); 
}

在上面的代码片段中,假定您已经创建了一个名为 memoryBitmapBitmap 类型实例,并且您已经创建并初始化了一个名为 someImageImage 类型实例,以及一个用于绘制图像的目标矩形。在 OnPaint 方法的末尾调用 base.OnPaint 方法意味着 Paint 事件的订阅者有机会在您已在自己的 OnPaint 方法中显示的内容之上进行绘制。

要为自定义控件添加可视化设计器支持,您必须创建一个 .xmta 文件,这是一种特殊的 XML 文件。Visual Studio 2008 通过提供**IntelliSense** 以及在解决方案资源管理器中向项目添加初始**设计时属性文件**的可能性来支持您创建此文件。以下代码片段显示了一个设计时属性文件示例,它将自定义控件的属性放在“属性”窗口的特定类别中,并显示有关该属性的一些帮助。

<?xml version="1.0" encoding="utf-16"?>
<classes xmlns="http://schemas.microsoft.com/VisualStudio/2004/03/SmartDevices/XMTA.xsd">
  <class name="Compass.Compass">
    <property name="Heading">
      <category>CompassSpecific</category>
      <description>Sets the travelling speed in mph</description>
    </property>
  </class>
</classes>

在应用程序中使用自定义控件

创建自定义控件后,您自然希望在应用程序中使用它。如果您已正确为自定义控件添加了可视化设计器支持,并且已将自定义控件添加到 Visual Studio 2008 工具箱,那么添加自定义控件将和向用户界面添加通用控件一样简单。如果您查看图 4,您可以看到一个电子罗盘的用户界面,该界面使用了**自定义控件**——**Compass 控件**。您可以看到**Compass 控件**具有完整的可视化设计器支持,并且您可以设置该控件上的多个属性。此外,您还可以看到所有属性都有默认值,并且包含有关属性用法的帮助。为了创建这个特定的用户界面,一个**Compass 控件**已被拖放到 **MainForm**,并添加了一个**菜单**以启用/禁用从(内置)GPS 接收器检索数据。

MOB4DEVS04/mob04fig4.jpg

图 4 - 使用自定义控件的电子罗盘应用程序

一旦应用程序获得 GPS 位置信息,用户的方向和行进速度就会连续显示在**Compass 控件**上。为了更新此信息,**Compass 控件**公开了一些可以通过编程方式设置的属性,这些属性将被分配从 GPS 接收器读取的值。

为应用程序添加位置感知功能

由于 Windows Mobile 5.0 和 Windows Mobile 6 设备都包含GPS 中间驱动程序 (GPSID),因此作为开发人员,您可以非常轻松地通过 GPS 检索位置信息,并在不同的应用程序之间共享 GPS 硬件。随着越来越多的设备拥有内置 GPS 接收器,开始考虑让您的应用程序具有位置感知功能是有意义的。

Windows Mobile 5.0 SDK 和 **Windows Mobile 6 SDK** 都包含大量的示例代码。其中一个示例不仅展示了如何使用 GPSID,还包含了一个围绕 GPSID 功能的托管包装器。您可以在以下文件夹中找到这些示例:<Installation Folder>\<Windows Mobile SDK>\Samples\PocketPC\Cs\Gps<Installation Folder>\<Windows Mobile SDK>\Samples\Smartphone\Cs\Gps

在您能够使用围绕 GPSID 的托管包装器之前,您需要构建存储在上述文件夹中的**GPS 解决方案**。如果您使用 Visual Studio 2008 来构建 GPS 解决方案,系统将要求您先转换项目才能构建它。原因是示例代码的解决方案和项目文件是使用 Visual Studio 2005 创建的,并且在 Visual Studio 2008 中未经转换就无法使用。转换后,您可以构建 GPS 解决方案。构建完成后,您就可以利用**Microsoft.WindowsMobile.Samples.Location** 程序集中的功能了。为此,您需要将此程序集导入到您自己的解决方案中。添加了对围绕 GPSID 的托管包装器的引用后,您就可以使用以下类,本文相关的属性和方法在图 5 中显示。

MOB4DEVS04/mob04fig5.jpg

图 5 - 围绕 GPSID 的托管包装器的类图

Gps 类是您访问 GPS 硬件的入口点。在此类中,您可以找到同步检索 GPS 位置信息的方法,以及位置信息更改时触发的事件。您还可以找到打开和关闭 GPS 硬件的方法。

在能够从 GPS 接收器检索位置信息之前,您需要创建一个 Gps 类型的对象,并在此对象上调用 Open 方法。此操作将激活 GPS 硬件,前提是您的应用程序是第一个使用 GPS 硬件的应用程序。一旦您的应用程序使用完 GPS 硬件,您就需要在此 Gps 对象上调用 Close 方法,以便在您的应用程序是最后一个使用它的应用程序时关闭 GPS 硬件。通过这种方式,您可以确保在 Windows Mobile 设备上不再需要 GPS 功能时节省电池电量。以下代码片段展示了如何打开和关闭 GPS 硬件的连接,并在单独的事件处理程序中执行:

private void menuEnableGPS_Click(object sender, EventArgs e)
{
    gps.Open();
    gps.LocationChanged += 
      new LocationChangedEventHandler(gps_LocationChanged);
    menuDisableGPS.Enabled = true;
    menuEnableGPS.Enabled = false;
}
private void menuDisableGPS_Click(object sender, EventArgs e)
{
    gps.LocationChanged -= gps_LocationChanged;
    gps.Close();
    menuEnableGPS.Enabled = true;
    menuDisableGPS.Enabled = false;
}

如果用户决定终止应用程序,您还必须确保关闭 GPS 连接,因此在应用程序的 Closing 事件处理程序中验证用户是否仍启用 GPS,然后取消订阅 LocationChanged 事件并在此 Gps 对象上调用 Close 方法是很有意义的。电子罗盘应用程序的“实际”工作将在 gps_LocationChanged 事件处理程序中执行。任何应用程序都可以通过提供事件处理程序来订阅此事件。每次 GPS 位置信息更改时,都会调用应用程序的事件处理程序,以允许应用程序根据位置变化进行操作。订阅 LocationChanged 事件可确保您的应用程序始终具有最新的位置信息,前提是 GPS 硬件能够读取有效的卫星数据。以下代码片段显示了 gps_LocationChanged 事件处理程序的原始版本。

void gps_LocationChanged(object sender, LocationChangedEventArgs args)
{
     GpsPosition pos = args.Position;
     compass1.HeadingValid = pos.HeadingValid;
     if (pos.HeadingValid)
     {
         compass1.Heading = pos.Heading;
     }
     compass1.SpeedValid = pos.SpeedValid;
     if (pos.SpeedValid)
     {
         compass1.Speed = pos.Speed * 1.152;   // convert knots to MPH
     }
     if (pos.HeadingValid | pos.SpeedValid)
     {
         compass1.Invalidate();
     }
}

在此事件处理程序中,您只查看 HeadingSpeed 属性,尽管还有大量其他属性。由于该应用程序只是一个电子罗盘,因此您甚至省略了 LattitudeLongitude 等位置信息。由于卫星读数可能无效,您还需要检查您感兴趣的属性是否包含“真实”数据。最后,您调用 Invalidate 方法来强制更新 Compass 控件,前提是至少有一个读数已更改。

如果您编译并运行应用程序,并实现上面代码片段中所示的 gps_LocationChanged 事件处理程序,您将遇到一个异常。看起来您无法更新 Compass 控件上的信息。异常的描述会指向一个方向。异常消息包含以下文本:“Control.Invoke 必须用于与在单独线程上创建的控件进行交互”。堆栈跟踪显示以下信息:

StackTrace: 
   at Microsoft.AGL.Common.MISC.HandleAr(PAL_ERROR ar)  
   at System.Windows.Forms.Control.Invalidate() 
   at ElectronicCompass.MainForm.gps_LocationChanged(Object sender, 
      LocationChangedEventArgs args) 
   at Microsoft.WindowsMobile.Samples.Location.Gps.WaitForGpsEvents()

异常消息包含识别此问题的最重要信息。似乎您的应用程序中存在多个活动线程的问题,尽管您没有创建其他线程。从**堆栈跟踪**中,您可以得出结论,发生异常的 Invalidate 方法是从您的事件处理程序内部调用的,而该事件处理程序又是由围绕**GPS 中间驱动程序**的托管包装器中的某个方法调用的。显然,GPSID 或其托管包装器使用多个线程,这是您在自己的应用程序中必须注意的。

MOB4DEVS04/mob04fig6.jpg

图 6 - 更新 Compass 控件会导致异常

从多个线程更新用户界面控件

许多开发人员会犯的一个常见错误是尝试直接从工作线程更新或访问用户界面控件。此操作会导致意外行为;在 .NET Compact Framework 1.0 版本中,应用程序经常无响应。在 .NET Compact Framework 2.0 及更高版本中,行为有所改善,因为当您尝试从创建者的线程以外的线程更新用户界面控件时,会引发 NotSupportedException。在图 6 中,您在电子罗盘应用程序中看到了这种特定行为。

要解决此问题,请遵循以下规则:*只有创建 UI 控件的线程才能安全地更新该控件*。如果您需要在工作线程中更新控件,则应始终使用 Control.Invoke 方法。此方法在拥有控件底层窗口句柄的线程上执行指定的委托,换句话说,就是创建控件的线程。有了这些知识,您现在可以修改 gps_LocationChanged 事件处理程序。为此,您首先必须声明一个 delegate,以便能够将方法作为参数传递给 Control.Invoke 方法。一个 delegate 只是一个定义了方法签名的数据类型,它可以与任何具有兼容签名的方法关联。

private delegate void UpdateDelegate();
void gps_LocationChanged(object sender, LocationChangedEventArgs args)
{
    GpsPosition pos = args.Position;
    compass1.HeadingValid = pos.HeadingValid;
    if (pos.HeadingValid)
    {
        compass1.Heading = pos.Heading;
    }
    compass1.SpeedValid = pos.SpeedValid;
    if (pos.SpeedValid)
    {
        compass1.Speed = pos.Speed * 1.152;
        // convert knots to MPH before displaying
    }
    if (pos.HeadingValid | pos.SpeedValid)
    {
        compass1.Invoke((UpdateDelegate)delegate()
        {
            compass1.Invalidate();
        });
    }
}

gps_LocationChanged 事件处理程序中,您可以看到 compass1.Invalidate 不是直接调用的,而是通过另一个方法 compass1.Invoke 调用的。如果您不熟悉**匿名委托**,使用的语法可能会有些晦涩。在 gps_LocationChanged 事件处理程序中,使用了此 C# 2.0 功能通过 compass1.Invoke 调用 compass1.Invalidate。也可以定义一个单独的方法来调用 compass1.Invalidate,并通过 UpdateDelegate 委托来调用该方法。因为 Compass 控件的更新逻辑只在一个位置调用,所以使用匿名委托可以使代码更紧凑,并且(虽然是个人品味问题)可读性更好。

通过这些修改,现在就可以在不引发异常的情况下运行电子罗盘应用程序,并通过**GPS 中间驱动程序**接收 GPS 卫星信息了。

MOB4DEVS04/mob04fig7.jpg

图 7 - 正在运行并显示 GPS 信息的电子罗盘

但是……等等,我听到您在说了。应用程序正在设备模拟器中运行,但它却在接收 GPS 卫星信息。这是怎么回事?

测试位置感知应用程序

如果您编写使用 GPS 硬件的应用程序,测试这些应用程序是一项挑战。许多 GPS 接收器在室内工作不佳,通常,您开发应用程序的地方。即使 GPS 接收器能够接收 GPS 信息,该信息也是静态的,因为您在开发应用程序时不太可能进行太多移动。为了克服这个问题,Windows Mobile 6 SDK 提供了一个名为 FakeGPS 的实用程序,它使用包含 GPS 信息的文本文件来模拟 GPS 接收器的功能。使用 GPS 中间驱动程序的应用程序可以使用 FakeGPS,并且其功能与存在 GPS 接收器时完全相同,无需进行任何修改。由于 FakeGPS 也在设备模拟器上运行,因此您甚至可以使用设备模拟器测试整个应用程序。

要使用 FakeGPS,您首先必须将其安装在目标设备或设备模拟器上。FakeGPS 以 CAB 文件形式提供。要将 FakeGPS 安装在设备模拟器上,您可以共享 FakeGPS 所在文件夹,这可以通过设备模拟器属性进行设置。

MOB4DEVS04/mob04fig8.jpg

图 8 - 共享 FakeGPS 所在文件夹

现在,您可以使用设备模拟器中的文件资源管理器导航到存储卡。在指向共享文件夹的存储卡上,选择 FakeGPS CAB 文件以将其安装在设备模拟器上。FakeGPS 附带了几个包含 GPS 数据的示例文本文件。您可以添加包含 GPS 数据的自己的文本文件来为特定测试场景创建测试文件。FakeGPS 文本文件的内容如下所示:

$GPGLL,4738.0173,N,12211.1874,W,191934.767,A*21 
$GPGSA,A,3,08,27,10,28,13,19,,,,,,,2.6,1.4,2.3*3E 
$GPGSV,3,1,9,8,71,307,43,27,78,59,41,3,21,47,0,10,26,283,40*77 
$GPGSV,3,2,9,29,13,317,0,28,37,226,37,13,32,155,36,19,37,79,42*42 
$GPGSV,3,3,9,134,0,0,0*46 
$GPRMC,191934.767,A,4738.0173,N,12211.1874,W,0.109623,12.14,291004,,*21 
$GPGGA,191935.767,4738.0172,N,12211.1874,W,1,06,1.4,32.9,M,-17.2,M,0.0,0000*75 
$GPGLL,4738.0172,N,12211.1874,W,191935.767,A*21 
$GPGSA,A,3,08,27,10,28,13,19,,,,,,,2.6,1.4,2.3*3E 
$GPRMC,191935.767,A,4738.0172,N,12211.1874,W,0.081611,15.81,291004,,*2A

要激活 FakeGPS 数据流,请在设备模拟器上启动 FakeGPS 实用程序,启用它,然后选择所需的 GPS 数据文件。最后,单击“完成”软键即可开始向 GPSID 提供 GPS 数据。

MOB4DEVS04/mob04fig9.jpg

图 9 - 在 FakeGPS 中选择 GPS 数据文件

这就是开始测试 GPS 启用应用程序所需的所有操作,无论是在设备模拟器上还是在物理 Windows Mobile 设备上。安装和设置 FakeGPS 后,您就可以开始测试电子罗盘了。

本系列相关文章

其他资源和参考

请访问 www.myrampup.com 以获取更多信息。

© . All rights reserved.