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






4.33/5 (11投票s)
为您的应用程序设置皮肤的新方法!
引言
自从我开始开发 Windows Forms 应用程序以来,我一直渴望为同一个应用程序拥有不同的用户界面。我考虑过从基于图像的皮肤化到创建自定义窗口框架(例如 Mozilla 的 XUL)的各种技术。基于图像的皮肤化不是非常动态,而创建框架需要大量的开发。
在寻找动态皮肤化的过程中,我产生了使用 .NET 库提供必要窗体的想法。在本文中,我将详细介绍我的解决方案。
使用代码
下图简要说明了 GUISS 的工作原理
我们有三个组件:父应用程序、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.FormParent
。FormParent
抽象类具有几个您必须覆盖的属性/函数。它们是:
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.
}
...
}
}
那么,FormParent
和 FormWindow
是如何交互的呢?看一下代码块上方的示意图。父应用程序通过调用 Skin.CreateForm(FormParent formParent)
函数来请求 FormWindow
。一旦父应用程序调用此函数,GUISS 库就会调用您刚刚在代码块中看到的已覆盖函数。
首先,GUISS 检查 FormParent
的 FormName
属性是否与 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()
之后立即调用。
例如,FormParent
在 FormWindow
上需要 btn_SaveAs
控件。它还要求它是一个 RichTextBox
。FormWindow
必须使用 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.Controls
、FormWindow.Controls.Controls
、FormWindow.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 的文章。