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

适用于 Windows Mobile 的基于 TreeView 的选项组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2009年9月21日

CPOL

9分钟阅读

viewsIcon

21855

这是一个适用于 Windows Mobile 的基于 TreeView 的选项组件

问题陈述

许多 Windows Mobile 应用程序使用“选项”或“设置”对话框来管理应用程序级别的设置。该对话框通常包含一个选项卡控件,每个选项卡上都有一个或多个常规控件,例如标签、复选框、单选按钮、下拉列表、微调器、文本框等。以下屏幕截图显示了一些示例。

设计此类对话框时,我们需要仔细考虑要使用哪种类型的控件,每个选项卡上有多少控件,以及如何布局这些控件,使其在不同的屏幕分辨率、尺寸和方向(纵向和横向)下都能美观且易于操作。如果需要弹出软键盘来收集用户输入,布局问题将变得更加棘手。处理布局问题的一种方法是设计成适应最低公分母——最低分辨率和最小屏幕尺寸。但很多时候,我们会发现不得不使用更多的选项卡和更少的控件,这会浪费宝贵的屏幕空间,尤其是当用户恰好拥有更高分辨率和/或更大屏幕尺寸的设备时。以下来自 480x800 WVGA 设备的屏幕截图说明了这一点。

由“选项”对话框管理的应用程序设置需要持久化到数据存储中。在完整的 .NET Framework 中,这通常通过“设置”文件完成,它对应于 `app.config` 文件中的 `<userSettings>` 部分。在设计时,`Settings` 文件用于生成一个 `Settings` 类(继承自 `System.Configuration.ApplicationSettingsBase`),该类为每个设置提供了强类型访问器,并能够将设置保存到用户特定的文件 (`user.config`) 中。不幸的是,`.NET Compact Framework` 中不存在 `Settings` 文件机制。我们必须将设置存储在注册表中,这容易出现部署和安全问题,或者自行设计存储设置的方法,就像 pbrooks 在 Compact Framework 的 AppSettings 实现以及 Shawn Miller 在 .NET Compact Framework ConfigurationManager 中所做的那样。两者都使用了类似于 `app.config` 文件 `<appSettings>` 部分的 XML 文件。他们将应用程序设置视为“平面”的键值对,没有关于设置如何组织结构的任何支持。

基于 TreeView 的选项组件

在本文中,我建议使用 `TreeView` 控件结合 XML 文件来管理应用程序设置。以下屏幕截图展示了该组件的实际运行效果。

从 UI 的角度来看,基于 `TreeView` 的选项组件具有以下优点:

  • 不再有布局方面的烦恼。无论我们有多少选项,使用何种屏幕分辨率、尺寸和方向,它都能很好地工作。
  • 选项可以组织成组、子组等。
  • 直观且便于游戏杆操作(用户可以用一只手操作)。
    • 上下移动游戏杆以上下移动选择光标(灰色)。
    • 向右移动游戏杆以展开节点。
    • 向左移动游戏杆以折叠节点。
    • 当当前节点是值节点时,按下游戏杆以选择它。
    • 当当前节点是折叠的组或选项节点时,按下游戏杆以展开它。
    • 当当前节点是展开的组或选项节点时,按下游戏杆以折叠它。
  • 选定的值会被高亮显示(黄色)。
  • 高效利用屏幕空间。无需翻阅不同的选项卡或屏幕即可找到正确的选项。
  • 所有选项都采用一致的多项选择式操作。用户无需学习不同且有时令人困惑的用户界面。

使用该组件的唯一要求是,每个选项或设置必须有一个从一组离散值中选择的值,或者类似于多项选择。这并不算太受限制。UI 控件,如单选按钮、下拉列表、微调器和复选框,实际上是多项选择类型的控件。文本框,通常用于连续值选项,如距离、重量、缓存大小等,应不惜一切代价避免使用,因为在移动设备上输入文本框非常麻烦。因此,在处理连续值选项时,我们应该想办法将其离散化。在我们的移动应用程序的特定上下文中,它是否可以用一组代表性值来合理地描述?例如,是否有可能将其分解为几个范围?所有选项离散化的一个额外好处是:我们不再需要输入验证,因为所有值都经过仔细选择且有效。

用于填充 `TreeView` 的数据存储在 XML 文件中。特定的 XML 文件除了根节点外,只有三种节点类型:`group`、`option` 和 `value`。下面的示例展示了一个 XML 文件:

