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

Windows Mobile 的属性头控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (11投票s)

2008年12月16日

CPOL

11分钟阅读

viewsIcon

82077

downloadIcon

315

是否曾想过创建一个在许多 Microsoft 应用程序中看到的、在 Windows Mobile 上具有标准外观和感觉的属性头部控件?那么,这里就是实现它的代码。

引言

Microsoft 推荐的 Windows Mobile UI 设计文档指出,Windows Mobile 应用程序中的每个窗体都必须具有相同的名称。示例如下:

假设,Form1 的标题是 My Acme App

Form1

点击“Do Stuff”按钮会加载另一个窗体,Form2

PropertyHeaderWM/img2.PNG

Form2

请注意,窗体的名称现在显示为“Do Stuff”。这是不好的,并且违反了 Microsoft 的 UI 设计建议,原因有很多,其中之一是:

PropertyHeaderWM/img3.PNG

任务管理器

你注意到问题了吗?我敢打赌你注意到了!我们在任务管理器(Windows Mobile 6.1 中的一个新功能)中有一个单独的引用。这真是个麻烦——尽管在 WM 6.1 上稍微宽容一些。如果用户从列表中选择 My Acme App(在这种情况下是子窗体),操作系统实际上会将顶层窗口置于 Z 顺序的顶部,在本例中是“Do Stuff”窗口——相当巧妙。在 WM 6.0 之前的版本中情况并非如此,所以请记住这一点。

那么,我们是否应该在整个应用程序中简单地命名每个窗口?当然可以,但这很简单。一种简单易行的方法是将窗口的所有者属性设置为父窗口(.NET Compact Framework 2.0 中的一项新功能)。因此,在显示Form2Form1按钮点击事件处理程序中,我们这样做:

private void button1_Click(object sender, EventArgs e)
{
   using (Form2 form2 = new Form2())
   {
       form2.Owner = this;
       form2.ShowDialog();
   }
}

现在,我们来看看结果:

PropertyHeaderWM/img4.PNG

Form1

Form1 看起来和改变之前一样,所以这里没有新内容。但是现在,如果我们点击“Do Stuff”按钮加载Form2,我们会得到以下结果:

PropertyHeaderWM/img5.PNG

Form2

请注意,即使Form2Text属性仍然设置为“Do Stuff”,标题现在也与Form1相同。

此外,如果我们现在检查任务管理器,我们会得到以下结果:

PropertyHeaderWM/img6.PNG

任务管理器

如你所见,只有一个名为“My Acme App”的应用程序。这真的很棒。但是,这可能会导致对不同窗体功能感到困惑,因为现在你加载的每个窗体都没有名称。如果你查看标准的 Microsoft 应用程序,你会注意到它们有一个“头部面板”控件,通常会描述窗体的功能。

PropertyHeaderWM/img7.PNG

一个典型的 Microsoft Windows Mobile 应用程序示例

开箱即用的控件没有提供这个功能,所以本文将讨论这个。我们将讨论如何创建一个,并在文章底部提供源代码。我们将展示如何添加帮助支持,以及一个可选的图标来表示窗体。我们还将讨论设计器支持,以便可以轻松地将其从 Visual Studio 的工具箱拖到窗体上。

必备组件

本文配套的代码示例需要安装了 Smart Device 扩展的 Visual Studio 2008 以及 Windows Mobile Professional 6.1 Professional 模拟器

创建自定义属性头部控件

我们将按顺序讨论每一步。在所需文件方面,我们只需要三个。XMTA 控件设计器文件,以及两个 C# 类:其中一个是源代码(不包括资源文件),另一个是用于处理图标加载的资源类。

我们选择使用 GDI,因为这样我们可以支持多种设备,而且 GDI 相当轻量级且易于编程。

1. 创建 PropertyHeader 类并继承自 UserControl

当然,你需要创建一个“类库”项目来托管该控件。我假设你已经知道如何在 Visual Studio 中完成此操作。我在本文中决定使用 C#。我将我的项目命名为 **Microsoft.Windows.Mobile.UI**。

首先,我们将从派生自UserControl(CF 2.0 的新支持)的PropertyHeader类开始。

