Windows Mobile 的属性头控件






4.80/5 (11投票s)
是否曾想过创建一个在许多 Microsoft 应用程序中看到的、在 Windows Mobile 上具有标准外观和感觉的属性头部控件?那么,这里就是实现它的代码。
引言
Microsoft 推荐的 Windows Mobile UI 设计文档指出,Windows Mobile 应用程序中的每个窗体都必须具有相同的名称。示例如下:
假设,Form1
的标题是 My Acme App。
点击“Do Stuff”按钮会加载另一个窗体,Form2
。
请注意,窗体的名称现在显示为“Do Stuff”。这是不好的,并且违反了 Microsoft 的 UI 设计建议,原因有很多,其中之一是:
你注意到问题了吗?我敢打赌你注意到了!我们在任务管理器(Windows Mobile 6.1 中的一个新功能)中有一个单独的引用。这真是个麻烦——尽管在 WM 6.1 上稍微宽容一些。如果用户从列表中选择 My Acme App(在这种情况下是子窗体),操作系统实际上会将顶层窗口置于 Z 顺序的顶部,在本例中是“Do Stuff”窗口——相当巧妙。在 WM 6.0 之前的版本中情况并非如此,所以请记住这一点。
那么,我们是否应该在整个应用程序中简单地命名每个窗口?当然可以,但这很简单。一种简单易行的方法是将窗口的所有者属性设置为父窗口(.NET Compact Framework 2.0 中的一项新功能)。因此,在显示Form2
的Form1
按钮点击事件处理程序中,我们这样做:
private void button1_Click(object sender, EventArgs e)
{
using (Form2 form2 = new Form2())
{
form2.Owner = this;
form2.ShowDialog();
}
}
现在,我们来看看结果:
Form1
看起来和改变之前一样,所以这里没有新内容。但是现在,如果我们点击“Do Stuff”按钮加载Form2
,我们会得到以下结果:
请注意,即使Form2
的Text
属性仍然设置为“Do Stuff”,标题现在也与Form1
相同。
此外,如果我们现在检查任务管理器,我们会得到以下结果:
如你所见,只有一个名为“My Acme App”的应用程序。这真的很棒。但是,这可能会导致对不同窗体功能感到困惑,因为现在你加载的每个窗体都没有名称。如果你查看标准的 Microsoft 应用程序,你会注意到它们有一个“头部面板”控件,通常会描述窗体的功能。
开箱即用的控件没有提供这个功能,所以本文将讨论这个。我们将讨论如何创建一个,并在文章底部提供源代码。我们将展示如何添加帮助支持,以及一个可选的图标来表示窗体。我们还将讨论设计器支持,以便可以轻松地将其从 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 事件中如何处理这个问题。如果没有图标,那么Title
和Description
属性将绘制在控件的更左侧。
如前所述,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. 实现事件处理程序
控件类的最后一部分是实现OnPaint
和OnMouseDown
事件的两个事件处理程序。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”。
注意,你可以将这些属性添加到代码中,但我认为将其从代码中抽象到一个 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 中对属性进行分组,如下所示:
从属性窗口底部可以看到帮助文本。当然,当我们将更多控件添加到Microsoft.Windows.Mobile.UI
命名空间时,我们会将相应的类添加到 XMTA 文件中。控件的类图如下所示:
9. 针对控件进行编程
既然我们有了一个控件,我们该如何使用它呢?你可以通过右键单击工具箱并选择“Choose Items”,然后浏览二进制文件来将其添加到 Visual Studio 工具箱。选择后,单击“OK”。
请注意,此处列出了两个控件。这是因为自 VS2005 以来,Visual Studio 在 VS 中编译控件时会在工具箱中添加.obj文件。拖到窗体上哪个控件并不重要,依赖关系才重要。你可以简单地将控件拖放到窗体上,然后设置其属性。
请注意,我们没有使用设计器设置图标。我们这样做是为了良好的实践,以便支持多分辨率设备。但请记住,根据代码,即使我们在多分辨率设备上显示相同大小的图标,控件也会自动计算文本和图标的相对位置。因此,控件是分辨率感知的。我们可以通过以下方式演示:
在 VGA 设备 640x480 上运行相同的代码,使用相同的图标。
如你所见,在高清设备上运行效果很好。请注意帮助图标的尺寸如何变大。我们在控件内部使用以下代码行来实现这一点:
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 的事件来实现此功能,所以我们免费获得了这个功能。点击控件上的帮助按钮会显示此消息框:
注意:你可以调用System.Windows.Forms.ContainerControl.CurrentAutoScaleDimensions.Width
来获取设备的 Dpi。正如我们所提到的,控件可以通过属性进行高度定制。因此,例如,我们可以删除图标以获得更多的屏幕空间。
现在,如果我们愿意,我们可以为更长的文本使用更多的屏幕空间。请注意,当没有图标存在时,文本会靠左对齐。同样,我们可以删除帮助图标——尽管这样做自然会移除帮助支持。
我们可以通过将ShowHelpIcon
属性设置为 false
来实现上述效果。如果我们想为窗体的控件保留更多屏幕空间,我们可以将属性控件的大小调整为只有一行(不显示描述)。我们可以使用设计器来实现这一点。
你也可以在上面的一行视图中添加帮助图标。请注意,许多标准的 Windows Mobile 应用程序都使用上述视图。
我们也可以关闭线条分隔符。
这就是完整的控件。这段代码已上传到 MSDN 代码库供你使用。也许未来的增强功能可以包含带渐变背景的 GDI+。