<?xml version="1.0" encoding="utf-8" ?>
<options>
  <group name="General">
    <option name="TimeZone" displayName="Time zone">
      <value name="EST" selected="true" />
      <value name="CST" selected="false" />
      <value name="MST" selected="false" />
      <value name="PST" selected="false" />
    </option>
    <option name="UpdateInterval" displayName="Update interval">
      <value name="10" displayName="10 sec" selected="false" />
      <value name="30" displayName="30 sec" selected="true" />
      <value name="60" displayName="1 min" selected="false" />
      <value name="300" displayName="5 min" selected="false" />
      <value name="600" displayName="10 min" selected="false" />
    </option>
    <option name="CacheSize" displayName="Cache size">
      <value name="32" displayName="32 MB" selected="true" />
      <value name="64" displayName="64 MB" selected="false" />
      <value name="128" displayName="128 MB" selected="false" />
    </option>
    <option name="CheckInterval" displayName="Check for app update">
      <value name="0" displayName="Every time app starts" selected="true" />
      <value name="1" displayName="Every day" selected="false" />
      <value name="7" displayName="Every week" selected="false" />
      <value name="30" displayName="Every month" selected="false" />
      <value name="365" displayName="Every year" selected="false" />
    </option>
  </group>
  <group name="Appearance">
    <option name="Skin">
      <value name="Classic" selected="true" />
      <value name="IceFusion" displayName="Ice Fusion" selected="false" />
      <value name="Monochrome" selected="false" />
    </option>
    <option name="ShowToolbar" displayName="Show toolbar">
      <value name="true" selected="true" />
      <value name="false" selected="false" />
    </option>
    <option name="ShowStatusBar" displayName="Show status bar">
      <value name="true" selected="true" />
      <value name="false" selected="false" />
    </option>
  </group>
  <group name="SecurityPrivacy" displayName="Security & Privacy">
    <option name="EnablePassword" displayName="Enable password protection">
      <value name="true" selected="true" />
      <value name="false" selected="false" />
    </option>
    <group name="SharedContents" displayName="Shared contents">
      <option name="ContactInfo" displayName="Contact info">
        <value name="true" selected="false" />
        <value name="false" selected="true" />
      </option>
      <option name="Photos">
        <value name="true" selected="true" />
        <value name="false" selected="false" />
      </option>
      <option name="Posts">
        <value name="true" selected="true" />
        <value name="false" selected="false" />
      </option>
    </group>  

  </group>
</options>

关于此 XML 有一些规则:

  • 每种节点类型都有一个 `name` 和一个 `displayName` 属性。`name` 是必需的,用于或引用在我们的程序中。`displayName` 是可选的,用于在 `TreeView` 中显示。如果缺少 `displayName`,则使用相应的 `name` 进行显示。
  • `group` 可以包含零个或多个 `option` 节点和 `group` 节点(或子组)。我们可以任意嵌套 `group` 节点级别。
  • `option` 必须只包含一个或多个 `value` 节点。
  • 多个 `value` 节点中只有一个可以具有 `selected` 属性为 "`true`",表示它是该 `option` 的当前 `value`。

从编程的角度来看,使用这样的 XML 作为我们选项组件的数据存储具有以下优点:

  • XML 和 `TreeView` 都基于树形结构。一个用于存储,一个用于显示。这种完美的匹配使编程变得容易。
  • 按组和子组组织选项是内在支持的。
  • 它使得管理选定值和所有可用值变得容易。使用传统的设置文件,只有选定值会被持久化,而可用值要么在单独的资源文件中,要么在程序中硬编码。
  • 在某种程度上,我们可以更改选项而无需重新编译我们的程序。也可以远程更新 XML。
  • 它简化了本地化。只需将所有 `displayName` 属性翻译成不同的语言。

实现

为了最大的灵活性,我没有将选项“组件”制作成用户控件或类库。它只是一个简单的类,您可以将其复制并粘贴到您的项目中。