namespace Microsoft.Windows.Mobile.UI
{
    public partial class PropertyHeader : UserControl
    {
       private string _title = "<Title goes here>";
       private string _desc = "<Description goes here>";
       const int DESIGNPOINTSPERINCH = 96;
    }
}

我们可以将该类定义为 partial,以防将来需要实现其他功能。

2. 实现控件的文本属性

因为我们要显示标题和描述,所以我们将它们公开为公共属性。

private string _title = "<Title goes here>";
private string _desc = "<Description goes here>";
const int DESIGNPOINTSPERINCH = 96;

  
/// <summary>
/// Gets or sets the header title of this control.
/// </summary>
public string Title
{
     get
     {
         return _title;
     }
     set
     {
         _title = value;
         Invalidate();
     }
} 

/// <summary>
/// Gets or sets the header description of this control.
/// </summary>
public string Description
{
    get
    {
        return _desc;
    }
    set
    {
        _desc = value;
        Invalidate();
    }
}

请注意,我们在每个 setter 中都调用了Invalidate。我们这样做是为了在值更改时重新绘制控件。

3. 实现控件的私有帮助属性

如前所述,我们希望给使用该控件的开发人员一个选项,是否显示帮助图标。因此,如果HelpRequested事件已被注册,我们将在用户单击帮助图标时引发该事件。这种功能在整个平台上都很常见。

通常,开发人员会在发生此事件时通过 System.Windows.Forms.Help加载 PegHelp。所以,我们需要创建几个 private 属性来帮助我们做到这一点。

/// <summary>
/// Private: Gets or sets the help icon.
/// </summary>
private Icon HelpIcon
{
    get; 
    set;
}

/// <summary>
/// Private: Gets or sets the help point location so we know when there is a 
/// mouse down event, whether to raise a click event.
/// </summary>
private Point HelpIconPoint
{
     get; 
     set;
}

HelpIcon属性是 private,并在对象构造时设置,以支持多分辨率设备。稍后我们将详细讨论这一点。如果我们绘制帮助图标,因为它是最右边的图标,所以其位置会在屏幕模式更改期间发生变化。当我们绘制帮助图标时,我们会将位置存储在HelpIconPoint属性中,作为一个Point对象;我们这样做是为了在MouseDown事件发生时,知道用户是否将触笔放置在帮助图标的边界内。稍后我们将详细讨论这一点。

4. 实现控件的其余属性

所以,我们还有一些属性需要实现,这些属性将允许视觉定制。

/// <summary>
/// Gets or sets the main Icon of this control.
/// </summary>
public Icon Icon
{
    get; 
    set;
}

/// <summary>
/// Gets or sets whether the help icon is displayed or not.
/// </summary>
public bool ShowHelpIcon
{
    get; 
    set;
}

/// <summary>
/// Gets or sets whether the line separator
/// is drawn at the bottom of this control.
/// </summary>
public bool ShowLineSeparator
{
    get; 
    set;
}

Icon属性包含图标的引用,如果开发人员选择设置的话,因为这是可选的。稍后你将看到我们在 paint 事件中如何处理这个问题。如果没有图标,那么TitleDescription属性将绘制在控件的更左侧。

如前所述,ShowHelpIcon允许开发人员显示帮助图标或不显示。值得注意的是,在控件的当前实现中,无法更改帮助图标,因为它是内部加载的。当然,如果需要,可以轻松更改。默认情况下,此属性为 true

ShowLineSeparator属性正是起这个作用!如果设置为 true,则会在控件底部绘制一条线。

5. 实现控件的构造函数

控件的构造函数如下所示:

public PropertyHeader()
{
    InitializeComponent();
    HelpIcon = CurrentAutoScaleDimensions.Height <= 96 ? 
       Resources.GetIcon("help.ico", 16, 16) :  
       Resources.GetIcon("help.ico", 32, 32);
    ShowLineSeparator = true;
    ShowHelpIcon = true;
}

InitializeComponent方法执行所需的生成器逻辑,因为我们继承自UserControl类。接下来,我们从Resources类加载HelpIcon属性。稍后我们将讨论Resources类。

接下来的两行设置了默认值、线条分隔符,并默认显示帮助图标。

6. 实现事件处理程序

