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

MyXAML--XAML风格GUI生成器(已添加样式)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (69投票s)

2004年2月13日

CPOL

11分钟阅读

viewsIcon

423091

downloadIcon

960

在运行时从XML定义生成控件,设置属性值,并连接事件处理程序。

最新消息

名称现已正式确定:MyXaml!!!

MyXaml现在支持样式!(请参阅修订历史)。

有兴趣参与此技术开发吗?加入开源项目!此项目不旨在模拟Longhorn的标记语言或MSAvalon命名空间。相反,MyXAML支持命名空间而不使用DOM主干,而是直接使用命名空间类中定义的属性设置器。

目录

如果您对本文进行评分,请说明您的评分原因!

修订历史

3/10/04

  • 样式表
  • 改进了标记语法——消除了对FormBase属性的需求
  • 现在可以在根级别实例化声明命名空间中的任何类,而不仅仅是System.Windows.Forms类。

样式规范的示例是

<xgg:Style def:Name='XP'>
  <xgg:StyleProperties>
    <xgg:PropertyStyle FlatStyle='System'/>
  </xgg:StyleProperties>
</xgg:Style>

<xgg:Style def:Name='ButtonStyle' BasedOn='{XP}'>
  <xgg:StyleProperties>
    <xgg:PropertyStyle Size='80, 25'/>
  </xgg:StyleProperties>
</xgg:Style>

用法也非常简单

<Button def:Name='OKButton'
        Style='{ButtonStyle}'
        Location='225, 10'
        Visible='true'
        Click='OnOK'
        Text='&OK'/>

关于样式的几点说明:派生样式优先于“BasedOn”样式中定义的属性。由于属性是从左到右解析的,因此具有Style属性的对象可以通过在Style属性的右侧指定属性值来覆盖该属性值。

重构您之前的标记

要回顾对标记本身所做的更改,请参阅本文的正文。基本上,旧的表单是

<Canvas Name="Foo" FormBase="Form"..></Canvas>

新的标记结构是

<Form Name="Foo"...></Form>

更多信息可在http://www.myxaml.com/网站上找到——点击教程。

2004年2月25日——许多新功能

  • 支持菜单
  • 通过XML命名空间支持类命名空间
  • 支持在列表控件中作为属性使用的DataSource属性
  • 支持用于XML定义的列表的DataSource元素
  • 支持DataBinding
  • 内联代码支持
  • 后台代码支持
  • 改进的属性设置器
  • XML语法改进,更像XAML
  • 实例化运行时类的构造函数参数
  • 以及更多功能

2004年2月16日——增加了对作为属性定义的属性以及窗体事件的支持

2004年2月13日——初始发布

根据Leppie的建议,我修改了窗体生成器,以处理XML属性作为指定属性值的一种方式。然而,属性模型架构对于更复杂的属性类型仍然是必需的。

这无疑使XML的可读性大大提高!目前,两种模式都得到支持(使用属性与使用节点),因此您可以根据需要混合搭配,但这将来可能会改变。

引言

这是对文章的完全重写。我将不再深入讲解代码示例,而是通过说明来展示该库的各项功能。

XML

首先,让我们从最有趣的部分开始,也就是在XML中指定窗体。我将使用上面的截图来演示生成器的各种功能。在本节之后,我将提供一些架构图来说明不同的使用模式。

总体架构

一个重要的考虑因素是,XML中的节点直接映射到当前上下文实例的属性。例如,当一个窗体使用“Form”作为其基础时,上下文实例是System.Windows.Forms.Form,您可以设置Form类提供的任何属性,包括属性集合,如Menu和Controls,只要它们可以通过TypeConverter或属性设置器模型得到支持。唯一的例外是DataSource元素,其中的子节点Item并不与任何特定内容相关联,因为DataSource属性接受Object类型。

另一个重要的考虑因素是,您可以在任何时候指定Canvas作为属性,并为该特定控件定义一个新类型,而不是内联控件的属性和子项。我们稍后将看到这方面的例子。这意味着,对于特定控件,您可以定义后台代码实例,因为所有根节点都允许后台代码实例。

第三方组件、命名空间和XML头

首先,让我们检查XML头

<?xml version="1.0" encoding="utf-8"?>