public class OptionsManager
{
    private const string optionValueSeparator = ": ";
    private static XmlDocument xdoc = null;
    private static string theFile = null;
    private static Color selectedValueBackColor = Color.Yellow;
    private static bool isChanged = false; //is there any difference between xdoc and theFile 
    static OptionsManager()
    {
        theFile = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().
            GetName().CodeBase) + @"\Options.xml";
        xdoc = new XmlDocument();
        if (File.Exists(theFile))
            xdoc.Load(theFile);
        else
        {
            LoadDefault();
            Save();         //create theFile
        }
    } 

    //load default options from embeded resource
    public static void LoadDefault()
    {
        using (StreamReader sr =new StreamReader(Assembly.GetExecutingAssembly().
            GetManifestResourceStream("TreeviewOptions.Options.Options.xml")))
        {
            xdoc.LoadXml(sr.ReadToEnd());
            sr.Close();
            isChanged = true;
        }
    } 

    //Persist xdoc to theFile
    public static void Save()
    {
        if (isChanged)
        {
            xdoc.Save(theFile);
            isChanged = false;
        }
    } 

    //Cancel changes to xdoc (DOM) by reloading it from theFile  

    public static void Cancel()
    {
        if (isChanged)
        {
            xdoc.Load(theFile);
            isChanged = false;
        }
    } 

    //Load xdoc to the specified TreeView
    public static void LoadToTreeView(TreeView tvw)
    {
        tvw.Nodes.Clear();
        XmlNode root = xdoc.DocumentElement;
        DoLoading(tvw, root);
    } 

    //treeviewNode can be a TreeView or a TreeNode object
    private static void DoLoading(object treeviewNode, XmlNode xmlNode)
    {
        XmlNodeList xmlSubnodes = xmlNode.ChildNodes;
        foreach (XmlNode xsn in xmlSubnodes)
        {
            NodeType nodeType = GetXmlNodeType(xsn);
            if (nodeType == NodeType.Group)
            {
                string groupDisplayName = GetXmlNodeDisplayName(xsn);
                TreeNode tn = null;
                if (treeviewNode is TreeView)
                {
                    tn = ((TreeView)treeviewNode).Nodes.Add(groupDisplayName);
                    tn.Tag = string.Format("/group[@name='{0}']", 

                        ((XmlElement)xsn).GetAttribute("name"));
                }
                else
                {
                    tn = ((TreeNode)treeviewNode).Nodes.Add(groupDisplayName);
                    tn.Tag = tn.Parent.Tag + string.Format("/group[@name='{0}']", 

                        ((XmlElement)xsn).GetAttribute("name"));
                }
                DoLoading(tn, xsn);
            }
            else if (nodeType == NodeType.Option)
            {
                string optionDisplayName = GetXmlNodeDisplayName(xsn);
                TreeNode tn = null;
                if (treeviewNode is TreeView)
                {
                    tn = ((TreeView)treeviewNode).Nodes.Add(optionDisplayName);
                    tn.Tag = string.Format("/option[@name='{0}']", 

                        ((XmlElement)xsn).GetAttribute("name"));
                }
                else
                {
                    tn = ((TreeNode)treeviewNode).Nodes.Add(optionDisplayName);
                    tn.Tag = tn.Parent.Tag + string.Format("/option[@name='{0}']", 

                        ((XmlElement)xsn).GetAttribute("name"));
                }
                XmlNodeList values = xsn.ChildNodes;
                string selectedValueName = null;
                foreach (XmlNode v in values)
                {
                    string valueDisplayName = GetXmlNodeDisplayName(v);
                    TreeNode vtn = tn.Nodes.Add(valueDisplayName);
                    vtn.Tag = tn.Tag + string.Format("/value[@name='{0}']", 

                        ((XmlElement)v).GetAttribute("name"));
                    if (((XmlElement)v).GetAttribute("selected") == "true")
                    {
                        vtn.BackColor = selectedValueBackColor;
                        selectedValueName = valueDisplayName;
                    }
                }
                tn.Text += optionValueSeparator + selectedValueName;
            }
        }
    } 

    private static string GetXmlNodeDisplayName(XmlNode node)
    {
        string dName = ((XmlElement)node).GetAttribute("displayName");
        if (string.IsNullOrEmpty(dName))
            dName = ((XmlElement)node).GetAttribute("name");
        return dName;
    } 

    //if tn is a value node, update tn.Parent display and corresponding xml value node selection
    public static void ChangeValue(TreeNode tn)
    {
        if (IsValueNode(tn))
        {
            //update tn.Parent display
            string parentDisplayName = Regex.Split(tn.Parent.Text, optionValueSeparator)[0];
            tn.Parent.Text = parentDisplayName + optionValueSeparator + tn.Text;
            tn.BackColor = selectedValueBackColor;
            TreeNodeCollection valueNodes = tn.Parent.Nodes;
            foreach (TreeNode vn in valueNodes)
            {
                vn.BackColor = (vn == tn) ? selectedValueBackColor : Color.Empty;
            } 


            //update xml value node selection
            XmlNode option = xdoc.DocumentElement.
                SelectSingleNode("/options" + tn.Tag).ParentNode;
            XmlNodeList values = option.ChildNodes;
            foreach (XmlNode v in values)
                ((XmlElement)v).SetAttribute("selected", "false");
            ((XmlElement)xdoc.DocumentElement.SelectSingleNode("/options" + tn.Tag)).
                SetAttribute("selected", "true"); 


            isChanged = true;
        }
    } 

    //If the XPath stored in tn.Tag ends with a value tag, then tn is a value treenode 

    public static bool IsValueNode(TreeNode tn)
    {
        string path = (string)tn.Tag;
        string[] parts = path.Split("/".ToCharArray());
        return (parts[parts.Length - 1].StartsWith("value"));
    } 


    //optionXPath is the XPath to the option whose value (name attribute) 
    //is to be retrieve//The root /options can be omitted. 
    public static string GetOptionValue(string optionXPath)
    {
        if (!optionXPath.StartsWith("/options"))
            optionXPath = "/options" + optionXPath;
        XmlNode option = xdoc.DocumentElement.SelectSingleNode(optionXPath);
        if (option != null)
        {
            XmlNodeList values = option.ChildNodes;
            foreach (XmlNode v in values)
            {
                if (((XmlElement)v).GetAttribute("selected") == "true")
                    return ((XmlElement)v).GetAttribute("name");
            }
        }
        return null;
    } 

    private static NodeType GetXmlNodeType(XmlNode node)
    {
        switch (node.Name)
        {
            case "options":
                return NodeType.Root;
            case "group":
                return NodeType.Group;
            case "option":
                return NodeType.Option;
            case "value":
                return NodeType.Value;
            default:
                throw new ApplicationException("Unknow Xml node type.");
        }
    } 

    private enum NodeType
    {
        Root,
        Group,
        Option,
        Value
    }
}

