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

用于 Windows Forms .NET 的 GUI 皮肤系统

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (11投票s)

2008 年 12 月 22 日

CPL

5分钟阅读

viewsIcon

106480

downloadIcon

3324

为您的应用程序设置皮肤的新方法!

GUISS Screenshot

引言

自从我开始开发 Windows Forms 应用程序以来,我一直渴望为同一个应用程序拥有不同的用户界面。我考虑过从基于图像的皮肤化到创建自定义窗口框架(例如 Mozilla 的 XUL)的各种技术。基于图像的皮肤化不是非常动态,而创建框架需要大量的开发。

在寻找动态皮肤化的过程中,我产生了使用 .NET 库提供必要窗体的想法。在本文中,我将详细介绍我的解决方案。

使用代码

下图简要说明了 GUISS 的工作原理

Schematic1

我们有三个组件:父应用程序、GUISS 库和皮肤库。处理顺序如下(请注意,这非常简化)

  • 父应用程序实例化一个新的 Forcepoint.GUISS.Skin 类。
  • 要加载皮肤库,请调用 Forcepoint.GUISS.Skin.LoadSkin(string fileName)
  • 父应用程序创建一个继承 FormParent 的自身类的新实例。
  • 新实例化的类用作 Forcepoint.GUISS.Skin.CreateForm(FormParent formParent) 的参数。
  • GUISS 库“提取”一个与给定 FormParent 匹配的 FormWindow,并返回一个功能齐全的 FormWindow,它具有与常规窗体相同的属性、方法等。
  • GUISS 检查 FormParent 所需的所有控件是否在 FormWindow 上可用。
  • GUISS 将所有必要的事件挂接到控件。
  • 要获得具有不同皮肤的 FormWindow,请加载不同的皮肤库。

使用 GUISS 所需的一切都可以在 Forcepoint.GUISS 命名空间中找到。

创建父应用程序

简而言之,父应用程序(我经常缩写为“PA”)包含运行应用程序所需的逻辑代码。皮肤库则完全相反,它们仅包含父应用程序所需的窗体。

在您的解决方案中,您将拥有一个或多个 FormParent 类,而不是常规窗体。它们只是标准类,但它们继承自 Forcepoint.GUISS.FormParentFormParent 抽象类具有几个您必须覆盖的属性/函数。它们是:

  • string FormName
  • Dictionary<string,> DeclareRequiredControls()
  • List<subscribedevent> DeclareEventsToSubscribeTo()
  • Dictionary<string,> DeclareWatchedObjects()

在代码中,它看起来像这样:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

using Forcepoint.GUISS;

namespace ParentApplication
{
    public class MainForm : FormParent
    {
        public override string FormName
        {
            get { return "MainForm"; }
        }

        public override Dictionary<string,> DeclareRequiredControls()
        {
            Dictionary<string,> tmp = new Dictionary<string,>();

            tmp.Add("rtb_Text", typeof(RichTextBox));

            tmp.Add("btn_Open", null);
            tmp.Add("btn_Save", null);
            tmp.Add("btn_SaveAs", null);

            return tmp;
        }
        public override List<subscribedevent> DeclareEventsToSubscribeTo()
        {
            List<subscribedevent> tmp = new List<subscribedevent>();

            tmp.Add(new SubscribedEvent(null, "Load", 
                    new Subscriber.GenericEventHandler(MainForm_Load)));

            //The btn_..._Click's aren't shown in this code block.
            tmp.Add(new SubscribedEvent("btn_Open", "Click", 
                        new Subscriber.GenericEventHandler(btn_Open_Click)));
            tmp.Add(new SubscribedEvent("btn_Save", "Click", 
                        new Subscriber.GenericEventHandler(btn_Save_Click)));
            tmp.Add(new SubscribedEvent("btn_SaveAs", "Click", 
                        new Subscriber.GenericEventHandler(btn_SaveAs_Click)));

            return tmp;
        }
        public override Dictionary<string,> DeclareWatchedObjects()
        {
            //Will be discussed later in this article.
        }
        
        ...
        
    }
}

那么,FormParentFormWindow 是如何交互的呢?看一下代码块上方的示意图。父应用程序通过调用 Skin.CreateForm(FormParent formParent) 函数来请求 FormWindow。一旦父应用程序调用此函数,GUISS 库就会调用您刚刚在代码块中看到的已覆盖函数。

首先,GUISS 检查 FormParentFormName 属性是否与 FormWindow 的相同。之后,GUISS 验证 FormParent 所需的所有控件(DeclareRequiredControls())是否在 FormWindow 上。如果不在,则会抛出错误。完成后,GUISS 将适当的事件挂接到所有控件(DeclareEventsToSubscribeTo())。最后,GUISS 调用 DeclareWatchedObjects(),我们将在本文后面讨论这一点。

如果一切顺利,Skin.CreateForm(FormParent formParent) 将返回一个 FormWindow。这是一个继承 System.Windows.Forms.Form 的类,因此它的行为方式相同。

创建皮肤库

皮肤库由继承自 FormWindow 的类组成,而 FormWindow 又继承自 Form。因此,您可以使用 Visual Studio 设计器。您无需覆盖任何内容,FormWindow 类仅添加了一些您可以(也应该)使用的函数和方法。这是该类外观的示例:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using Forcepoint.GUISS;

