本来想分析MarkUX框架的代码,但是需要购买,暂时就放下了,恰巧遇到PerGUI几乎雷同的设计思路。那就直接拿来分析吧。此文是我读此框架的笔记,外行看热闹内行看门道至于能理解多少就见仁见智了,如果我没理解的地方还请不吝指出。最后你可以在CodePlex上找到此开源代码。

此项目的介绍

基于XML解析的数据,生产出对应的Widget。如果你有WPF开发经验的话会发现,这就是同一种思路。配置(xml或者其他存储方式)文件 -> 映射(通过某种转换) -> UI表现 .且不说游戏领域的这套做法,其他领域已经玩的非常溜了。此源码总的来说极轻量级 非常容易定制,对于拿来主义来说还真是比较不错的选择(当然不是说我自己啦 哈哈)。

使用场景

关于这种UI的开发模式 肯定适用于热更新(这个还真需要脑补),其次适用于给非程序人员使用(甚至是策划),极大降低程序对UI表现改动的。好处显而易见。

亮点

通过配置创建出UI,支持嵌套,容易上手

框架实现思路

数据模型类(Model) = 代码(包括函数与属性) + xml工具(此处使用Microsoft System.Xml)

预实现UI类 + 数据模型类(Model) = PurGUI

预实现UI类 与 Model 之间的糅合 使用的是 属性映射。直接通过Model的属性映射到配置文件,序列化出来的就是所需组件。

核心:灵活利用xml反序列化特性抽象控件树加载逻辑。

代码目录结构及简要说明(通关攻略请配合源码使用)

Attributes

BindControlAttribute.cs 属性级别标记 标记出需要绑定的数据模型(Model)的当前属性用何种控件显示
ButtonClickEventAttribute.cs 函数级别标记 标记出需要绑定到数据模型(Model)的函数
ControlValueTargetAttribute.cs 属性级别标记 标记出需要绑定的数据模型(Model)的属性
WindowResourceAttribute.cs 类级别标记 标记出需要读取配置(XML)名

看到这几个文件名,基本上能猜出来 这个框架的套路了。通过新建一个 “数据模型(Model)”,也可称之为浑身都是标记的具体UI类,我们将 需要使用到的控件全部标记在属性上与函数上 我称之为 <控件与具体UI类强绑定> 。 通过类级别标记的值 映射(通过某种转换) 到配置 。

Cursor

PGCursor.cs 小彩蛋,关于鼠标的配置。

Helper

PGCursorListener.cs 彩蛋相关(覆写了鼠标点击的接口,配合PGCursor实现鼠标的姿态切换)
PGDraggable.cs 彩蛋x2 UI拖动组建(覆写了 IDragHandler 拖动接口 添加了拖动超屏判断)

1
2
3
4
5
6
7
8
9
10
11
public void OnDrag(PointerEventData eventData)
{
_mTransform.SetAsLastSibling();
_mTransform.position += new Vector3(eventData.delta.x, eventData.delta.y);
_mTransform.position = new Vector3
(
Mathf.Min(Screen.width - _mTransform.sizeDelta.x, Mathf.Max(0f, _mTransform.position.x)),
Mathf.Min(Screen.height - _mTransform.sizeDelta.y, Mathf.Max(0f, _mTransform.position.y)),
_mTransform.position.z
);
}

PGEditFixer.cs 对UGUI的bug修正,闪动光标的初始位置偏移问题(执行一次)