关于代码的一些亮点:

首先,我们需要将 XML 文件 `Options.xml` 设为嵌入式资源,方法是在 Visual Studio 中将其“生成操作”属性设置为“嵌入式资源”。正如我们的 `OptionsManager` 类的 `static` 构造函数中所示,在应用程序首次启动时,XML 文件会通过 `LoadDefault` 方法从嵌入式资源加载到内存,并保存到我们的应用程序可执行文件所在的位置。在后续启动时,由于 XML 文件已存在于我们的应用程序根目录,我们只需从中加载。

`LoadToTreeView` 方法将 XML 文档加载到指定的 `TreeView` 控件中。由于我们可以有多层嵌套的 `group` 节点,因此我们需要递归加载,如 `private` 方法 `DoLoading` 所示。对于 `group` 节点,我们在 `TreeView` 中显示其 `displayName`。对于 `option` 节点,我们显示其 `displayName` 和当前选定值的 `displayName`(用“:”分隔)。对于 `value` 节点,我们显示其 `displayName`,如果它当前被选中,我们还会将其背景色设置为 `selectedValueBackColor`。对于每个树节点,我们构建一个指向相应 XML 节点的 `XPath`,并将其存储在树节点的 `Tag` 属性中。之后,我们可以通过这个 `XPath` 轻松地将树节点与 XML 节点匹配。例如,下表显示了某个树节点的 `XPath` 外观:

树节点 XPath
General/Time zone option /group[@name='General']/option[@name='TimeZone']
General/Time zone/PST value /group[@name='General']/option[@name='TimeZone']/value[@name='PST']

当我们想更改选项的选定值时,会调用 `ChangeValue` 方法。这通常由按下游戏杆的事件触发。在此方法中,我们需要做两件事。首先,在 `TreeView` 控件中,我们需要更新相应的 `option` 节点的显示,并将高亮标记更改为当前选定的 `value` 节点。其次,在底层 XML 文档中,我们需要更新相应 `option` 的 `value` 节点的 `selected` 属性,以反映当前选定的是哪个值。