<UI xmlns="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, 
Culture=neutral, PublicKeyToken=b77a5c561934e089"

xmlns:ka="OutlookBar, OutlookBarAssembly, Version=1.0.1.2, Culture=neutral, 
PublicKeyToken=null"

xmlns:def="Definition">

<Form Name='PropertyTest' Location='10, 10' Size='600, 330' 
      Visible='false' BackColor='LightSteelBlue' Closing='OnCloseEvent'
      Load='PropertyTestLoaded' 
Text='Simple Test Form'>
请注意使用命名空间为程序集提供完全限定的名称。这允许您在XML定义中使用第三方组件。我选择直接在命名空间中编写程序集信息,而不是使用Microsoft的XAML命名空间。这些信息必须存储在某个地方,而这也不是XAML。

定义窗体、属性和事件

在XAML中,根节点通常是一个窗体,您可以在其中指定子对象的坐标,所以我选择了相同的名称。窗体指定如下:

<Form Name='PropertyTest' Location='10, 10' Size='600, 330' 
Visible='false' BackColor='LightSteelBlue' Closing='OnCloseEvent' Load='OnLoad' 
Text='Simple Test Form'>
在这里,正在设置各种属性,并且“Load”事件被连接到一个事件处理程序。事件处理程序是XmlGuiGenerator提供的目标实例中可用的方法名称。例如:
public class Demo
{
  Form form;
  public Demo()
  {
    form=(Form)Generator.LoadForm("propertyTest.xml", "PropertyTest", 
          this, null);
    form.ShowDialog();
  }

  public void OnLoad(object sender, EventArgs e)
  {
    MessageBox.Show("OnLoad", sender.ToString());
  }
}

System.Web.UI命名空间类似,XmlGuiGenerator将为*任何*实例化的控件触发“Load”事件,前提是该事件已定义。

请注意,FormBase是“Form”。FormBase实际上可以是任何东西,我们将在稍后讨论。根节点是特殊的,因为它还可以具有在运行时编译和实例化的后台代码或内联代码实例。稍后将讨论其中几个示例。

定义菜单结构

如果根节点或子控件支持Menu属性,您可以定义一个菜单

<Menu>
  <MenuItems>
    <MenuItem Text='&File' Popup='OnPopup'>
    <MenuItems>
      <MenuItem Text='New'/>
      <MenuItem Text='-'/>
      <MenuItem Text='E&xit' Click='ExitApp'/>
    </MenuItems>
  </MenuItem>
  <MenuItem Text='&Edit'/>
  <MenuItem Text='&View'/>
  </MenuItems>
</Menu>

控件

控件被添加到Controls集合中,例如
<Controls>
  <Button Location='225, 100' Size='80, 25' Visible='true'
          BackColor='Control'
          Click='OnMyEvent'>Click Event!</Button>
  <Label Location='225, 50' Size='120, 40' Visible='true'
         Font='French Script MT, 22pt, style=Bold'>Font Test!</Label>
  <Label Location='10, 12' Size='50, 15' Visible='true'>Edit 1:</Label>
  <Label Location='10, 37' Size='50, 15' Visible='true'>Edit 2:</Label>
  <TextBox Location='60, 10' Size='100, 20' Visible='true'/>
  <TextBox Location='60, 35' Size='100, 20' Visible='true' Enabled='false'>
           Disabled</TextBox>
  <Panel BorderStyle='Fixed3D' Top='95' Left='20' Size='200, 178'
         Visible='true' BackgroundImage='jojo.jpg' Anchor='Bottom, Left'/>
  <GroupBox Location='225, 130' Size='120, 60' Visible='true'
            Text='First Group Box' Font='MS Sans Serif, 8pt, style=Bold'>
    <Controls>
      <RadioButton Location='10, 15' Size='70, 15' Visible='true'
            Font='MS Sans Serif, 8pt' Load='InitialRadioButtonState'>
            Like</RadioButton>
      <RadioButton Location='10, 30' Size='70, 15' Visible='true'
            Font='MS Sans Serif, 8pt'>Don't Like</RadioButton>
    </Controls>
  </GroupBox>
  ...

这是一个内联控件的好例子,其中分组框指定了它包含的子控件。

画布控件

演示中的第二个分组框是画布化的,这意味着它在一个单独的根节点中定义。在父窗体XML中,它声明为

