UnityEditor 中制作和 Console 窗口一样可拖动的 Splitter 控件

前言

分享一个大佬写的 Splitter 控件, 主要是实现了和 Unity 原生 Console 窗口一样的效果, 有一条可以拖动的分割线, 用于将一个窗口分割成两部分.

这就是最终实现的效果, 看一下是否满足你的需要, 如果正好满足, 那你就可以继续往下看了.

Splitter

代码结构

主要由一个公共基类和一个自定义的窗口类构成.

基类: Splitter

基类主要负责的是控制分割条的拖动, 以及重新计算拖动分割条之后主区域和子区域的尺寸.

下面是源码, 本质就只是一个 OnGUI 方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using UnityEngine;
using UnityEditor;
using System;

namespace Kuroha.GUI.Editor.Splitter
{
[Serializable]
public abstract class Splitter
{
internal enum SplitMode
{
Horizontal,
Vertical
}

// 不允许子类访问的字段
private EditorWindow editorWindow;
private MouseCursor mouseCursor;
private SplitMode splitMode;
private float lockSize;
private bool isResizing;
private bool isFreeze;

// 需要子类访问的字段
protected float barSize;
protected float mainAreaSize;

/// <summary>
/// 可触发鼠标变化的区域, 即分割条的全部有效区域
/// </summary>
private Rect mouseCursorRect;

/// <summary>
/// 主区域占整个窗口的比例
/// </summary>
private float mainAreaRatio = 0.5f;

/// <summary>
/// 专业版: 分割条的颜色
/// </summary>
private static readonly Color splitterColorPro = Color.black;

/// <summary>
/// 免费版: 分割条的颜色
/// </summary>
private static readonly Color splitterColorFree = Color.gray;

/// <summary>
/// 构造函数
/// </summary>
/// <param name="window">使用当前布局的窗口</param>
/// <param name="splitMode">分割方式, 分为上下切分和左右切分</param>
/// <param name="mainAreaSize">主区域的默认大小, 上下切分时为默认高度, 左右切分时为默认宽度</param>
/// <param name="minSize">主区域的最小大小, 上下切分时为最小高度, 左右切分时为最小宽度</param>
/// <param name="barSize">分割条的有效大小, 即鼠标放置时会变化的区域, 上下切分时为有效高度, 左右切分时为有效宽度</param>
/// <param name="isFreeze">是否冻结分割线 (不允许滑动调整范围)</param>
internal Splitter(EditorWindow window, SplitMode splitMode, float mainAreaSize, float minSize, float barSize, bool isFreeze)
{
editorWindow = window;
this.mainAreaSize = mainAreaSize;
this.splitMode = splitMode;
lockSize = minSize;
this.barSize = barSize;
this.isFreeze = isFreeze;
mouseCursor = this.splitMode == SplitMode.Vertical
? MouseCursor.ResizeHorizontal
: MouseCursor.ResizeVertical;
}

/// <summary>
/// 主窗口
/// </summary>
/// <param name="rect">区域矩形</param>
/// <returns></returns>
protected abstract Rect MainRect(Rect rect);

/// <summary>
/// 子窗口
/// </summary>
/// <param name="rect">区域矩形</param>
/// <returns></returns>
protected abstract Rect SubRect(Rect rect);

/// <summary>
/// 分割条的全部区域
/// </summary>
/// <param name="rect">区域矩形</param>
/// <returns></returns>
protected abstract Rect BarRect(Rect rect);

/// <summary>
/// 分割条的无色区域
/// 默认分割条为 16 像素, 这里设置顶部 7 像素和 底部 8 像素都不显示, 仅显示中间的 1 个像素.
/// 但是整个厚度为 16 像素的区域都可以触发鼠标变化, 可以触发拖拽.
/// </summary>
protected abstract RectOffset BarRectOffset();

/// <summary>
/// 绘制界面
/// </summary>
/// <param name="windowRect"></param>
/// <param name="mainGUI"></param>
/// <param name="subGUI"></param>
public void OnGUI(Rect windowRect, Action<Rect> mainGUI, Action<Rect> subGUI)
{
var current = Event.current;

// 绘制主区域内容
mainGUI(MainRect(windowRect));

// 绘制子区域内容
subGUI(SubRect(windowRect));

// 分割条全部有效区域 (可触发鼠标变化的整个区域, 外观上部分不显示)
mouseCursorRect = BarRect(windowRect);
EditorGUIUtility.AddCursorRect(mouseCursorRect, mouseCursor);

if (isFreeze == false)
{
// 单个区域的最大大小
var clampMax = splitMode == SplitMode.Vertical ? windowRect.width - lockSize : windowRect.height - lockSize;

// 整个区域的最大大小 (两个区域之和, 即整个显示区域的大小)
var targetSplitterValue = splitMode == SplitMode.Vertical ? windowRect.width : windowRect.height;

// 主区域占整个区域的比例
mainAreaRatio = splitMode == SplitMode.Vertical ? mainAreaSize / windowRect.width : mainAreaSize / windowRect.height;

// 鼠标点击了分割条
if (current.type == EventType.MouseDown)
{
if (mouseCursorRect.Contains(current.mousePosition))
{
isResizing = true;
}
}

// 鼠标松开
if (current.type == EventType.MouseUp)
{
isResizing = false;
}

// 鼠标按住分割条并滑动
if (isResizing)
{
if (current.type == EventType.MouseDrag)
{
var targetValue = splitMode == SplitMode.Vertical ? current.mousePosition.x : current.mousePosition.y;
var diffValue = splitMode == SplitMode.Vertical ? windowRect.width : windowRect.height;
mainAreaRatio = targetValue / diffValue;
}
}
else if (current.type != EventType.Layout && current.type != EventType.Used)
{
mainAreaRatio = targetSplitterValue * mainAreaRatio / targetSplitterValue;
}

// 计算主区域大小
mainAreaSize = Mathf.Clamp(targetSplitterValue * mainAreaRatio, lockSize, clampMax);
}

// 绘制分割条
var color = EditorGUIUtility.isProSkin ? splitterColorPro : splitterColorFree;

// API: RectOffset.Remove(rect) => 从指定的 rect 中移除 RectOffset 偏移
EditorGUI.DrawRect(BarRectOffset().Remove(mouseCursorRect), color);

// 即时刷新
if (isResizing)
{
editorWindow.Repaint();
}
}
}
}