控件类的最后一部分是实现OnPaintOnMouseDown事件的两个事件处理程序。OnPaint如下所示:

protected override void OnPaint(PaintEventArgs e)
{
    var graphics = e.Graphics;
    var bitmap = new Bitmap(ClientSize.Width,
                            ClientSize.Height);
    var graphicsOffScreen = Graphics.FromImage(bitmap);
    var solidBlackBrush = new SolidBrush(Color.Black);
    var blackPen = new Pen(Color.Black, 
        1 * graphicsOffScreen.DpiY / DESIGNPOINTSPERINCH);
    var solidBackColorBrush = new SolidBrush(BackColor);

    graphicsOffScreen.FillRectangle(solidBackColorBrush, ClientRectangle);
    var x = 0;
         
    x = Icon != null ? Convert.ToInt32((5 * graphicsOffScreen.DpiY / 
                       DESIGNPOINTSPERINCH) * 2) + Icon.Width : Convert.ToInt32(5 * 
        graphicsOffScreen.DpiY / DESIGNPOINTSPERINCH);

    graphicsOffScreen.DrawString(Title, new Font(Font.Name, 9F, FontStyle.Bold), 
                      solidBlackBrush, x, 4 * graphicsOffScreen.DpiY / 
                      DESIGNPOINTSPERINCH);

    if (Description.Length > 0)
        graphicsOffScreen.DrawString(Description, new Font(Font.Name, 8.5F, 
        FontStyle.Regular), solidBlackBrush, x, 19 * graphicsOffScreen.DpiY / 
        DESIGNPOINTSPERINCH);
         
    var cy = 0;
    var cx = 0;
    if (Icon != null)
    {
        cy = (Height - Icon.Height) / 2;
        cx = (x - Icon.Width) / 2;
        graphicsOffScreen.DrawIcon(Icon, cx, cy);
    }

    if (ShowHelpIcon && HelpIcon != null)
    {
        if (Height > HelpIcon.Height)
        {
            cy = (Height - HelpIcon.Height) / 2;
            x = Convert.ToInt32(5 * graphicsOffScreen.DpiX / DESIGNPOINTSPERINCH);
            cx = (Width - x - HelpIcon.Width);
            graphicsOffScreen.DrawIcon(HelpIcon, cx, cy);
            //Store the location of the drawn icon.
            HelpIconPoint = new Point(cx, cy);
        }
    }

    if (ShowLineSeparator)
    {
        //Draw the line at the bottom of the control.
        graphicsOffScreen.DrawLine(blackPen,
              0, Convert.ToInt32(Height - (2 * graphicsOffScreen.DpiY / 
                                           DESIGNPOINTSPERINCH) / 2), Width,
        Convert.ToInt32(Height - (2 * graphicsOffScreen.DpiY / 
        DESIGNPOINTSPERINCH) / 2));
    }

    graphics.DrawImage(bitmap, 0, 0);
    solidBlackBrush.Dispose();
    blackPen.Dispose();
    bitmap.Dispose();
    solidBackColorBrush.Dispose();
    graphicsOffScreen.Dispose();
}

上面需要注意的一点是双缓冲的使用。这是一种简单的技术,只需将内容绘制到一个屏幕外的图像(位图对象),完成后再将图像写入界面。这可以防止闪烁,通常会提高性能。

7. 实现 Resources 类

从 .NET Compact Framework 2.0 版本开始,我们拥有了强类型 resx 资源文件的优势,该文件允许轻松检索资源。在使用图标时存在一个问题,那就是同一个图标不支持多种尺寸选择。当然,你可以将每个图标剥离出来,然后单独添加到你的项目中。但是,你最好自己编写资源类。这就是我们实现一个单独的资源类而不是使用内置支持的原因。资源类如下所示:

class Resources
{ 
    private static readonly Assembly _assembly = Assembly.GetExecutingAssembly();