<GroupBox Canvas="GroupBox2"/>

单独的画布定义了分组框的属性和集合

<GroupBox Name='GroupBox2' Location='225, 200' Size='120, 60' Visible='true'
          Text='Second Group Box' Font='MS Sans Serif, 8pt, style=Bold'>
  <Controls>
    <RadioButton Location='10, 15' Size='70, 15' Visible='true'
        Font='MS Sans Serif, 8pt' Load='InitialRadioButtonState'>
        To Click</RadioButton>
    <RadioButton Location='10, 30' Size='100, 15' Visible='true'
        Font='MS Sans Serif, 8pt'>Or Not To Click</RadioButton>
  </Controls>
</GroupBox>

请注意,事件由父窗体声明的目标上下文处理,在本例中是传递给生成器的类实例,如上所述。(稍后将详细介绍事件上下文)。

Tab Control

类似地,选项卡页面可以内联或画布化

<TabControl Location='350, 10' Size='240, 250' Visible='true'>
  <TabPages Canvas='TabPage1'/>
  <TabPages Canvas='TabPage2'/>
  <TabPages Name='TabPage3' Text='ListView' Load='PopulateListView'>
    <Controls>
      <ListView Location='5, 10' Size='222, 210' HeaderStyle='Nonclickable'
        FullRowSelect='true' GridLines='true' View='Details'>
        <Columns Width='150' TextAlign='Left'>State</Columns>
        <Columns Width='50' TextAlign='Center'>Abbr.</Columns>
      </ListView> 
     </Controls>
  </TabPages>
  <TabPages Canvas='TabPage4'/>
</TabControl>

(实际上,生成器允许您指定画布以及内联集合,但该功能不应依赖)。

第三方控件

还记得XML头中的“ka”命名空间吗?这是我的Outlook Bar控件在XML中的实现方式

<TabPage Name='TabPage1' Text='Custom Control'>
...
  <Controls>
    <ka:OutlookBar Location='10, 10' Size='150, 210' Visible='true'
        BorderStyle='FixedSingle' Load='MyForm.OnLoad'/>
  </Controls>
</TabPage>

请注意,OutlookBar是用“ka”命名空间前缀声明的。这告诉生成器该控件的类名称来自XML头中声明的命名空间。使用命名空间,可以获得完全限定的名称并实例化类。

现在,您可能会问,那个“...”之间是什么,以及“MyForm.OnLoad”是什么?请继续阅读。

内联代码

选项卡页面说明了实现后台代码的不同方式。第一种是内联。TabPage1的完整画布定义如下:

<TabPage Name='TabPage1' Text='Custom Control'>
  <def:Code language='C#'>
    <reference assembly="System.Drawing.dll"/>
    <reference assembly="System.Windows.Forms.dll"/>
    <reference assembly="OutlookBarAssembly.dll"/>
    <![CDATA[
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;

using OutlookBar;

class MyForm
{
  private OutlookBar.OutlookBar outlookBar;

  // controls in the tab page do not exist yet when the code-behind is 
  // instantiated!
  public MyForm() {}

  public void OnLoad(object sender, EventArgs e)
  {
    outlookBar=(OutlookBar.OutlookBar)sender;
    IconPanel iconPanel1=new IconPanel();
    IconPanel iconPanel2=new IconPanel();
    IconPanel iconPanel3=new IconPanel();
    outlookBar.AddBand("Outlook Shortcuts", iconPanel1);
    outlookBar.AddBand("My Shortcuts", iconPanel2);
    outlookBar.AddBand("Other Shortcuts", iconPanel3);

    iconPanel1.AddIcon("Outlook Today", Image.FromFile("img1.ico"),
       new EventHandler(PanelEvent));
    iconPanel1.AddIcon("Calendar", Image.FromFile("img2.ico"),
       new EventHandler(PanelEvent));
    iconPanel1.AddIcon("Contacts", Image.FromFile("img3.ico"),
       new EventHandler(PanelEvent));
    iconPanel1.AddIcon("Tasks", Image.FromFile("img4.ico"),
       new EventHandler(PanelEvent));
    outlookBar.SelectBand(0);
  }

  public void PanelEvent(object sender, EventArgs e)
  {
    Control ctrl=(Control)sender;
    PanelIcon panelIcon=ctrl.Tag as PanelIcon;
    MessageBox.Show("#"+panelIcon.Index.ToString(), "Panel Event");
  }
}
]]>
    </def:Code>
    <Controls>
    <ka:OutlookBar Location='10, 10' Size='150, 210' Visible='true'
        BorderStyle='FixedSingle' Load='MyForm.OnLoad'/>
  </Controls>
