赞
踩
UI Toolkit是Unity最新的UI系统,它主要被设计用来优化不同平台的性能,此项技术是基于标准的web技术开发的(standard web technologies),既可以使用UI Toolkit来拓展Unity Editor,也可以在打包出来的游戏和应用里使用Runtime的UI(但需要安装UI Toolkit Package)
UI Toolkit包括以下内容:
Unity想推荐UI Toolkit成为新项目的UI系统,但是它跟传统的uGUI和IMGUI相比,还是少了一些功能,后面会再提到。
UI Toolkit是一系列用于创建UI的资源、函数、特性和工具的集合,它可以被用过来创建常规的UI,也可以用来拓展Unity Editor、制作Runtime的Debug工具和创建Runtime的游戏UI。
UI Toolkit受standard web technologies启发得到,很多核心的概念是类似的。
UI Toolkit分为以下三类:
UI Toolkit的核心是一个retained-mode UI system based on recognized web technologie。它支持stylesheets,和dynamic and contextual event handling.
UI System有以下内容:
UI Assets也就是UI Toolkit里用到的资源文件,UI Toolkit提供了两种资源文件来帮助构建UI,与web应用类似:
UXML全称为Unity eXtensible Markup Lauguage,是受HTML和XML启发得到的一种markup(标记)语言,用于定义UI结构和可复用的UI模板,Unity推荐使用UXML来创建UI,而不是在C#脚本里进行
USS全称为Unity Style Sheets:可以对UI使用可视的style和behaviours,与web的CSS类似,跟上面相同,Unity推荐用USS文件来定义style,而不是直接在C#脚本里对style这个property进行修改
提供了以下工具和资源:
UI Toolkit有两种获取方法,或者说有两个版本:
二者的区别如下:
该选择UI Toolkit两个版本的哪一个
如果相关UI只会在Editor下使用的话,那么使用内置的UI Toolkit,如果该UI需要既能在Editor,也能在Runtime下使用的话,那么使用对应的Package的版本,而且对应的版本也能安装最新的
安装 UI Toolkit package
打开Unity Editor的Package Manager:
UI Toolkit里UI的最基本构建单元被称为Visual Element,这些elements会被排序,形成一个有层次结构的树,称为Visual Tree,下图是一个例子:
Visual elements
VisualElement类是所有出现在Visual Tree里节点的基类,它定义了通用的properties,比如style、layout data和event handles。可以使用
stylesheet来自定义Visual Element的形状,也可以使用event callback来自定义Visual Element的行为
VisualElement的派生类可以再添加behaviour和功能,比如UI Controls,下面的这些都是基于Visual Element派生出来的:
后面还会介绍更多的内置的Controls
Panels
panel是Visual Tree的父object,对于一个Visual Tree,它需要连接到panel上才能被渲染出来,所有的Panels都从属于Window,比如EditorWindow,Panel除了处理Visual Tree的渲染外,还会处理相关的focus control和event dispatching。
每一个在Visual Tree里的Visual Element都会记录该Panel的引用,VisualElement对象里叫panel的property可以用于检测Element是否与Panel相连,若panel为null说明不相连
Draw Order
Visual Tree里默认是按深度遍历的顺序绘制Element的,如果想要改顺序,可以使用以下函数:
VisualElement e;
// 注意,下面的front和back都是视觉上的绘制关系,front意味着重叠部分不会被遮挡
// 会把该元素移到它原本的parent的children列表的最后面,所以该元素最后画,所以在top
e.BringToFront();
// 同上,正好反过来
e.SendToBack();
// 在parent的childrenn列表里,把e放到sbling的前面,即先画e再画sibling,所以e在底层
e.PlaceBehind(UIElements.VisualElement sibling);
// 同上,正好反过来
e.PlaceInFront(UIElements.VisualElement sibling);
Coordinate and position systems
UI Toolkit有一个强大的layout系统,根据每一个Visual Element里名为style的property,就能自动计算出每个Element的位置和size,后面还会详细提到Layout Engine.
UI Toolkit有两种坐标(coordinates):
设置一个Element的Coordinates的方法如下所示:
var newElement = new VisualElement();
newElement.style.position = Position.Relative;
newElement.style.left = 15;
newElement.style.top = 35;
在实际计算pos的时候,layout system会为每个element计算位置和size,再把前面的relative或absolute的coordinate offset加进去,最后的结果计算出来,存到element.layout里(类型是Rect)
The layout.position is expressed in points, relative to the coordinate space of its parent.
VisualElement类还有一个继承的Property,叫做ITransform,修改它可以添加额外的Local的position和rotation的变化,相关的变化不会显示在layout属性里,ITransform默认是Identity.
VisualElement.worldBounds代表Element在窗口空间的最终坐标bounds,它既考虑了layout,也考虑了ITransform,This position includes the height of the header of the window.
下面介绍一个例子,使用内置的UI Toolkit来创建Editor下的窗口。首先可以创建一个脚本,脚本内容如下:
using UnityEditor; using UnityEngine; using UnityEngine.UIElements; public class PositioningTestWindow : EditorWindow { [MenuItem("Window/UI Toolkit/Positioning Test Window")] public static void ShowExample() { var wnd = GetWindow<PositioningTestWindow>(); wnd.titleContent = new GUIContent("Positioning Test Window"); } public void CreateGUI() { // 创建两个数据一模一样的Element, 注意这里没有指定位置,因为位置是Layout系统自己算的 for (int i = 0; i < 2; i++) { // 创建两个Element, 为一个正方形, 背景是灰色 var temp = new VisualElement(); temp.style.width = 70; temp.style.height = 70; // marginBottom代表当Layout系统计算布局时, 此Element下方会预留20个像素的距离 temp.style.marginBottom = 20; temp.style.backgroundColor = Color.gray; rootVisualElement.Add(temp); } } }
点击对应的menu操作,就能出现窗口,如下图所示:
继续补充CreateGUI代码,现在画一个Label,而且更改它的style里的pos,代码如下:
public void CreateGUI()
{
// 创建两个数据一模一样的Element, 注意这里没有指定位置,因为位置是Layout系统自己算的
...//原本的不变
// 创建一个Label, Label是VisualElement的派生类
var relative = new Label("Relative\nPos\n25, 0");
// relative.style.position = Position.Relative;// 默认的就是Relative的方式, 所以不用刻意去写
relative.style.width = 70;
relative.style.height = 70;
relative.style.left = 25;
relative.style.marginBottom = 20;
relative.style.backgroundColor = Color.red;
rootVisualElement.Add(relative);
}
现在的结果变成了下图所示的样子,可以看到,原本Label应该是跟之前的一样,往下20个像素绘制的,但是这里有style.left = 25,所以在原本的基础上,加上offset(25, 0),得到最后右移的位置:
展示完了Relative的方式,下面再看看Absolute的例子,代码也是类似:
public void CreateGUI() { ...// 画原本三个Element的代码不变 // 又画两个相同的方块进行对比 for (int i = 0; i < 2; i++) { var temp = new VisualElement(); temp.style.width = 70; temp.style.height = 70; temp.style.marginBottom = 20; temp.style.backgroundColor = Color.gray; rootVisualElement.Add(temp); } // 绘制Absolute类型的方块:Absolute Positioning var absolutePositionElement = new Label("Absolute\nPos\n25, 25"); // 类型是Absolute, 基准点是parent element, 其parent element就是窗口里的rootVisualElement absolutePositionElement.style.position = Position.Absolute; absolutePositionElement.style.top = 25; // 设置上方间距 absolutePositionElement.style.left = 25; // 设置左边间距 absolutePositionElement.style.width = 70; absolutePositionElement.style.height = 70; absolutePositionElement.style.backgroundColor = Color.black; rootVisualElement.Add(absolutePositionElement); }
最后的效果如下图所示,黑色的方块:
注意,在EidtorWindow类里,有一个Property叫做public VisualElement rootVisualElement { get; }
,可以用于取得窗口的Visual Tree的root visual element。
Transformation between coordinate systems
VisualElement.layout.position和VisualElement.transform两个参数,决定了local coordinate system 和 the parent coordinate system直接的转换,静态类VisualElementExtensions为这些转换提供了一些方法:
Layout Engine可以基于Visual Elements的layout和style属性自动计算UI布局,它是基于Github上的开源项目Yoga开发的(Yoga implements a subset of Flexbox: a HTML/CSS layout system)。
要学习Yoga和Flexbox,还需要到文档上提供的链接里去看,这里就不挂链接了。
Layout System默认有以下特点:
使用layout engine的一些方法:
flex-grow: <value>;
) ,当element的大小由其兄弟element决定时, flexGrow 属性的值用作权重。UXML是一种文本文件,它定义了UI的逻辑结构,本章会介绍UXML的语法、还要如何写入、读取和定义UXML模板等,还包含了一些自定义新的UI Element的方法,以及使用UQuery的方法。
In UXML 可以:
如何理解USS和UXML文件
这里强调一下初次看到这的时候我不理解的问题,UI的structure和UI layout有何区别?
其实Structure代表了节点的组织关系,就是Hierarchy里的父子关系,而UI Layout则代表了每个UI节点的具体的style等参数,如下图所示,HTML文件记录是Structure,CSS文件里记录的是每个节点的绘制信息,这样一看应该就很清楚了:
类比到UI Toolkit里,UXML文件用于描述整体节点之间的Structure,也就是对应的父子连接关系,而每个节点都有自己的USS文件,用于描述那个节点的尺寸等UI信息。
自定义Visual Element
Unity的原文档连接在这里:https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-UXML.html
坦白说,这一段文档官方文档居然没有配合具体的代码展示,感觉官方写的东西就是一坨屎,下面会基于这坨垃圾玩意儿,进行解释,然后加上自己的解释和样例去帮助理解。
// 需要继承于VisualElement
class StatusBar : VisualElement
{
// 必须要实现一个默认构造函数
public StatusBar()
{
}
public string status { get; set; }
}
然后我试了试,创建了个EditorWindow窗口,代码如下:
public class MyEditorWindow :EditorWindow
{
[MenuItem("Window/Open My Window")]
public static void OpenWindow()
{
var window = GetWindow<MyEditorWindow>();
StatusBar statusBar = new StatusBar();
statusBar.status = "Hello World";
statusBar.style.width = 50;
statusBar.style.height = 50;
window.rootVisualElement.Add(statusBar);
}
}
然后打开EditorWindow,发现没有任何显示,但是我打开UIElements Debugger发现是有东西的,只是没有显示String和UI而已,如下图所示:
这是因为,还没有读取对应的UXML,来决定该element的结构。为了读取UXML文件,需要创建一个对应的factory类,这个类可以继承于UxmlFactory<T>
,一般推荐在Element类内定义,代码如下:
class StatusBar : VisualElement
{
// 在定义了这个类之后, 就可以在UXML文件里写StatusBar元素了,
// 不过我还不熟悉这个new class的写法
public new class UxmlFactory : UxmlFactory<StatusBar> { }
...
};
class StatusBar : VisualElement { public new class UxmlFactory : UxmlFactory<StatusBar, UxmlTraits> {} // 取的类名不变 public new class UxmlTraits : VisualElement.UxmlTraits { // 创建一个StringAttribute对象, StatusBar只有一个Attribute, 名字叫status UxmlStringAttributeDescription m_Status = new UxmlStringAttributeDescription { name = "status" }; // 定义UxmlChildElementDescription函数 // 函数返回空的IEnumerable,表示StatusBar的没有任何child element, 也不接受任何children public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get { yield break; } } // 会从XML parser里读取到对应的bag, 然后赋值给m_status public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { // calls base.Init() to initialize the base class properties. base.Init(ve, bag, cc); // 把此类定义在StatusBar内部, 可以直接获取私有成员status ((StatusBar)ve).status = m_Status.GetValueFromBag(bag, cc); } } public StatusBar() { m_Status = String.Empty; } string m_Status; public string status { get; set; } }
UxmlTraits类有两个作用:
上面的Trait类里定义了UxmlStringAttributeDescription
对象代表String的Attribute,一共有以下类型:
前面的uxmlChildElementsDescription函数里,写的代码是不支持任何Children的,如果想支持任何Children,可以这么写:
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get
{
yield return new UxmlChildElementDescription(typeof(VisualElement));
}
}
UxmlFactory和UxmlTraits实例
这一块内容Unity的文档居然没有给例子,真是辣鸡,这里举个例子。
举个例子,在定义这么一个类以后:
class TwoPaneSplitView : VisualElement
{
// 定义UxmlFactory类, 用于在UXML里识别此类, 并在里面创建此类对应的Tag
public new class UxmlFactory : UxmlFactory<TwoPaneSplitView, UxmlTraits> {}
// UxmlTraits类用于在UXML文件里添加自定义的Attributes, 它们都可以在UI Builder里看到
public new class UxmlTraits : VisualElement.UxmlTraits{}
}
只有在里面加上了UxmlFactory,才可以在Uxml里这么写:
<BuilderAttributesTestElement/>// 目前没有加任何Attribute
Defining a namespace prefix
在完成上面的代码后,就可以在UXML文件里使用对应的Element了,如果是在Namespace里面自定义Element,还需要做额外的处理。
需要定义一个namspace prefix, Namespace prefixes其实就是在UXML的root element上面声明的attributes,它会replace the full namespace name when scoping elements.
写法如下:
// This can be done at the root level (outside any namespace) of any C# file of the assembly.
[assembly: UxmlNamespacePrefix("My.First.Namespace", "first")]
[assembly: UxmlNamespacePrefix("My.Second.Namespace", "second")]
schema generation系统会做这些事情:
<UXML>
这个element上添加namespace prefix的定义xsi:schemaLocation
attribute.接下来,需要更新项目里的UXML schema,选择Assets > Update UXML Schema,保证text editor可以辨别出来新的element。
The defined prefix is available in the newly created UXML by selecting Create > UI Toolkit > Editor Window in the Project/Assets/Editor folder.
Customizing a UXML name
可以通过override继承于UxmlFactory类的Property,代码如下:
public class FactoryWithCustomName : UxmlFactory<..., ...>
{
// 暂时还不知道具体会展示在哪里
public override string uxmlName
{
get { return "UniqueName"; }
}
public override string uxmlQualifiedName
{
get { return uxmlNamespace + "." + uxmlName; }
}
}
Selecting a factory for an element
默认情况下,IUxmlFactory
会创建一个element,然后选择根据它的名字来选择对应的element,主要是为了让它在UXML文件里能够被识别出来
其实就是用XML语言写的表示UI逻辑结构的uxml文件,举个例子:
<-- 第一行是XML declaration, it is optional, 只可以出现在第一行, 前面不允许有空格--> <-- version的attribute必须要写, encoding可以不写, 如果写了, 就必须说清楚文件的字符encoding --> <?xml version="1.0" encoding="utf-8"?> <-- UXML 代表document root, 包含了用于namespace prefix definitions和schema的源文件位置的attributes --> <UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <-- 下面这句话有点像是using UnityEngine.UIElements, 表示后面的Label什么的都是这个ns下的, 这里的ns是作为默认的ns --> xmlns="UnityEngine.UIElements" xsi:noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd" xsi:schemaLocation="UnityEngine.UIElements ../UIElementsSchema/UnityEngine.UIElements.xsd"> <-- 这下面的Label、Box、Button等都是Visual Element --> <-- 前面的Label代表继承于VisualElement的类名, 而后面的text叫做Element的Attributes---> <Label text="Select something to remove from your suitcase:"/> <Box> <Toggle name="boots" label="Boots" value="false" /> <Toggle name="helmet" label="Helmet" value="false" /> <Toggle name="cloak" label="Cloak of invisibility" value="false"/> </Box> <Box> <Button name="cancel" text="Cancel" /> <Button name="ok" text="OK" /> </Box> </UXML>
补充几点:
xmlns:engine="UnityEngine.UIElements"
,这种写法,相当于是typedef,之后可以写<engine:Button />
,等同于<UnityEngine.UIElements:Button />
<UXML>
的tag里包含对应的 namespace definition and schema file location,同时还要包含Unity原本的namespacesVisualElement通用的Attribute
一共有如下:
创建UXML template asset
When you create a new UXML template asset by selecting Asset > Create > UI Toolkit > Editor Window, the Editor automatically defines namespaces for you.
Adding styles to UXML
UXML文件可以引用USS文件,需要在任何element的声明下面使用<Style>
这个element,举个例子:
<engine:UXML ...>
<engine:VisualElement class="root">
<-- 意思所有的VisualElement都在调用这个style.uss作为布局? -->
<Style src="styles.uss" />
</engine:VisualElement>
</engine:UXML>
此时的USS文化和UXML需要在相同文件夹下,具体的style.uss文件内容如下:
#root {
width: 200px;
height: 200px;
background-color: red;
}
也可以不要uss文件,直接UXML里一行代码设置style:
<engine:UXML ...>
<engine:VisualElement style="width: 200px; height: 200px; background-color: red;" />
</engine:UXML>
Reusing UXML files
UXML文件也可以作为类似prefab的东西进行复用,举个例子,这里有个当作人像的UXML文件,它的UI里有一个图形和人名:
<engine:UXML ...>
<engine:VisualElement class="portrait">
<engine:Image name="portaitImage" style="--unity-image: url(\"a.png\")"/>
<engine:Label name="nameLabel" text="Name"/>
<engine:Label name="levelLabel" text="42"/>
</engine:VisualElement>
</engine:UXML>
在其他的UXML文件里,就可以把这个人像的UXML作为模板使用了:
<engine:UXML ...>
<-- 类名叫Template, 路径src为...., Element的名字为Portrait, 感觉这里是创建了一个模板的类 -->
<engine:Template src="/Assets/Portrait.uxml" name="Portrait"/>
<engine:VisualElement name="players">
<-- Instance代表模板的示例, 后面template后面是类名, 然后根据name创建具体的Instance -->
<engine:Instance template="Portrait" name="player1"/>
<engine:Instance template="Portrait" name="player2"/>
</engine:VisualElement>
</engine:UXML>
总结来说,就是使用Template
和Instance
关键字,可以在UXML里使用别的UXML里创建的class
Overriding UXML attributes
即使基于UXML Template创建了Instance,还是可以override其elements里默认的Attribute的值。
具体操作如下,要写一行xml语句指名下面的内容:
举个例子,看下面这段代码:
<-- 由于override的是Instance不是Template, 所以可以输入多个参数,比如这里输入
两个参数:一个是类名,一个是Element的名字,满足这两个条件的Element, 其text的attribute都会被Override -->
<AttributeOverrides element-name="player-name-label" text="Alice" />
再举一个例子,假设有不同的玩家,他们都要展示相同的Template,但是每个人具体的数值不同:
<-- 指明namespace -->
<UXML xmlns="UnityEngine.UIElements">
<-- 其实是UnityEngine.UIElements.Label -->
<-- 创建两个Label, 名字分别为player-name-label和player-score-label -->
<Label name="player-name-label" text="default name" />
<Label name="player-score-label" text="default score" />
</UXML>
在创建完模板后,可以创建其Instance,然后override它的attributes,其实就是语法上的学习,没什么难度:
<-- 添加两个namespace的include -->
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<-- 声明使用的模板和路径 -->
<Template src="MyTemplate.uxml" name="MyTemplate" />
<-- 基于名为MyTemplate模板创建Instance -->
<Instance name="player1" template="MyTemplate">
<-- Override两个element的text对应的attribute -->
<AttributeOverrides element-name="player-name-label" text="Alice" />
<AttributeOverrides element-name="player-score-label" text="2" />
</Instance>
<Instance name="player2" template="MyTemplate">
<AttributeOverrides element-name="player-name-label" text="Bob" />
<AttributeOverrides element-name="player-score-label" text="1" />
</Instance>
</UXML>
Overriding multiple attributes
上面的例子都只override了一个attribute,用同样的方法还可以ovverride多个attribute:
<-- ovverride text和tooltip两个attribute -->
<AttributeOverrides element-name="player-name-label" text="Alice" tooltip="Tooltip 1" />
Nesting attribute overrides
When you override attributes in nested templates, the deepest override takes precedence.
UXML文件可以引用别的UXML文件和USS文件
其中,<Template>
和Style
两种Element可以接受src
或者path
的attribute,二者有些许差别。
src
存的是相对路径,要么是相对于Project Root路径,要么是相对于所在的UXML文件的路径。举个例子,我的UXML文件在Assets\Editor\UXML下,USS文件在Assets\Editor\USS下:
src="../USS/styles.uss"
,如果要读取别的UXML文件,那么src="template.uxml"
src="/Assets/Editor/USS/styles.uss"
or src="project:/Assets/Editor/UXML/template.uxml"
.path
path只支持在Resources或者Editor的Resouces下的文件夹的文件:
path="template"
代表Assets/Resources/template.uxml
。path="template.uxml"
代表Assets/Editor Default Resources/template.uxml.
很简单,记录下写法:
// 写法一
var template = EditorGUIUtility.Load("path/to/file.uxml") as VisualTreeAsset;
// 这里的parentElement, 可以是EditorWindow下的rootVisualElement
template.CloneTree(parentElement, slots);
// 写法二
var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("path/to/file.uxml");
template.CloneTree(parentElement, slots);
实际使用的时候大概是这样:
public class MyWindow : EditorWindow {
[MenuItem ("Window/My Window")]
public static void ShowWindow () {
EditorWindow w = EditorWindow.GetWindow(typeof(MyWindow));
VisualTreeAsset uiAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/MyWindow.uxml");
VisualElement ui = uiAsset.CloneTree(null);
w.rootVisualElement.Add(ui);
}
void OnGUI () {
// Nothing to do here, unless you need to also handle IMGUI stuff.
}
}
UQuery
UQuery是Unity实现的自己版本的JQuery/Linq,可以使用UQuery获取VisualElement的子节点Tree里特定的Element,示例代码如下:
// 查找第一个叫foo的Button
root.Query<Button>("foo").First();
// 对每个叫foo的Button做...
root.Query("foo").Children<Button>().ForEach(//do stuff);
总结了UnityEngine.UIElements和UnityEditor.UIElements命名空间下可以用 的UXML Element:
基本的Element
就两种:
public class BindableElement : VisualElement, IBindable
Utilities
提供的常用的UI Element有:
focus-index
和focusable
两个Attribute这些Element都是在UnityEngine.UIElements下
Templates
一共三种:
太多了,自己看吧。。。。
https://docs.unity3d.com/2021.2/Documentation/Manual/UIE-ElementRef.html
每个Visual Element都有一个style属性,可以使用USS文件来定义它的UI,规则如下:
Style Rule
我理解的就是语法规则,如下所示:
selector {
property1:value;
property2:value;
}
Attaching USS to visual elements
AssetDatabase.Load()
或Resources.Load()
加载文件,使用VisualElement.styleSheets.Add()
添加stylesheetStyle matching with rules
StyleSheet可以直接添加到一个Visual Tree上,它会自动去匹配:
/* 自动匹配叫做Button的Visual Element */
Button {
width: 200px;
}
参考:https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-USS-Selectors-Complex.html
USS Selector负责根据uss文件里的内容名字,找到对应匹配的Style Rule,在我理解,Selector本质就是一些语法,通过不同的语法,可以实现uss里的Style Rule能应用到指定的Visual Element上
常见的写法:
#name{} Button{} .classlist{} // 两个selector直接组合, 代表层级关系 #container1 .yellow // 名为container1的带.yellow的selector的子element { background-color: yellow; } // 直接父element为selector1,且自身element为selector2 selector1 > selector2 {...} // 满足任意一个selector的element,相当于 或 操作 selector1, selector2 {...}// 不太确定, 后面加不加空格 // 既满足selector1, 也满足selector2,相当于 与 操作 selector1selector2 {...} // *代表所有的element selector1 > * > selector2{..}
PS:奇怪的是,如果我想要同时满足两个selector,在uss里应该是selector1selector2
,跟css是一样的,中间没有空格,但是我试了一下,好像会失败(应该是我的问题),我后面想了别的办法,比如通过代码添加selector和删除selector
之前想在uss里写类似if else的判断,为了适配Unity不同的风格,因为Unity有白色和黑色两种风格,但是后来发现好像不可以写。但是我通过两种selector,可以模拟条件判断,比如我在代码里根据EditorGUIUtility.isProSkin
添加whiteselector和blackselector,就可以进行区分了
// 删除一整个数组的UI Element
for (int i = 0; i < modelAreasUI.Count; i++)
{
modelAreasUI[i].parent.Remove(modelAreasUI[i]);
}
modelAreasUI.Clear();
XML的Element可以拥有Attribute,二者是从属关系,比如下面的
<person gender="female">
里的person是Element,而gender是Attribute
再看两个例子:
<!-- 第一个例子 -->
<person gender="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
<!-- 第二个例子 -->
<person>
<gender>female</gender>
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
第一个例子里,gender是Attribute,第二个例子里,gender是element
参考来源:https://www.w3schools.com/xml/schema_intro.asp
https://www.differencebetween.com/difference-between-xml-and-vs-xsd/
schema翻译过来是模式、概要和议程。在计算机术语里,schema经常用于描述不同类型的数据的structure,最通用的就是数据和XML的schemas。
An XML Schema describes the structure of an XML document. The XML Schema language is also referred to as XML Schema Definition (XSD). 如下所示是一个XSD的例子:
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="note">
<xs:complexType>
<xs:sequence>
<xs:element name="to" type="xs:string"/>
<xs:element name="from" type="xs:string"/>
<xs:element name="heading" type="xs:string"/>
<xs:element name="body" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
核心在于,xml schema旨在定义XML文档本身的结构和内容,xml和xml schema的区别,也可以认为是XML和XSD的区别。在我理解,比如说xml里的node的节点关系,element可以添加attribute这些,应该都是schema来设置的。
参考链接:https://css-tricks.com/snippets/css/a-guide-to-flexbox/
在VisualElement里有一个Property:
StyleFloat flexGrow: Specifies how much the item will grow relative to the rest of the flexible items inside the same container.
本质上flexGrow是一个float值,这个概念源于Flexbox Layout
,用于为那些尺寸不确定、或者说是动态的Box进行布局的分配,其核心在于,在一个固定尺寸的Container里,如何灵活的变化里面的Box的尺寸,让他们能布局在Container里
The Flexbox Layout (Flexible Box) module (a W3C Candidate Recommendation as of October 2017) aims at providing a more efficient way to lay out, align and distribute space among items in a container, even when their size is unknown and/or dynamic (thus the word “flex”).
如下图所示,是Flexbox的相关概念:
具体有以下概念:
flex-direction
属性uss或者说css相关的layout的代码,根据作用的对象,可以分为两种,由于Visual Element,往往是Parent作为所有Children的容器,所以这里分为:
display
如下所示,可以定义一个允许子节点灵活变化的容器:
/* 可以选择flex或者inline-flex */
.container {
display: flex; /* or inline-flex */
}
flex-direction
决定了main-axis的方向,也就是容器里的元素排列的方向,一共四种:左到右、右到左、上到下、下到上
.container {
flex-direction: row | row-reverse | column | column-reverse;
}
如下图所示:
flex-wrap
正常情况下,flex container里的flex items会尽量放到一行(或一列),这里可以通过flex-wrap设置,允许它在需要的时候放到多行
.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}
flex-flow
它是flex-direction和flex-wrap的总体简称,默认的就是row nowrap:
/* main axis沿竖直方向, 而且有wrap */
.container {
flex-flow: column wrap;
}
justify-content
This defines the alignment along the main axis. 还有一些定义,可以定义main axis上的flex items对齐的一些方法,如下图所示:
代码如下:
.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}
align-items
This defines the default behavior for how flex items are laid out along the cross axis on the current line. 前面决定的是flex items沿着main axis的对齐,这里指的是flex items沿着cross axis的对齐,如下图所示,main-aixs是横向的,cross axis是纵向的:
写法如下:
.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}
align-content
感觉跟align-items很像,如下图所示:
代码如下:
.container {
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}
前面提到的flex属性都是针对flex container的,用于调整里面的元素的layout,下面介绍用于container里面具体的item的property
order
flex item有个属性叫order,用于确定其排序,如下图所示:
.item {
order: 5; /* default is 0 */
}
flex-grow
This defines the ability for a flex item to grow if necessary. 其实就是在它所有的兄弟里面,它试图占有的权重值,如下图所示,权重为2的,长度也是2倍,如果所有的flex item的flex-grow都是1,那么他们的长度还会是一样的:
.item {
flex-grow: 4; /* default 0 */
}
flex-shrink
如果有必要的话,一个flex item会收缩
.item {
flex-shrink: 3; /* default 1 */
}
flex-basis
代表元素被分配尺寸之前的默认尺寸,代码如下:
.item {
flex-basis: | auto; /* default auto */
}
flex
flex-grow(子节点扩大权重)、flex-shrink(允许收缩的程度)和flex-basis(基本默认尺寸)这三个属性的总体简称,代码如下:
/*It is recommended that you use this shorthand property rather than set the individual properties. The shorthand sets the other values intelligently.*/
.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
align-self
自定义一个元素的alignment:
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
// 获得Visual Element的实际尺寸
element.resolvedStyle.width //(recommended)
element.resolvedStyle.height //(recommennded)
element.worldBound //(relative to the EditorWindow)
element.transform
element.layout
但是要注意一点,这些参数都不会在第一帧创建对应的element之后马上生效,而是需要等待Unity计算每个元素的size和position之后,才可以生效。
如果想要在该值可用后的第一时间读取该值,可以在该元素上登记GeometryChangeEvent回调函数
在代码里看到了这个类,主要是API方面,类的定义如下:
// 此类的实例表示一个Visual Element的Tree, 这个Tree是从UXML文件里读取出来的
// 在UXML文件里, 每一个Node(xml概念里的Node)都代表一个VisualElementAsset
public class VisualTreeAsset : ScriptableObject
{
public VisualTreeAsset();
...
}
其实这个类就是帮助从UXML文件里,得到对应的Visual Element的,代码如下所示:
VisualTreeAsset template = EditorGUIUtility.Load("Assets/TrainningDataViewer.uxml") as VisualTreeAsset;
VisualElement root = template.CloneTree();
如下图所示,在UnityC#的源码里去引用得到的:
分为两种,一类是在UnityEditor下用到,这里提到的Inserter、SelectionDropper、ShortcutHandler和ContentZoomer都是在GraphView的Namespace里提供的,而MouseManipulator是Unity UI Elements命名空间下的。
继承MouseManipulator的有:
其中,ElementResizer、ClickSelector、ContentDragger、Dragger、EdgeConnector、EdgeManipulator、FreehandSelector和RectangleSelector都是在GraphView的命名空间下的
其实在UI Samples里都有介绍,代码如下:
// 在uxml里加入Enum Field(也可以在代码里加入)
<uie:EnumField label="MyEnum" value="2D" name="MyEnum"/>
// 在C#脚本里
enum MyEnum
{
One,
Two
}
var enumField = rootVisualElement.Q<EnumField>("MyEnum");
enumField .Init(MyEnum.One);// 初始值
enumField .value = MyEnum.Two;// 再设别的值
参考链接:https://docs.unity3d.com/Packages/com.unity.ui@1.0/api/UnityEditor.UIElements.PopupField-1.html
构造函数的接口:
public PopupField(string label, List<T> choices, T defaultValue, Func<T, string> formatSelectedValueCallback = null, Func<T, string> formatListItemCallback = null)
但这玩意儿好像没有对应的Factory代码,无法在UXML里使用,实际好像只能在C#里使用:
List<string> s = new List<string>();
s.Add("321");
s.Add("11");
var ClipsField = new PopupField<string>("Choose Clips", s, "11");
Add(ClipsField);// 加到一个Visual Element里
效果如下图所示,跟EnumField有点像:
可以通过下面的方式直接进行选择:
// 相当于点选第21个choice
ClipsField.index = 20;
可以通过反射,获取里面的choices:
_graphFields = new PopupField<string>(graphNames, 0);
Type popupFieldType = typeof(PopupField<string>);
FieldInfo fieldInfo = popupFieldType.GetField("m_Choices",
BindingFlags.NonPublic | BindingFlags.Instance);
var _graphFieldChoices = (List<string>)fieldInfo.GetValue(_graphFields);
参考:https://forum.unity.com/threads/any-good-way-to-find-out-if-a-keyboard-key-is-pressed-with-the-mouse-over-a-visualelement.1063190/
相关的event可以写在MouseManipulator类里,不过使用之前,一定要注意,这里的Keyboard Event只对focused element起作用,所以要保证:
focusable = true;
然后还要保证接受的Element处于focused状态
这种判断UI Element的UI状态的,Unity里叫做Pseudo-classes,有这么几种:
举个例子:
-- 连续用两个: 来表示两个状态的与
Toggle:checked:hover
{
background-color: yellow;
}
如下图所示:
再比如我自己创建的继承于Button的类:
AnimClipButton:hover
{
background-color: rgba(99, 99, 99, 255);
}
// 注意普通的 background-color也要写在这里,不要用脚本控制,不然脚本控制颜色的优先级永远高于stylesheets
// 也不要直接更改Button类的background-color,可能跟Unity对Button自身的Stylesheets起冲突
AnimClipButton
{
background-color: rgba(56, 56, 56, 255);
}
样子是这样:
我还尝试在自己继承的Button类上面加,focus的代码,这么写:
// cs文件里
myBtn.focusable = true
// uss文件里
MyButton:focus
{
background-color: rgba(99, 99, 99, 255);
}
但是没有效果,可能是只有继承了Unity的UI Element的Focusable类才可以:
比如说我在我按住Ctrl键的时候,滚轮,不想让Unity的ScrollView自己滚动,我做了这个代码:
// MouseManipulator里 private void OnKeyDown(KeyDownEvent evt) { bool _ctrlPressed = evt.keyCode == KeyCode.LeftControl; if (_ctrlPressed) { target.CaptureMouse(); // 按住Ctrl时, 禁止ScrollView的滚动, _wndRef是manipulator存的EditorWnd的引用, 其实就是target _wndRef.SwitchScrollViewScroll(false); } } private void OnKeyUp(KeyUpEvent evt) { if (evt.keyCode == KeyCode.LeftControl) { _wndRef.SwitchScrollViewScroll(true); target.ReleaseMouse(); } } // EditorWindow里, 它存了对应的ScrollView的引用 // 一种Trick, 用于在滚动鼠标滑轮时禁止ScrollView滚动 internal void SwitchScrollViewScroll(bool enabled) { if (enabled) _MyScrollView.verticalPageSize = 20.0f;// 默认Unity的值为20.0f else _MyScrollView.verticalPageSize = 0.0f; }
代码如下:
private void _LoadTemplate() { _verticalScroller = _myScrollView.Q<Scroller>(classes: "unity-scroller--vertical"); } internal void ScrollByArrowKey(bool up) { if (up) _verticalScroller.ScrollPageUp(); else _verticalScroller.ScrollPageDown(); } internal void ScrollByPageKey(bool up) { if (up) _verticalScroller.ScrollPageUp(_verticalClipsAreaScroller.slider.pageSize); else _verticalScroller.ScrollPageDown(_verticalClipsAreaScroller.slider.pageSize); }
看了下Unity的C#源码,感觉即使没有这个ScrollPageDown的接口,这么写也不是不行:
// 其实就是把ScrollPageUp的底层代码拿出来而已
float factor = inputValue;
var slider = _verticalSlider;
if (up)
slider.value -= factor * (slider.pageSize * (slider.lowValue < slider.highValue ? 1f : -1f));
else
slider.value += factor * (slider.pageSize * (slider.lowValue < slider.highValue ? 1f : -1f));
List<VisualElement> hierarchyItems = wnd._hierarchyPane.Query<VisualElement>(classes: "HierarchyItem").Build().ToList();
参考:https://forum.unity.com/threads/setting-the-binding-path-to-a-value-inside-a-struct.839182/
参考:https://docs.unity3d.com/2019.3/Documentation/Manual/UIE-Binding.html
bindings的概念是把UI的Element与实际的SerializedProperty绑定到一起,从而我修改UI的值可以改变对应的数据。PropertyField是Unity提供的VisualElement的子类,其实就是一个带了数据的VisualElement,相当于数据的Wrapper,部分代码如下:
public class PropertyField : VisualElement, IBindable
{
public PropertyField();
public PropertyField(SerializedProperty property);
public PropertyField(SerializedProperty property, string label);
public IBinding binding { get; set; }
public string bindingPath { get; set; }
public string label { get; set; }
}
其bindingPath,其实也就是Property在SerializedProperty里的Path,示例代码如下:
using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; public class BindableObject : ScriptableObject { public int intField = 5; public bool boolField = true; public float floatField = 0.6f; public string stringField = "some text."; public Color colorField = Color.green; } public class MyBoundWindow : EditorWindow { [MenuItem("UIElements/MyBoundWindow")] public static void ShowWindow() { MyBoundWindow wnd = GetWindow<MyBoundWindow>(); wnd.titleContent = new GUIContent("MyBoundWindow"); } public BindableObject m_BindableObject = null; public void OnEnable() { m_BindableObject = ScriptableObject.CreateInstance<BindableObject>(); var serializedObject = new SerializedObject(m_BindableObject); // Each editor window contains a root VisualElement object VisualElement root = rootVisualElement; // 获取m_BindableObject对象的每个可序列化的Property, 为其创建PropertyField SerializedProperty property = serializedObject.GetIterator(); // true参数为enterChildren, 对于第一个绘制的对象(serializedObject)而言, 其children // 也就是其内部数据, 需要被绘制出来, 所以传入true property.NextVisible(true); do { // Create the UIElements PropertyField. var uieDefaultProperty = new PropertyField(property); root.Add(uieDefaultProperty); } while (property.NextVisible(false)); // Also, 额外再绘制一次intField, 这里的bindingPath是intField property = serializedObject.FindProperty("intField"); root.Add(new PropertyField(property)); // Also, 额外再绘制一次intField root.Add(new FloatField() { bindingPath = "floatField" }); root.Add(new PropertyField() { bindingPath = "m_BindableObject" }); // Bind the entire window. root.Bind(serializedObject); } }
绘制结果如下,注意这里的m_BindableObject的默认bindingPath为m_Script
:
Unity版本为2019.4,参考:https://forum.unity.com/threads/register-callback-question.858193/
貌似不支持自定义数据类的值改变的回调,只支持primitive类型的回调,举个例子:
// assetField是一个PropertyField, Property类型为我自定义的ScriptableObject类型
assetField.RegisterCallback<ChangeEvent<string>>(OnSelectedAssetChanged);// 正确, 传入的是asset的名字
assetField.RegisterCallback<ChangeEvent<MyScriptableObject>>(OnSelectedAssetChanged);// 错误, 回调不会产生
不过这样写的话,在OnSelectedAssetChanged
函数里无法直接获取新的ScriptableObject的值,因为这个时间点赋值行为还未产生,但是又无法从string里获取值(可以通过string读取资产,只要保证没有同名资产),所以可以通过EditorApplication.delayCall
,保证赋值后,再进行操作。
比如是个Button:
m_AddBtn = this.Q<Button>("addButton");
m_AddBtn.clicked += OnAddBtnClicked;
private void OnAddBtnClicked()
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("One"), false, MyFuncCallback);
menu.AddSeparator("");
menu.AddItem(new GUIContent("Two"), false, MyFuncCallback);
menu.DropDown(m_AddBtn.worldBound);
}
效果如下图所示:
事件不一定非要在EditorWindow的派生类里的OnGui函数里去处理,也可以在UI Element里处理,应该是处于focus状态下的Element可以接受Event,代码也很简单:
private void AddCallbacksForSampleArea(VisualElement sampleArea)
{
sampleArea.RegisterCallback<DragUpdatedEvent>(DragUpdatedCallback);
sampleArea.RegisterCallback<DragPerformEvent>(DragPerformedCallback);
sampleArea.RegisterCallback<KeyDownEvent>(KeyDownCallback);
sampleArea.RegisterCallback<MouseDownEvent>(MouseDownCallback);
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。