    internal static Icon GetIcon(string name, int width, int height)
    {
       if (string.IsNullOrEmpty(name))
           throw new ArgumentNullException("name", "Icon name cannot be null");
            
       if (width <= 0)
           throw new ArgumentException("The width of the icon must" + 
                     " be greater than 0", "width");

       if (height <= 0)
          throw new ArgumentException("The height of the icon must" + 
                    " be greater than 0", "height");

       var myIcon = string.Format("Microsoft.Windows.Mobile.UI.Resources.{0}", name);

       var stream = _assembly.GetManifestResourceStream(myIcon);
       if (stream == null)
           throw new NullReferenceException(string.Format(
                     "The Icon {0}could not be loaded, " + 
                     "check that it exists and is of the correct name.", 
                     myIcon));
            
       return new Icon(stream, width, height);
   }
}

请注意,目前它仅通过反射支持嵌入式图标资源。你可以简单地请求要从程序集中检索的图标大小,它将返回一个Icon类型的对象。注意:Resources类与PropertyClass完全没有耦合。它只是一个辅助类。如果我们继续使用完全的设计器支持(稍后我们将这样做),我们就无法使用Resources类。此外,如果我们继续使用完全设计器支持(设置图标等),我们将无法使用相同的代码库支持多分辨率设备。

8. Visual Studio 设计时支持

如果没有设计器支持,该控件就不完整了。这样我们就可以拖放图标,设置控件的外观和感觉等。尽管如此,自 Compact Framework v2.0(引入 UserControl 支持)以来,我们免费获得了设计器支持。也就是说,你可以通过对功能进行分类并添加帮助支持来清理它,为开发人员提供更好的用户体验。要添加 XMTA 文件,只需右键单击项目并选择“Design-Time Attribute File”。

PropertyHeaderWM/img8.PNG

将文件重命名为 DesignTimeAttrributes.xmta。

注意,你可以将这些属性添加到代码中,但我认为将其从代码中抽象到一个 XML 文件中是更好的做法。完整的 XML 文件如下所示:

<?xml version="1.0" encoding="utf-16"?>
<Classes xmlns="http://schemas.microsoft.com/VisualStudio/2004/03/SmartDevices/XMTA.xsd">
  <Class Name="Microsoft.Windows.Mobile.UI.PropertyHeader">
    <DesktopCompatible>true</DesktopCompatible>
    <Property Name="Description">
      <Description>Displays the description of the control.</Description>
      <Category>Appearance</Category>
      <Browsable>true</Browsable>
    </Property>
    <Property Name="Title">
      <Description>Displays the title of the control (in bold font).</Description>
      <Category>Appearance</Category>
      <Browsable>true</Browsable>      
    </Property>
    <Property Name="Icon">
      <Description>Indicates the icon for the control. The icon is displayed to the left
      of both the Title and the Description properties. 
      Ensure when running on hi-res devices you factor in
    the size of the icon.</Description>
      <Browsable>true</Browsable>
      <Category>Appearance</Category>
    </Property>
    <Property Name="ShowHelpIcon">
      <Description>Determines whether the help icon is shown. 
        If shown then event HelpRequested will be raised when clicked.</Description>
      <Category>Appearance</Category>
      <Browsable>true</Browsable>
    </Property>
    <Property Name="ShowLineSeparator">
      <Description>Determines whether a line separator 
        will be drawn at the base of the control.</Description>
      <Category>Appearance</Category>
      <Browsable>true</Browsable>
    </Property>
  </Class>
</Classes>

模式相对简单,可以在 Visual Studio 中手动添加。你有一个名为Classes的顶层节点;然后,对于每个控件,你都有一个名为Class的块,如下所示:

<Classes xmlns="http://schemas.microsoft.com/VisualStudio/2004/03/SmartDevices/XMTA.xsd">
    <Class Name="Microsoft.Windows.Mobile.UI.PropertyHeader"/>
</Classes>

如你所见,Class节点的Name属性必须包含控件的完整命名空间。在每个Class中,列出每个属性及其各自的元素。请注意,我们如何设置每个属性的Category,这是为了在 Visual Studio 中对属性进行分组,如下所示:

PropertyHeaderWM/img9.PNG

控件属性对话框

从属性窗口底部可以看到帮助文本。当然,当我们将更多控件添加到Microsoft.Windows.Mobile.UI命名空间时,我们会将相应的类添加到 XMTA 文件中。控件的类图如下所示:

PropertyHeaderWM/img10.PNG

类图

9. 针对控件进行编程