</TabPage>

定义引用

任何内联和后台代码都必须声明其引用的程序集,以便JIT编译器能够正确生成程序集。

将事件连接到事件处理程序

生成器允许您在后台代码中定义任意数量的类。它管理每个类的实例,并且由于每个类通常只有一个实例(至少它知道的),您可以通过在方法名称前加上类名称来指定将处理事件的实例,格式为<className>.<methodName>,如上面“Load='MyForm.OnLoad'”所示。

如果省略类名,事件将改为连接到创建窗体时传递给生成器的事件上下文。

请注意,源语言也在XML中定义,这允许您为后台代码源混合使用不同的语言!

后台代码

如果您更喜欢使用.cs或.vb文件并利用语法检查和自动帮助,您可以指定后台代码位于文件中而不是内联。第二个选项卡页面对此进行了演示

<TabPage Name='TabPage2' FormBase='TabPage' Text='ListBox'>
  <def:CodeBehind Src="InitDataSource.cs">
  <reference assembly="System.Windows.Forms.dll"/>
  </def:CodeBehind>
  <Controls>
    <ListBox Name='States' Location='10, 10' Size='130, 210'
       DataSource='ListBoxDS.DataSource' DisplayMember='LongName'
       ValueMember='ShortName'
       SelectedValueChanged='ListBoxDS.ShowValue'/>
    <Label Location='150, 10' Size='60, 15' Visible='true'>By event:</Label>
    <TextBox Name='ByEvent' Location='150, 25' Size='75, 20' Visible='true'
       ReadOnly='true'/>
    <Label Location='150, 55' Size='60, 15' Visible='true'>
           By binding:</Label>
    <TextBox Location='150, 70' Size='75, 20' Visible='true' ReadOnly='true'>
      <DataBindings>Text, ListBoxDS.DataSource, ShortName</DataBindings>
    </TextBox> 
  </Controls>
</TabPage>

这也说明了数据绑定。请继续。

DataBinding

支持DataBinding属性的控件可以在XML中设置该属性。格式很简单:

<DataBindings>Text, ListBoxDS.DataSource, ShortName</DataBindings>

您指定要绑定到的字段、数据源以及数据源中返回数据的属性名称。请注意,数据源必须从代码后台或传递给生成器的实例的上下文属性中获取。检查构成代码后台的C#代码,ListBoxDS类由生成器自动实例化(并且,有趣的是,DataItem类只有一个实例——在将来的版本中,我将实现一个类属性来告诉生成器排除实例化该类)。

public class ListBoxDS
{
  // the DataSource property can handle any IList derived object
  private ArrayList dataSource;

  // must be a property! (not a field!!!)
  public ArrayList DataSource
  {
    get {return dataSource;}
  }

  public ListBoxDS()
  {
    dataSource=new ArrayList();
    dataSource.Add(new DataItem("AL", "Alabama"));
    dataSource.Add(new DataItem("WA", "Washington"));
    dataSource.Add(new DataItem("WV", "West Virginia"));
    dataSource.Add(new DataItem("CT", "Connecticut"));
    dataSource.Add(new DataItem("CA", "California"));
    dataSource.Add(new DataItem("RI", "Rhode Island"));
    dataSource.Add(new DataItem("NY", "New York"));
  }

  public void ShowValue(object sender, EventArgs e)
  {
    ListBox lb=(ListBox)sender;
    TabPage tp=(TabPage)lb.Parent;
    TextBox tb=(TextBox)FormHelper.FindControl(tp, "ByEvent");
    tb.Text=lb.SelectedValue.ToString();
  }
}

DataItem类定义如下:

public class DataItem
{
  private string shortName;
  private string longName;

  public DataItem(string shortName, string longName)
  {
    this.shortName=shortName;
    this.longName=longName;
  }

  public string ShortName
  {
    get {return shortName;}
  }

