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

Windows 样式通过 IExtenderProvider

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.36/5 (4投票s)

2005 年 8 月 1 日

8分钟阅读

viewsIcon

39784

downloadIcon

575

使用 IExtenderProvider 和 C# 为 .NET Windows Forms 实现级联样式表。

The Designer before and after style is applied

引言

当我读到 James T. Johnson 关于 IExtenderProvider 的文章时,我找到了我一直在寻找的东西:一种在 Windows Forms 中实现级联样式表 (CSS) 的方法。虽然完全实现与 HTML 版本匹配的 CSS 需要大量的细节,但本文将重点关注 ButtonTextBox 控件。它更多的是一个概念证明,证明可以做到这一点,而不是一个成品。将来,我希望发布一个功能齐全的 Windows Forms CSS 实现。目前,CSS 的大部分酷炫功能(如“级联”和各种元素选择器)都留给程序员想象。

背景和资源

读者应该熟悉 C# 编程和 IExtenderProvider。后者可以从 James T. Johnson 的精彩文章:《了解 IExtenderProvider》中学习。我将在设置我的 IExtenderProvider 时使用 James 的结构。您不需要熟悉 CSS 样式表,因为我使用的概念非常基础。但是,如果您想扩展这个小型应用程序以帮助您的 Windows 开发,您绝对应该参考 W3 的 CSS,并仔细考虑每个概念如何最好地通过 IExtenderProvider 实现。由于此解决方案涉及在 Visual Studio 的设计时运行的代码,因此这可能很有趣:调试设计时功能

使用代码

Zip 文件包含一个名为 WindowsStyle 的解决方案。您需要执行以下操作才能使解决方案正常运行:

  • 将名为 WindowsStyle.zip 的 Zip 文件解压缩到 c:\projects 或您的项目目录。
  • 双击解决方案以在 Visual Studio 中打开它。
  • 生成解决方案 (Ctrl+Shift+B)。
  • 在解决方案目录中找到 stylesheet.xml 文件。
  • 打开 Form1.cs 窗体。
  • 找到名为 Stylesheet 的属性,并将其设置为上面 stylesheet.xml 的完整路径。
  • 如果您找不到 Form1Stylesheet 属性,您可能需要先将一个 Style IExtenderProvider 组件添加到 Form1 中。
    • 右键单击工具箱(按 Ctrl+Alt+X 获取)并选择“添加/删除项...”
    • 单击“浏览”,转到 bin 目录并选择 WindowsStyle.exe
    • 现在您将拥有一个 Style 组件,可以从工具箱拖放到 Form1 上。
    • 执行此操作后,按照上述方法设置 Form1Stylesheet 属性。
  • 单击任何 ButtonTextBox 控件,或将新控件拖到窗体上。设置它们的 CssClass 属性,然后观察它们的变化。
  • 请注意,手动将属性设置为其他值然后运行代码,仍然保留 CssClass 指定的外观。

详细信息

IExtenderProvider 实现为一个组件(典型),它为控件提供 CssClass 属性,为窗体提供 Stylesheet 属性。

[ProvideProperty("CssClass", typeof(System.Windows.Forms.Control))]
[ProvideProperty("Stylesheet", typeof(System.Windows.Forms.Form))]
public class Style : System.ComponentModel.Component, IExtenderProvider
{
    private Hashtable properties = new Hashtable();

Hashtable properties 成员包含 (object) --> (properties) 对。这里 object 可以是控件 (Button, TextBox, Form 等),而 propertiesProperties 类的一个实例。

private class Properties
{
    public string CssClass;
    public string Stylesheet;