PGXmlLoader.cs 配置加载
以下配置加载是体现此框架的技巧性的部分之一了,这是一个加载规则(Rule) , <PreGUI.Widgets.PG[???]> 就是命名空间。利用 PreGUI.Widgets 命名空间下的已PG开头的类 作为反序列化对象。好处是能通过配置序列化出各种预定义的控件类型,抽象了具体的判断。(灵活利用Microsoft提供的Xml类库也是一种能力的体现方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static IPGControl XmlProcessChildControl(this XmlNode node)
{
var type = System.Type.GetType("PreGUI.Widgets.PG" + node.Name);

if (type == null)
return null;

var iType = type.GetInterface("IPGControl");

if (iType == null)
return null;

return ((IPGControl)System.Activator.CreateInstance(type)).GenerateFromXmlNode(node);
}

PreGUIExtensions.cs 里面几个扩展函数就比较有意思了(重要)。

//反射被绑定的类的事件实现 -> 此处直接定位到了 Model(数据模型) 标记ButtonClickEvent特性的 函数,属于函数映射(Mapping)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void CallEventAction<T>(this object viewModel, string controlName, T source)
where T : class, new()
{
var eventsToCall = viewModel
.GetType()
.GetMethods()
.Where(x => x.GetCustomAttributes(typeof(ButtonClickEventAttribute), false).Any(z => ((ButtonClickEventAttribute)z).ControlName == controlName));

foreach (var ev in eventsToCall)
{
ev.Invoke(viewModel, new object[] { source });
}

}

//反射被绑定的类的属性(数值)实现 -> 此处直接定位到了 Model(数据模型) 标记ControlValueTarget特性的 属性(有点拗口),属于属性映射(Mapping)

1
2
3
4
5
6
7
8
9
10
11
12
public static void UpdateControlValue<T>(this object viewModel, string controlName, T newValue)
{
var members = viewModel
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(typeof(ControlValueTargetAttribute), false).Any(z => ((ControlValueTargetAttribute)z).ControlName == controlName));

foreach (var member in members)
{
member.SetValue(viewModel, newValue, null);
}
}

//此函数一般由Widget自己调用 用来给 Model(数据模型) 内 标记了 BindControl 的标签 赋值为当前Widget.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void BindTo<T>(this T thisControl, string controlName, object viewModel)
where T : IPGControl
{
var members = viewModel
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(typeof(BindControlAttribute), false).Any(z => ((BindControlAttribute)z).ControlName == controlName));

foreach (var member in members)
{
member.SetValue(viewModel, thisControl, null);
}
}

Interface

IPGControl.cs 抽象控件的标准化接口(这就是实现多种输入输出组件统一化的最终奥义)
此 LayoutControls(PreGui gui, GameObject parent, string windowName) 函数签名 告诉我们可以基于此接口创建出控件.
此 IPGControl GenerateFromXmlNode(XmlNode node) 函数签名告诉我们它是用来加载配置的.此处用了一个称之为FluentInterface的技巧(喂自己吃了颗语法糖)。既然有了配置的信息,那么自然可以通过上面的接口构造出组件。

1
2
3
4
5
public interface IPGControl 
{
GameObject LayoutControls(PreGui gui, GameObject parent, string windowName);
IPGControl GenerateFromXmlNode(XmlNode node);
}

Widgets

PGButton.cs
PGCaptionLabel.cs
PGCheckBox.cs
PGHorizontalLayout.cs
PGLabel.cs
PGPanel.cs
PGScrollWindow.cs
PGTextEdit.cs
PGVerticalLayout.cs 此处以上文件皆遵循 IPGControl 接口
PGWindowTitle.cs 此文件意在添加一个自定义的Head,实际上亦可转化为 遵循 IPGControl 的组件
PGWindow.cs 控件制造机/控件工厂 随便怎么叫都行,它是一切控件的(LayoutControls接口)组装的指派者。

static PGWindow LoadFromXml (XmlNode node) 函数签名中截取代码 实现框架的关键函数

1
2
3
for (var i = 0; i < node.ChildNodes.Count; i++) {
result.Children [i] = node.ChildNodes.Item (i).XmlProcessChildControl ();
}

void InitializeWindow (GameObject gameObject) 函数签名中截取代码 实现框架的关键函数

1
2
3
if (Children != null) {
Children.ProcessControls (Gui, contentArea, ControlName);
}

这两个函数决定了框架的UI组件以深度优先的方式加载(因为xml可以嵌套,也就是说代码中无需重复造轮子).

Root(根目录) -> PurGUI.cs

包含 创建UI的入口(函数签名) -> void BindViewModel (string windowName, object viewModel)
包含 事件绑定 就是基于 Key - Value的消息触发模型。Value即为Action.
包含 PurGUI框架的基础配置
包含(重要) UI所需基础元素,如Camera,_canvas

PGWindow的直接操控者,此框架的大门,每当创建一个UI组件的时候,都会将自身引用 传递给 所创建的组件。这意味着 PurGUI与Widget 是一个一对多的关系。只要PurGUI发生更改 所有的组件都可以获取实时的变更。这种做的坏处是无法(面对复杂需求就跪了)进行多个摄像机的管理,如果需要新增多个相机或根节点(Canvas)则需要创建多个基础(PurGUI),当然这存在很大的优化余地 这里暂时就不讨论了。