  public string LongName
  {
    get {return longName;}
  }

  public override string ToString()
  {
    return LongName+" ("+ShortName+")";
  }
}

内联数据源

或者,您可能有一个简短的列表,您只想硬编码成员,而不是以编程方式创建。这可以通过将DataSource指定为XML中的元素而不是属性来实现,如第四个选项卡页面所示:

<MyCodeBehindClass Name='TabPage4' Text='Inherit'
     Load='MyCodeBehindClass.OnLoad'>
  <def:CodeBehind Src="MyCodeBehind.cs">
  <reference assembly="System.Windows.Forms.dll"/>
  </def:CodeBehind>
  <Controls>
    <Label Location='10, 10' Size='150, 15' Visible='true'>Test</Label>
    <Label Location='10, 42' Size='50, 15' Visible='true'>Colors</Label>
    <ComboBox Name='Colors' Location='60, 40' Size='100, 100'
      DisplayMember='text' ValueMember='value'>
      <DataSource>
        <item value='1' text='Red'/>
        <item value='2' text='Green'/>
        <item value='3' text='Blue'/>
      </DataSource>
    </ComboBox>
  </Controls>
</MyCodeBehindClass>

请注意,项目*必须*指定具有“value”和“text”属性。

架构和用法

XmlGuiGenerator是一个非常灵活的东西(事实上,它完全不限于窗体/控件!),我想我应该制作一些使用场景图。希望它们能让您了解这个工具的丰富性。

一个简单示例

在此场景中,没有后台代码,XML本身也没有指定任何事件。在这里,应用程序必须以编程方式连接任何事件处理程序,就像在常规开发中一样。

自动事件连接

在此示例中,生成器自动处理事件连接。事件在XML中与指定给应用程序的类实例中的方法名称一起指定。当然,没有什么能阻止应用程序以编程方式连接其他事件。

后台事件处理程序

在此示例中,应用程序未提供事件上下文。它必须以编程方式连接事件。此外,由于XML中定义了后台代码,事件可以由后台代码而不是应用程序处理。

后台代码和应用程序上下文事件

在此示例中,应用程序和后台代码事件处理程序都通过XML中指定的事件进行连接,并且两者都参与处理事件。

子控件

这是如何实例化子控件(或通常是集合)的示例,以及应用程序和后台代码上下文如何处理主窗体和子控件的事件。此图假定所有子控件都内联指定。

多个画布

此示例更复杂一些。在这里,我们有通过单独画布指定的子控件,允许子控件拥有独特的后台代码来管理事件。

继承自后台代码

最后,而不是将窗体基础指定为System.Windows.Forms命名空间(或其他命名空间)中的一个类,您可以将窗体基础指定为后台代码中定义的类。只要后台代码类继承自相应的类,您就可以在后台代码和超类中设置属性和事件。

调试

调试XML生成的GUI并不容易。有几个项目正在进行中,以使您的生活更轻松。一个是Form2Xml转换器,它解析现有的Visual Studio Designer开发的窗体。另一个是通用的窗体布局工具(http://www.divil.co.uk/net/articles/designers/hosting.asp))。任何人愿意从事这些项目,或者如果您有更好的想法,请与我联系。

致谢

荣誉归于CPian Leppie的许多建议,CPian tditiecher发现了一个bug,Kent Roylance在处理此工作的CVS存储库方面很有耐心(顺便说一句,如果您有兴趣贡献,请联系我),创建了一个Form2Xml实用程序并向我指出了一个通用窗体设计器,jconwell撰写了关于Dot Net Script的出色文章,以及Roy Davis的许多优秀建议。

其他链接

有人指出,这非常像Longhorn发布时计划的XAML。有趣的是,我开始这个项目时甚至不知道XAML。哦,好吧。无知是福。有关XAML的进一步阅读:http://longhorn.msdn.microsoft.com/lhsdk/port_ref_elements.aspx

许可

使用此GUI生成器必须遵守GNU通用公共许可证的条款。

结论

考虑到这种生成器不仅仅局限于Windows窗体控件。它可以轻松应用于Web窗体控件,并且可以泛化以构造和初始化任何类型的对象!使用反射,可以为特定类实例成员提供控件值的设置器和获取器。您可以用它做很多事情。

© . All rights reserved.