namespace TextEditor.DarkSkin
{
    public partial class MainForm : FormWindow
    {
        public MainForm()
        {
            InitializeComponent();

            RegisterControl(rtb_Text.Name, rtb_Text);
            RegisterControl("btn_Open", btn_Open);
            RegisterControl("btn_Save", btn_Save);
            RegisterControl("btn_SaveAs", btn_SaveAs);
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            //This will be discussed later on in this article.
            this.WatchedObjects["current_Text"].OnSet += 
              new EventHandler<watchedobjectseteventargs>(current_Text_OnSet);
            this.WatchedObjects["current_Text"].OnGet += 
              new EventHandler<watchedobjectgeteventargs>(current_Text_OnGet);
        }
       
        ...
        
    }
}

它们如何交互

FormWindow 必须“注册”FormParent 所需的控件。这可以通过 RegisterControl()RegisterAllControls() 方法来完成。一个或两个这些方法必须在 FormWindow 的构造函数中的 InitializeComponent() 之后立即调用。

例如,FormParentFormWindow 上需要 btn_SaveAs 控件。它还要求它是一个 RichTextBoxFormWindow 必须使用 RegisterControl()RegisterAllControls() 注册此控件。FormParent 类可以通过 FormWindow.GetRegisteredControl() 函数访问这些控件。

string ExampleString = 
 ((RichTextBox)this.FormWindow.GetRegisteredControl(
 "btn_SaveAs").ControlObject).Text;

在使用 GetRegisteredControl 时,您并不总是知道控件的类型。如果发生这种情况,您可以使用 WatchedObject

this.WatchedObjects["current_Text"].OnSet += 
  new EventHandler<watchedobjectseteventargs>(current_Text_OnSet);
this.WatchedObjects["current_Text"].OnGet += 
  new EventHandler<watchedobjectgeteventargs>(current_Text_OnGet);

WatchedObject["current_Text"].Object 被获取或设置时,会调用相应的事件。父应用程序和皮肤库都可以设置这些事件。

例如(这些方法都在皮肤库中)

void current_Text_OnSet(object sender, WatchedObjectSetEventArgs e)
{
    this.rtb_Text.Text = e.NewValue.ToString();
}
void current_Text_OnGet(object sender, WatchedObjectGetEventArgs e)
{
    e.ValueToReturn = this.rtb_Text.Text;
}

当对象被设置时(由父应用程序完成),rtb_Text 控件的 Text 属性被设置为新值。请注意,父应用程序不必了解皮肤库中的控件。现在,当父应用程序获取该对象时,皮肤库返回 rtb_Text 控件的 Text 属性。

关注点

FormWindow 控件注册

如前所述,有两种方法用于注册控件。它们是 RegisterControl()RegisterAllControls()

RegisterAllControls 递归地遍历所有控件。它通过遍历 FormWindow.ControlsFormWindow.Controls.ControlsFormWindow.Controls.Controls.Controls 等来实现。这对于 Panel 和一些其他控件很有效,但对于 ToolStripMenuItem 等不起作用。为了解决这个问题,RegisterControl() 就派上用场了。

RegisterControl("ToolStripMenuItem_SaveAs", ToolStripMenuItem_SaveAs);
//even easier:
//RegisterControl(ToolStripMenuItem_SaveAs.Name, ToolStripMenuItem_SaveAs);

安全

父应用程序需要由皮肤库设置某些安全权限。请看下面的代码,了解如何获取权限:

[STAThread]
static void Main()
{
    ...
    Skin Skin = new Skin();
    //Set the security checks.
    Skin.SecurityChecks.Add(typeof(FileIOPermissionAttribute), 
         new SecurityCheckDelegate(SecurityCheck_FileIOPermission));

    Skin.LoadSkin(Application.StartupPath + @"\Skins\" + sp.listBox1.Text);

    Application.Run(Skin.CreateForm(new MainForm()));
}

private static bool SecurityCheck_FileIOPermission(object permissionAttribute)
{
    //PermissionAttribute is an attribute in the Skin Library.
    
    FileIOPermissionAttribute fiopa = 
       (FileIOPermissionAttribute)permissionAttribute;
    //This basically means: Request Refusal 
    //for having Unrestricted access to the FileSystem.
    //In other words: block all access.
    if (fiopa.Action == SecurityAction.RequestRefuse && fiopa.Unrestricted == true)
        return true;
    else
        return false;
}

皮肤库在 AssemblyInfo.cs 文件的底部设置安全权限:

//This will fail the security check. Unrestricted must be true to pass.
[assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse, 
                                     Unrestricted=false)]

最后的寄语

我希望本文能让一些事情变得清晰。我强烈建议您仔细查看演示项目。尝试在演示中复制相同的“技术”以用于您自己的项目。还有一个 参考库。它与二进制文件附带的帮助文件相同。如果您有任何疑问,请随时在本文底部留言。

最后,GUISS 在 Sourceforge 上也有一个页面。您可以去那里获取最新消息和更新。

历史

  • 2008-12-22:提交了陪同 GUISS 0.2.0.1 的文章。
© . All rights reserved.