    public Properties()
    {
        CssClass = string.Empty;
        Stylesheet = string.Empty;
    }
}

此类是我们要为所有控件提供的所有方法的包装器。它包含 CssClassStylesheet,因为我们的控件和窗体将需要它们。

实现 IExtenderProvider 接口时,唯一必须定义的是 CanExtend 方法。Designer 调用此方法,以便它可以确定此 IExtenderProvider 为哪些对象提供扩展。在我们的方法中,由于 FormSystem.Windows.Forms.Control 的子级,因此我们只需要扩展 System.Windows.Forms.Control

public bool CanExtend(object extendee)
{
    return extendee is System.Windows.Forms.Control;
}

因此,这会产生一个副作用,即在与我们的 IExtenderProvider 相同的窗体上的所有控件都会获得 CssClass 属性。这就是我们想要的,尽管目前我们只支持 ButtonTextBox 控件。

接下来,为了实际提供属性,我们的 IExtenderProvider 必须实现它提供的所有属性的 Get[property-name]Set[property-name] 方法。这些实际上是允许反射发挥其属性扩展魔力的必要条件。在我们的例子中,这些方法是 GetCssClass, SetCssClass, GetStylesheetSetStylesheet

[Description("Set this property to apply a class of Style to this Control")]
[Category("Style")]
public string GetCssClass(System.Windows.Forms.Control c)
{
    return EnsurePropertiesExists(c).CssClass;
}

/// <summary>
/// Set the CssClass property. When this is done, automatically read the
/// Form's Stylesheet document and change this control according to the
/// CssClass it wants from that Stylesheet
/// </SUMMARY>
/// <PARAM name="c">The Control changing its CssClass property</PARAM>
/// <PARAM name="value">the new CssClass value</PARAM>
public void SetCssClass(System.Windows.Forms.Control c, string value)
{
    // set the Control's CssClass property
    EnsurePropertiesExists(c).CssClass = value;

    // don't load the class if it's the empty string
    if( value.Length < 1 )
    {
        return;
    }

    // depending on the type of control, change its style
    switch( c.GetType().FullName )
    {
        case "System.Windows.Forms.Button":
            CssButton(c);
            break;
        case "System.Windows.Forms.TextBox":
            CssTextBox(c);
            break;
        default:
            break;
    }
}

我将讨论 CssClass 属性,您可以查看代码中的 Stylesheet 属性。Get 方法确保 CssClass 属性对于调用控件存在,然后返回它。如果此属性不存在,Ensure 方法可以处理错误。这是常见的问题来源,我在其中加入了一些错误处理。控件试图获取它们没有的属性可能会导致问题。

Set 方法稍微复杂一些。它首先设置属性的值。如果值为字符串为空,则返回。但是,如果它有实质内容(即实际的 CssClass),则继续进行。此方法会确定设置 CssClass 属性的控件类型,并调用适当的方法从样式表中加载该控件的样式。这里有一个直接处理 ButtonTextBox 控件的方法。当然可以设计出更好的算法,但由于后见之明总是 20/20,那还是留给后见之明吧。现在,继续 CssButton 方法。这实际上会尝试从样式表中加载特定的 CssClass 并将其属性应用于此 Button

private void CssButton(object sender)
{
    System.Windows.Forms.Button b = (System.Windows.Forms.Button)sender;
    Hashtable style = GetStyle(b);
    if( style == null ) return;

    if( style["Width"] != null )
    {
        b.Width = int.Parse((style["Width"]).ToString());
    }

    if( style["Height"] != null )
    {
        b.Height = int.Parse((style["Height"]).ToString());
    }
    
    if( style["ForeColor"] != null )
    {
        b.ForeColor = System.Drawing.Color.FromName(style["ForeColor"].ToString());
    }
    
    if( style["BackColor"] != null )
    {
        b.BackColor = System.Drawing.Color.FromName(style["BackColor"].ToString());
    }
    
    if( style["FlatStyle"] != null )
    {
        switch( style["FlatStyle"].ToString() )
        {
            default:
            case "Standard":
                b.FlatStyle = FlatStyle.Standard;
                break;
                        case "Popup":
                b.FlatStyle = FlatStyle.Popup;
                break;
                        case "Flat":
                b.FlatStyle = FlatStyle.Flat;
                break;
                        case "System":
                b.FlatStyle = FlatStyle.System;
                break;
        }
    }
}

那个返回 HashtableGetStyle 方法是关键。我们将在下面讨论它,但首先是简单的部分。返回的 Hashtable style 可能包含也可能不包含某些属性。如果这些属性在样式表中在此 ButtonCssClass 类下定义,那么它们将包含在内。然后,Button 可以查看 style Hashtable 并询问它一系列问题,例如:您有 Width 吗?如果有,则将按钮的宽度设置为它拥有的 Width。您可以看到,您可以做一些很酷的事情,例如设置 ButtonFlatStyle。围绕此代码进行一些错误处理可以消除关于样式表中格式不正确的即时错误(例如,为 Width 输入 1p 而不是 10 将导致 InvalidCast)。

现在,正如承诺的那样,让我们看看加载样式表本身并返回特定控件正在查找的特定 CssClass 下的属性的代码。

public Hashtable GetStyle( System.Windows.Forms.Control c )
{
     System.Windows.Forms.Control parentForm = c.Parent;
     while( parentForm != null && !(parentForm is System.Windows.Forms.Form) )
     {
         parentForm = parentForm.Parent;
     }
    if( parentForm == null ) return null;
    string stylesheet = EnsurePropertiesExists(parentForm).Stylesheet;
    if( stylesheet.Length < 1 || !File.Exists(stylesheet) ) return null;
    
    XmlDocument x = new XmlDocument();
    try
    {
        x.Load(stylesheet);
    }
    catch( IOException ex )
    {
        System.Diagnostics.Debug.Write("Error opening" + 
          " stylesheet document for "+c.Name+": "+ex.ToString());
        return null;
    }

    string cssClass = EnsurePropertiesExists(c).CssClass;
    
    XmlNodeList nodes = x.SelectNodes(string.Format("/stylesheet" + 
                                "/class[@name='{0}']/*",cssClass));
    if( nodes.Count < 1 )
    {
        EnsurePropertiesExists(c).CssClass = string.Empty;
        throw new Exception(string.Format(
            @"Stylesheet: {0}
              CssClass: {1}
              This style class does not exist or 
              does not have any properties",stylesheet,cssClass));
    }
    
    Hashtable style = new Hashtable();
    foreach( XmlNode node in nodes )
    {
        style[node.Name] = node.InnerText.TrimEnd('\n','\r','\t',' ');
    }
    return style;
}

这分几步进行。首先,它找到控件的 Form 父级。这可能是几级向上,因为控件可能位于 GroupBoxTab 或其他容器中。然后,它加载 Form 父级的样式表。请注意,这里有空间加载所有父级的样式表并将它们合并在一起。这将允许 CSS 的酷炫“级联”功能,但这超出了本文的范围。如果成功将样式表加载为 XML 文档,请获取此控件正在查找的 CssClass。这是使用 XPath 查询完成的。XPath 是一种强大的查询语言,在此处进行了很好的总结:.NET 和 XML:第 1 部分 — XPath 查询。只要知道这个特定的 XPath 查询返回的是 CssClass 名称下控制器的所有子节点即可。然后,GetStyle 方法将此类的所有属性打包到一个 Hashtable 中,并将其返回给第一个感兴趣的控件。

上面提到的 CssButton 方法使用此 GetStyle 方法,并遍历 Hashtable 来设置与之兼容的任何属性。这发生在设计时,当设置 ButtonCssClass 属性时。设计器将调用 SetCssClass 并执行上述所有操作。但是,运行时会发生什么?这部分的重要之处在于让每个控件在运行时加载其属性。虽然看到控件在运行窗体时的外观会很好,但如果您更改了样式表中的某些内容,控件不应该需要重置其 CssClass 属性才能传播更改。这将违背集中式样式表的全部目的。我确保控件在运行时加载其 CssClass 的方法是挂钩到 FormLoad 事件。这可以在 SetStylesheet 方法中完成。Form 可以遍历其组件并调用 CssButtonCssTextBoxCssWhatever 方法以在运行时设置样式。

public void SetStylesheet(System.Windows.Forms.Form f, string value)
{
    ...
    f.Load += new EventHandler(CssFormLoad);
}
        
private void CssFormLoad(object sender, EventArgs e)
{
    foreach( Control c in ((Form)sender).Controls )
    {
        // only apply style if the Control specified a CssClass
        if( EnsurePropertiesExists(c).CssClass.Length < 1 ) continue;
        
        switch( c.GetType().FullName )
        {
            case "System.Windows.Forms.Button":
                CssButton(c);
                break;
            case "System.Windows.Forms.TextBox":
                CssTextBox(c);
                break;
            default:
                break;
        }
    }
}

这会复制上面解释的所有内容,但在运行时执行。同样,肯定存在更好的数据结构来取代我使用的直接 switch 和单独的方法。

最后,让我们看一下我为样式表选择的格式。没什么特别的,stylesheet.xml 文件看起来像这样。我用它来演示文章顶部的示例。左侧的图像是我设置任何 CssClass 属性之前的样子。右侧的图像是之后的。

<stylesheet>
    <class name="RedButton">
        <WIDTH>40</WIDTH>
        <HEIGHT>40</HEIGHT>
        <FLATSTYLE>Popup</FLATSTYLE>
        <BACKCOLOR>Red</BACKCOLOR>
    </CLASS>
    <class name="WideButton">
        <WIDTH>200</WIDTH>
    </CLASS>
    <class name="PasswordTextBox">
        <PASSWORDCHAR>#</PASSWORDCHAR>
    </CLASS>
    <class name="MultilineTextBox">
        <MULTILINE>true</MULTILINE>
        <WIDTH>200</WIDTH>
        <HEIGHT>40</HEIGHT>
    </CLASS>
    <class name="SharedStyle">
        <FORECOLOR>White</FORECOLOR>
        <BACKCOLOR>Blue</BACKCOLOR>
        <FLATSTYLE>Flat</FLATSTYLE>
        <BORDERSTYLE>None</BORDERSTYLE>
    </CLASS>
</STYLESHEET>

就是这样。如果有什么不清楚的地方,请随时给我 写信。这是我第一次尝试,感谢您对任何不清楚的解释的耐心。

关注点

这是一个让我大开眼界项目。通过更彻底的实现,您可以节省大量花费在对 Windows Forms 中的各种控件进行对齐、调整大小、着色和标准化上的工时。通过仔细组织数据结构,可以扩展此功能以支持布局操作以及许多其他用途。目前,我认为 XML 可能是样式表本身的更好格式。另一方面,可以使用 Firefox 的开源 CSS 引擎来解析典型的 .css 文件作为样式表。

历史

首次提交。一个基本的概念证明,用于将样式从 XML 格式的样式表应用于 Windows Forms。

© . All rights reserved.