给定选项的 `XPath`,`GetOptionValue` 方法会返回该选项的选定值(确切地说是值的 `name` 属性)。由于我们了解 `Options.xml` 文件的详细信息,因此由我们负责将检索到的 `string` 值转换为适当的数据类型。

使用 OptionsManager 类

以下是在 Windows Mobile 项目中使用我们的 `OptionsManager` 类的步骤:

  1. 创建 `Options.xml` 文件并将其设为嵌入式资源。
    提示:使用示例 `Options.xml` 文件作为模板。确保 `LoadDefault` 方法中对 `Options.xml` 文件的命名空间引用正确。
  2. 将一个 Windows Form `OptionsForm` 添加到项目中。这将是我们的选项对话框。
  3. 向 `OptionsForm` 添加一个 `TreeView` 控件和以下四个菜单项或按钮:
    • 完成 - 保存对选项的更改并关闭选项对话框
    • 取消 - 取消对选项的更改并关闭选项对话框
    • 默认 - 将选项恢复到其默认值(如嵌入式资源中存储的 `Options.xml` 文件中所指定)
    • 更改值 - 将相应选项的值更改为 `TreeView` 选择光标指示的值
  4. 像这样为 `OptionsForm` 的 `Load` 和 `Closed` 事件处理程序接线:
    private void OptionsForm_Load(object sender, EventArgs e)
    {
        LoadTreeView();
    } 
    
    private void OptionsForm_Closed(object sender, EventArgs e)
    {
        if (this.DialogResult == DialogResult.OK)
            OptionsManager.Save();
        else
            OptionsManager.Cancel();
    } 
    
    private void LoadTreeView()
    {
        OptionsManager.LoadToTreeView(treeView1); 
    
        //expand the first level
        TreeNodeCollection nodes = treeView1.Nodes;
        foreach (TreeNode n in nodes)
            n.Expand();
    }
  5. 像这样为 `TreeView` 控件的 `KeyPress` 和 `AfterSelect` 事件处理程序接线:
    private void treeView1_KeyPress(object sender, KeyPressEventArgs e)
    {
        if (OptionsManager.IsValueNode(treeView1.SelectedNode))
        {
            OptionsManager.ChangeValue(treeView1.SelectedNode);
        }
        else
        {
            if (treeView1.SelectedNode.IsExpanded)
                treeView1.SelectedNode.Collapse();
            else
                treeView1.SelectedNode.Expand();
        }
    } 
    
    private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
    {
        //make the Change Value menu item available only when the current tree node is a value node
        menuMenuChangeValue.Enabled = OptionsManager.IsValueNode(treeView1.SelectedNode);
    }
  6. 像这样为四个菜单项或按钮的 `Click` 事件处理程序接线:
    private void menuDone_Click(object sender, EventArgs e)
    {
        this.DialogResult = DialogResult.OK;   
    
    }
    
    private void menuMenuCancel_Click(object sender, EventArgs e)
    {
        this.DialogResult = DialogResult.Cancel;
    }
    
    private void menuMenuDefault_Click(object sender, EventArgs e)
    {
        OptionsManager.LoadDefault();
        LoadTreeView();
    }
    
    private void menuMenuChangeValue_Click(object sender, EventArgs e)
    {
        OptionsManager.ChangeValue(treeView1.SelectedNode);
    }
  7. 在项目的任何地方调用 `GetOptionValue` 方法来获取任何选项的值。例如:
    int cacheSize = int.Parse(OptionsManager.GetOptionValue(
        "/group[@name='General']/option[@name='CacheSize']"));
    bool sharePhotos = bool.Parse(OptionsManager.GetOptionValue(
        "/group[@name='SecurityPrivacy']/group[@name='SharedContents']/option[@name='Photos']"));

进一步改进

“选项”组件可以在以下方面进一步改进:

  • 使用 XSD 架构验证 `Options.xml`。
  • 通过 `ImageList` 控件为树节点添加图标。
  • 允许同一级别的组或选项节点一次只展开一个。对于小屏幕尺寸,这种模式是可取的。

结论

提出并实现了一个基于 `TreeView` 的 Windows Mobile 选项组件。与传统的选项对话框设计方式相比,它具有几个优点。这些优点包括消除了布局问题,高效利用屏幕空间,直观一致的用户界面,以及在持久化和组织应用程序设置方面良好的结构支持。

您可以在 此处下载源代码。

© . All rights reserved.