自定义窗口类

自定义窗口类就是我们实际构建的窗口, 它需要继承自基类 Splitter, 并且重写基类中的 4 个区域方法: MainRect, SubRect, BarRect, BarRectOffset.

下面是动图中横向分割窗口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using UnityEditor;
using UnityEngine;
using System;

namespace Kuroha.GUI.Editor.Splitter
{
[Serializable]
public class HorizontalSplitter : Splitter
{
/// <summary>
/// 分割条的无色区域
/// 默认分割条为 16 像素, 这里设置顶部 7 像素和 底部 8 像素都不显示, 仅显示中间的 1 个像素.
/// 但是整个厚度为 16 像素的区域都可以触发鼠标变化, 可以触发拖拽.
/// </summary>
private static RectOffset barRectOffset;

/// <summary>
/// 分割条大小
/// </summary>
private const int BAR_SIZE = 16;

/// <summary>
/// 构造函数
/// </summary>
/// <param name="window">使用当前布局的窗口</param>
/// <param name="mainSize">主区域的默认大小, 上下切分时为默认高度, 左右切分时为默认宽度</param>
/// <param name="minSize">主区域的最小大小, 上下切分时为最小高度, 左右切分时为最小宽度</param>
/// <param name="isFreeze">是否冻结分割线 (不允许滑动调整范围)</param>
public HorizontalSplitter(EditorWindow window, float mainSize, float minSize, bool isFreeze)
: base(window, SplitMode.Horizontal, mainSize, minSize, BAR_SIZE, isFreeze) { }

/// <summary>
/// 分割条的无色区域
/// </summary>
/// <returns></returns>
protected override RectOffset BarRectOffset()
{
return barRectOffset ??= new RectOffset(0, 0, 7, 8);
}

/// <summary>
/// 主区域
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect MainRect(Rect rect)
{
return new Rect(rect)
{
x = 0,
y = 0,
height = mainAreaSize
};
}

/// <summary>
/// 子区域
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect SubRect(Rect rect)
{
return new Rect(rect)
{
x = 0,
y = mainAreaSize + 5,
height = rect.height - mainAreaSize - 15
};
}

/// <summary>
/// 分割条
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect BarRect(Rect rect)
{
return new Rect(rect)
{
x = 0,
y = mainAreaSize - barSize / 2,
height = barSize
};
}
}
}

下面是动图中纵向分割窗口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using System;
using UnityEditor;
using UnityEngine;

namespace Kuroha.GUI.Editor.Splitter
{
[Serializable]
public class VerticalSplitter : Splitter
{
/// <summary>
/// 分割条的无色区域
/// 默认分割条为 16 像素, 这里设置顶部 7 像素和 底部 8 像素都不显示, 仅显示中间的 1 个像素.
/// 但是整个厚度为 16 像素的区域都可以触发鼠标变化, 可以触发拖拽.
/// </summary>
private static RectOffset barRectOffset;

/// <summary>
/// 分割条大小
/// </summary>
private const int BAR_SIZE = 16;

/// <summary>
/// 构造函数
/// </summary>
/// <param name="window">使用当前布局的窗口</param>
/// <param name="mainSize">主区域的默认大小, 上下切分时为默认高度, 左右切分时为默认宽度</param>
/// <param name="minSize">主区域的最小大小, 上下切分时为最小高度, 左右切分时为最小宽度</param>
/// <param name="isFreeze">是否冻结分割线 (不允许滑动调整范围)</param>
public VerticalSplitter(EditorWindow window, float mainSize, float minSize, bool isFreeze)
: base(window, SplitMode.Vertical, mainSize, minSize, BAR_SIZE, isFreeze) { }

/// <summary>
/// 分割条的无色区域
/// </summary>
/// <returns></returns>
protected override RectOffset BarRectOffset()
{
return barRectOffset ??= new RectOffset(7, 8, 0, 0);
}

/// <summary>
/// 主区域
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect MainRect(Rect rect)
{
return new Rect(rect)
{
x = 0,
y = 0,
width = mainAreaSize
};
}

/// <summary>
/// 子区域
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect SubRect(Rect rect)
{
return new Rect(rect)
{
x = mainAreaSize + 5,
y = 0,
width = rect.width - mainAreaSize - 15
};
}

/// <summary>
/// 分割条
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
protected override Rect BarRect(Rect rect)
{
return new Rect(rect)
{
x = mainAreaSize - barSize / 2,
y = 0,
width = barSize
};
}
}
}

应用

最后再分享一个我实际项目中的应用:

Asset Check Tool

参考链接

Console Window で利用されているような Splitter を作る