既然我们有了一个控件,我们该如何使用它呢?你可以通过右键单击工具箱并选择“Choose Items”,然后浏览二进制文件来将其添加到 Visual Studio 工具箱。选择后,单击“OK”。

PropertyHeaderWM/img11.PNG

将控件添加到 Visual Studio

请注意,此处列出了两个控件。这是因为自 VS2005 以来,Visual Studio 在 VS 中编译控件时会在工具箱中添加.obj文件。拖到窗体上哪个控件并不重要,依赖关系才重要。你可以简单地将控件拖放到窗体上,然后设置其属性。

PropertyHeaderWM/img12.PNG

设计器中的控件

请注意,我们没有使用设计器设置图标。我们这样做是为了良好的实践,以便支持多分辨率设备。但请记住,根据代码,即使我们在多分辨率设备上显示相同大小的图标,控件也会自动计算文本和图标的相对位置。因此,控件是分辨率感知的。我们可以通过以下方式演示:

PropertyHeaderWM/img13.PNG

240x400 QVGA 屏幕,配有 32x32 像素图标

在 VGA 设备 640x480 上运行相同的代码,使用相同的图标。

PropertyHeaderWM/img14.PNG

控件运行在 800x640 分辨率的模拟器上。

如你所见,在高清设备上运行效果很好。请注意帮助图标的尺寸如何变大。我们在控件内部使用以下代码行来实现这一点:

HelpIcon = CurrentAutoScaleDimensions.Height <= 96 ? 
    Resources.GetIcon("help.ico", 16, 16) : Resources.GetIcon("help.ico", 32, 32);

填充控件的代码如下所示:

var _assembly = Assembly.GetExecutingAssembly();
            
var stream = _assembly.GetManifestResourceStream(
    string.Format("Client.{0}", "globe.ico"));
propertyHeader.Icon = new Icon(stream, 32, 32);

propertyHeader.Description = "This is a description";
propertyHeader.Title = "This is a title";
propertyHeader.HelpRequested += OnHelpRequested;

private void OnHelpRequested(object sender, HelpEventArgs hlpevent)
{
    MessageBox.Show("User requested help");
}

如你所见,我们已经硬编码了图标的宽度和高度值。在实际应用程序中,你通常不会这样做,但这只是演示了如何编写分辨率感知的控件,并演示了控件在高清和低分辨率设备上的渲染,这实际上并不那么痛苦。错误处理或不针对多分辨率设备进行编码对用户来说非常痛苦。请注意,我们还实现了帮助事件处理程序。由于我们利用了 UserControl 的事件来实现此功能,所以我们免费获得了这个功能。点击控件上的帮助按钮会显示此消息框:

PropertyHeaderWM/img15.PNG

用户请求帮助

注意:你可以调用System.Windows.Forms.ContainerControl.CurrentAutoScaleDimensions.Width来获取设备的 Dpi。正如我们所提到的,控件可以通过属性进行高度定制。因此,例如,我们可以删除图标以获得更多的屏幕空间。

PropertyHeaderWM/img16.PNG

未设置图标属性的属性控件

现在,如果我们愿意,我们可以为更长的文本使用更多的屏幕空间。请注意,当没有图标存在时,文本会靠左对齐。同样,我们可以删除帮助图标——尽管这样做自然会移除帮助支持。

PropertyHeaderWM/img17.PNG

没有帮助支持和图标的控件

我们可以通过将ShowHelpIcon属性设置为 false 来实现上述效果。如果我们想为窗体的控件保留更多屏幕空间,我们可以将属性控件的大小调整为只有一行(不显示描述)。我们可以使用设计器来实现这一点。

PropertyHeaderWM/img18.PNG

一行显示的控件

你也可以在上面的一行视图中添加帮助图标。请注意,许多标准的 Windows Mobile 应用程序都使用上述视图。

PropertyHeaderWM/img19.PNG

显示帮助图标的一行视图

我们也可以关闭线条分隔符。

PropertyHeaderWM/img20.PNG

没有线条分隔符的控件

这就是完整的控件。这段代码已上传到 MSDN 代码库供你使用。也许未来的增强功能可以包含带渐变背景的 GDI+。

© . All rights reserved.