第一部分,做出如下界面并实现相关功能,添加删除,是否确认完成。
实现步骤:
这个功能实现起来很简单,创建一个类,有一个数据的成员(此处用最简单的List)。调用获取界面的API(创建页面的方法),然后在OnGUI方法中(可以先理解为update)根据List调用GUILayout一系列API即可实现(绘制的相关方法)。
//调用窗口API
var window = GetWindow<T>(true);
//按钮API
if (GUILayout.Button(Text))
{
OnClickEvent.Invoke();
}
//文本输入框API
Text = EditorGUILayout.TextArea(Text);
//单选框API
bool = GUILayout.Toggle(bool, Text);
只要在OnGUI调用上述API即可完成界面的绘制,但是这样不易维护,而且属于面向过程编程,所以对上述内容进行封装一下。
代码规范优化(做一个渲染框架):
先创建一个IView的接口
public interface IView
{
//显示方法 功能有显示完成和未完成,Show和Hide控制
void Show();
//隐藏方法
void Hide();
//渲染方法
void DrawGUI();
void RemoveFromParent();
ILayout Parent { get; set; }
}
创建一个抽象类View继承与IView,把需要实现在OnGUI的方法留给子类去实现。
public abstract class View : IView
{
//是否可见,显示完成和未完成功能的判断参数
public bool Visible { get; set; } = true;
public void Show()
{
Visible = true;
}
public void Hide()
{
Visible = false;
}
//调用渲染的方法
public void DrawGUI()
{
if(Visible)
{
OnGUI();
}
}
//渲染
protected abstract void OnGUI();
public ILayout Parent { get; set; }
public void RemoveFromParent()
{
Parent.RemoveChild(this);
}
}
ButtonView:点击按钮,有一个string参数显示文本以及点击事件
public class ButtonView : View
{
public ButtonView(string text,Action onClickEvent)
{
Text = text;
OnClickEvent = onClickEvent;
}
//显示的文本
public string Text { get; set; }
//触发的点击事件
public Action OnClickEvent { get; set; }
protected override void OnGUI()
{
if (GUILayout.Button(Text))
{
OnClickEvent.Invoke();
}
}
}
CustomView:自定义,只有一个事件,用来添加装饰的代码,例如GUILayout.BeginVertical("box");
public class CustomView : View
{
public CustomView(Action onGUIAction)
{
OnGUIAction = onGUIAction;
}
public Action OnGUIAction { get; set; }
protected override void OnGUI()
{
OnGUIAction.Invoke();
}
}
SpaceView:添加空白处,有一个Int参数以及添加空白的方法写在OnGUI中
public class SpaceView : View
{
public int Pixel { get; set; } = 10;
public SpaceView(int pixel=10)
{
Pixel = pixel;
}
protected override void OnGUI()
{
GUILayout.Space(Pixel);
}
}
TextAreaView:文本输入框,有一个Property的数据工具类,后续会详细介绍
public class TextAreaView : View
{
public TextAreaView(string content)
{
Content = new Property<string>(content);
}
public Property<string> Content { get; set; }
protected override void OnGUI()
{
//EditorGUILayout相比GUILayou支持显示中文
Content.Value = EditorGUILayout.TextArea(Content.Value);
}
}
ToggleView:单选框,有一个string参数显示文本,以及Property数据工具类,后续会详细介绍
public class ToggleView : View
{
public ToggleView(string text,bool initValue = false)
{
Text = text;
Toggle = new Property<bool>(initValue);
}
public string Text { get; set; }
public Property<bool> Toggle { get; private set; }
protected override void OnGUI()
{
Toggle.Value = GUILayout.Toggle(Toggle.Value, Text);
}
}
Property:数据工具类。泛型,有一个Value值,一个Action 的委托,一个委托的List(用来解除绑定),一个绑定委托的方法,一个解除绑定的方法。TextAreaView输入参数的更改以及ToggleView点击完成之后更改的事件都是写在这个数据类型中。
[Serializable]
public class Property<T>
{
public Property()
{
}
//第一次初始化使用,即第一次不管Value是否改变都必定触发绑定的委托,主要针对第一次显示已完成
private bool setted = false;
public Property(T initValue)
{
mValue = initValue;
}
public T Value
{
get => mValue;
set
{
if (!value.Equals(mValue) || !setted)
{
mValue = value;
mSetter?.Invoke(mValue);
setted = true;
}
}
}
/// <summary>
/// TODO:注销功能也要做下
/// </summary>
/// <param name="setter"></param>
public void Bind(Action<T> setter)
{
mSetter += setter;
mBindings.Add(setter);
}
//不明白
public void UnBindAll()
{
foreach (var binding in mBindings)
{
mSetter -= binding;
}
}
private T mValue = default;
private event Action<T> mSetter;
private Func<T> mGetter => () => mValue;
private List<Action<T>> mBindings { get; set; } = new List<Action<T>>();
}
到这一步,EGOWindow(脚本代码类)中可以创建一个List,每创建一个View时添加,然后写一个在初始化方法中for循环调用每一个View的OnDrawGUI(调用OnGUI)。至此,面向过程改为面向对象,代码变成了一坨一坨的XXView,依旧有改进的空间。
布局:
如图所示,我们需要两种布局,即水平与垂直。
先创建一个ILayout接口。
public interface ILayout:IView
{
void AddChild(IView view);
void RemoveChild(IView view);
}
继承IView,拥有添加子类以及移除子类的方法。
创建一个基类Layout
public abstract class Layout : View, ILayout
{
List<IView> Children = new List<IView>();
Queue<Action> Commands = new Queue<Action>();
public void AddChild(IView view)
{
Children.Add(view);
view.Parent = this;
}
public void RemoveChild(IView view)
{
//牵扯到foreach,异常报错,应该排序执行
Commands.Enqueue(() => { Children.Remove(view); });
}
protected override void OnGUI()
{
OnGUIBegin();
foreach (var child in Children)
{
child.DrawGUI();
}
OnGUIEnd();
while(Commands.Count>0)
{
Commands.Dequeue().Invoke();
}
}
//表示行/列开始
protected abstract void OnGUIBegin();
//表示行/列结束
protected abstract void OnGUIEnd();
}
分别创建水平布局HorizontallLayout与垂直布局VerticalLayout
public class HorizontalLayout : Layout
{
protected override void OnGUIBegin()
{
GUILayout.BeginHorizontal();
}
protected override void OnGUIEnd()
{
GUILayout.EndHorizontal();
}
}
public class VerticalLayout : Layout
{
public string Style { get; set; }
public VerticalLayout(string style = null)
{
Style = style;
}
protected override void OnGUIBegin()
{
if (string.IsNullOrEmpty(Style))
{
GUILayout.BeginVertical();
}
else
{
GUILayout.BeginVertical(Style);
}
}
protected override void OnGUIEnd()
{
GUILayout.EndVertical();
}
}
很简单,实现begin与end方法即可,垂直布局比水平多一个style。
TodoView继承于水平布局,有一个Model参数,以及ToggleView与删除Button。当ToggleView的值改变时,model里的值也会改变(在创建代码中绑定了保存委托),因此点击单选框时就会触发保存功能。具体是图下这部分。
public class TodoView : HorizontalLayout
{
public Todo Model { get; }
public TodoView(Todo model)
{
Model = model;
var toggleView = new ToggleView(Model.Context, Model.Finished.Value);
toggleView.Toggle.Bind(value =>
{
Model.Finished.Value = value;
});
AddChild(toggleView);
var buttonView = new ButtonView("删除", () =>
{
Model.Finished.UnBindAll();
//list中使用移除时,不能用foerach,会影响
ModelLoader.Model.Todos.Remove(Model);
ModelLoader.Model.Save();
this.RemoveFromParent();
});
AddChild(buttonView);
}
}
TodoListView,继承与垂直布局代码,有一个mShowFinished参数,控制完成、未完成按钮的显示,有两个button,有创建TodoView的方法。具体是图下这部分。
public class TodoListView : VerticalLayout
{
private Property<bool> mShowFinished = new Property<bool>(false);
private ButtonView mShowUnFinishedButton = null;
private ButtonView mShowFinishedButton = null;
public TodoListView()
{
AddChild(new SpaceView());
mShowUnFinishedButton = new ButtonView("显示未完成", OnShowUnFinishedBtnClick);
mShowFinishedButton = new ButtonView("显示已完成", OnShowFinishedBtnClick);
mShowFinished.Bind(newValue =>
{
if (newValue)
{
mShowFinishedButton.Hide();
mShowUnFinishedButton.Show();
}
else
{
mShowUnFinishedButton.Hide();
mShowFinishedButton.Show();
}
});
AddChild(mShowUnFinishedButton);
AddChild(mShowFinishedButton);
//layout
foreach (var todo in ModelLoader.Model.Todos)
{
CreateTodoView(todo);
}
AddChild(mTodosParentContainer);
mShowFinished.Value = false;
}
ILayout mTodosParentContainer = new VerticalLayout("box");
public void CreateTodoView(Todo model)
{
var todoView = new TodoView(model);
mTodosParentContainer.AddChild(todoView);
mShowFinished.Bind(showFinished =>
{
if (todoView.Model.Finished.Value == showFinished)
{
todoView.Show();
}
else
{
todoView.Hide();
}
});
}
void OnShowFinishedBtnClick()
{
mShowFinished.Value = true;
}
void OnShowUnFinishedBtnClick()
{
mShowFinished.Value = false;
}
}
TodoListInputView,继承与垂直布局,具体是图下的这部分。有一个文本输入框,添加按钮,以及参数是Todo的委托,在外部赋值,添加的时候会调用。输入框值改变时会同时改变自己的string文本值。
public class TodoListInputView : VerticalLayout
{
public Action<Todo> OnTodoCreate;
private string mInputContent = string.Empty;
public TodoListInputView()
{
Style = "box";
AddChild(new SpaceView());
//var layout = new VerticalLayout("box");
//AddChild(new CustomView(() =>
//{
// GUILayout.BeginVertical("box");
//}));
var inputTextArea = new TextAreaView(mInputContent);
inputTextArea.
Content.
Bind((newContent => mInputContent = newContent));
AddChild(inputTextArea);
AddChild(new ButtonView("添加", () =>
{
if (!string.IsNullOrEmpty(mInputContent))
{
var newTodo = new Todo() { Context = mInputContent };
OnTodoCreate(newTodo);
mInputContent = string.Empty;
}
}));
}
}
现在布局做好了,需要一个控制器去控制它。
ViewController,有一个总的布局。以及创建View的方法。
public abstract class ViewController
{
public VerticalLayout View = new VerticalLayout();
public abstract void SetUpView();
}
TodoListController,拥有TodoListInputView 与TodoListView ,正好是图下的两部分。
绑定了InputView添加时会调用的委托。
public class TodoListController: Framework.ViewController
{
public TodoListInputView InputView { get; set; } = new TodoListInputView();
public TodoListView TodoListView { get; set; } = new TodoListView();
public override void SetUpView()
{
View.AddChild(new CustomView(() =>
{
GUILayout.Toolbar(0, new string[] { "1", "2" });
}));
InputView.OnTodoCreate = (newTodo) =>
{
newTodo.Finished.Bind(_ => ModelLoader.Model.Save());
ModelLoader.Model.Todos.Add(newTodo);
ModelLoader.Model.Save();
//创建一个新的行,即刚刚添加的
TodoListView.CreateTodoView(newTodo);
};
View.AddChild(InputView);
View.AddChild(TodoListView);
}
}
最后就是需要规划的Window类,做一个基类,继承与EditorWindow与IDispose(销毁时候用)。
自身有一个ViewController,有一个Init的方法,Open时调用,Init又会调用Onit虚方法(留给子类实现),OnGUI会调用自身的Viewtroller中的View的子类去调用OnGUI渲染。有一个CreateViewController方法去获取ViewController。
public abstract class Window : EditorWindow,IDisposable
{
public static Window MainWindow { get; protected set; }
public ViewController ViewController { get; set; }
public T CreateViewController<T>()where T: ViewController,new()
{
var t = new T();
t.SetUpView();
return t;
}
public static void Open<T>(string title) where T:Window
{
var window = GetWindow<T>(true);
if (!isShowing)
{
window.titleContent = new GUIContent(title);
window.Init();
isShowing = true;
window.Show();
}
else
{
isShowing = false;
window.Dispose();
window.Close();
}
}
void Init()
{
OnInit();
}
private void OnGUI()
{
ViewController.View.DrawGUI();
}
public void Dispose()
{
OnDispose();
}
protected abstract void OnInit();
protected abstract void OnDispose();
protected static bool isShowing = false;
}
最后是我们需要实现的EGOWindow,继承与Window。
public class EGOWindow : Window
{
[MenuItem("EGO/MainWindow %t")]
static void Open()
{
Open<EGOWindow>("EGO Window");
}
protected override void OnInit()
{
ViewController = CreateViewController<TodoListController>();
}
protected override void OnDispose()
{
}
//子类不能写OnGUI,因为继承,子类再写会直接覆盖
//private void OnGUI()
//{
// //mViews.ForEach(view => view.DrawGUI());
// //ViewController.View.DrawGUI();
//}
}
最终代码只有这么几行,简洁多了。
总结一下运行的流程,EGOWindow 调用Open方法,Open方法中会调用OnInit初始化方法,拿到TodoListViewController,并且执行SetUpView方法以及自身会先创建TodoListInputView与TodoListView,会调用自身默认的构造函数。SetUpView主要是给TodoListInputView添加绑定事件。TodoInputListView调用自身构造函数,创建界面。TodoListView调用自身构造函数,拿到所有的Models,然后循环创建每一个TodoView。至此,界面搭建完成。
关于添加删除功能,添加的时候会调用TodoListView的CreateTodoView方法新创建一条,即可完成创建功能。
删除功能较为麻烦,每个View都有一个Parent,用XXLayout布局去规范他们,即Layout有一个List参数,AddChild时,将自己定义为他们的父类,删除时调用RemovefromParent方法,移除自己,实现则是Children.remove(view),因为Children所有参数都会在OnGUI中调用,所以应该创建一个Avtion队列,将每一个删除Action添加,在OnGUI中最后调用,避免出现空引用的报错。
数据结构升级。
目前只有完成和未完成两种状态,不够完善,希望增加一个进行中的状态,并且能记录完成的时间,首先需要组一个数据结构更新器。
基类:
//参数是旧类型与新类型,将旧类型改为新类型
public abstract class UpdateAction<TOldModel, TNewMoedl>
where TOldModel : class
where TNewMoedl : class
{
public TNewMoedl Result { get; private set; }
public void Execute(object oldValue)
{
Result = ConvertOld2New(oldValue as TOldModel);
}
//子类需要重写的方法
protected abstract TNewMoedl ConvertOld2New(TOldModel oldModel);
}
实现的子类:
public class ModelUpdateCommandV1 : UpdateAction<TodoList, EGO.v1.TodoList>
{
protected override EGO.v1.TodoList ConvertOld2New(EGO.TodoList oldModel)
{
var newTodoList = new EGO.v1.TodoList();
foreach (var oldTodo in oldModel.Todos)
{
var newTodo = new EGO.v1.Todo();
newTodo.Content = oldTodo.Content;
newTodo.State.Value = oldTodo.Finished.Value ? EGO.v1.TodoState.Done : EGO.v1.TodoState.NotStart;
newTodoList.Todos.Add(newTodo);
}
return newTodoList;
}
}
两种数据类型的区别。
namespace EGO.v1
{
[Serializable]
public class TodoList
{
public List<Todo> Todos = new List<Todo>();
}
[Serializable]
public class Todo
{
public string Content;
public DateTime CreateAt = DateTime.Now;
public DateTime FinshedAt;
public DateTime StartTime;
public Property<TodoState> State = new Property<TodoState>(TodoState.NotStart);
public Property<bool> Finished = new Property<bool>();
}
[Serializable]
public enum TodoState
{
NotStart,
Started,
Done,
}
}
namespace EGO
{
[Serializable]
public class TodoList
{
public List<Todo> Todos = new List<Todo>();
}
[Serializable]
public class Todo
{
public string Content;
public Property<bool> Finished = new Property<bool>();
}
}
可以看出,区别不打,所谓升级,就是创建新类型,然后拿到旧类型,对已有存在的值进行初始化。
加载的时候,直接拿到数据进行反序列化,写在tryCatch中,如果有异常,那么则调用更新代码。
public static TModel Load()
{
var todoContent = EditorPrefs.GetString(KEY, string.Empty);
Debug.Log(todoContent);
if (string.IsNullOrEmpty(todoContent))
{
return mModel = new TModel();
}
try
{
mModel = JsonConvert.DeserializeObject<TModel>(todoContent);
}
catch (Exception e)
{
mModel = ModelUpdater.Update<TModel>(todoContent);
}
return mModel ;
}
数据更新非常麻烦,所有用到的数据都需要更改,例如XXView。
新的数据类型中用state取代Finshed,所有原来的代码都需要修改。
已完成未完成希望根据表头修改。
归功于前面做的铺垫,这个功能做起来非常的简单,只需要点击表头的事件绑定ListTodoView的是否显示参数即可。
TodoListView的ShowFished参数控制显示哪些,其实即使把button的事件添加到了toolBar中。
Button按钮希望添加样式。
非常简单,buttonView的构造函数自带。
但是我一定要吐槽一下这个老师讲课,显示在View中增加
又在IView中增加
最后button中还增加
我实在不知道意义何在。这个老师讲课听起来是真的累。
实际效果
给渲染框架提供样式的设置。
View添加两个参数,一个私有,一个提供给子类。一个增加的方法
子类创建的时候会将相关参数传入到创建时GUILaout.XX的API中,如图下的Button。
吐槽:正常来说只需要一个参数就够了,但是此处用了两个,最无语的是将另一个List参数转换为数组赋值给另一个,完全不知道意义所在。
另外创建一个拓展方法,方便调用(此处特指单独设置宽度)
拆分ViewController
目前,脚本中只有一个View,有一个控制参数的值确定显示哪一部分,其实这样非常不规范,此处额外创建一个View脚本,内容和当前TodoListView基本一样,不过一个只显示已完成,一个显示未完成,然后又toolbar来控制显示哪一个View。
此处较为简单,就不贴代码了。
添加正在进行状态。
改动,改为三个按钮加上label,而不是单独的toggleview,通过点击按钮更改状态,并且确定显示哪一个按钮。并执行相应的事件。
实时更新每个任务的状态,即,我点击了完成,当前页面就不应该显示。
这个改动简单,只要将自己隐藏即可。但是考虑到框架的问题,从跟上需要加上一个Refresh的方法。
吐槽:
此处todoView的Refresh方法应该是调用hide将自己隐藏,但是作者脑子抽了,非要将自己从父类移除,切回来可能会出问题(如果对状态进行多次更改),并且还牵扯到执行顺序加了一堆逻辑,后续还有改动,自出暂时不贴代码,不得不吐槽这个作者,真的是自己一边做一边讲,经常带进歪路。
已完成按照 完成的日期显示,很简单的小任务,对List进行排序即可。
计算每天用时,没有什么意义的功能,先按照GroupBy对List进行分组,然后计算时间即可。
完成的事项要显示完成的时间,没难度,多年加一个labelView传入相关参数即可。以上是没什么难度,调用相关API执行相关事件即可。
为Todo设置优先级。
这个任务需要用到一个新的组件EditorGUILayout.EnumPopup
本质上没有难度,在Horizontal布局里添加一下即可。
实时刷新功能。
这个功能非常重要,目前来说,有已完成列表界面与未完成列表界面。
目前只是实现了数据的更改保存,但是界面上不会更改,需要重新打开一次才行,这明显是不合理的,因此需要做一个刷新的功能。
首先第一个问题:代码中用到了forearch,foreach的数据在使用的时候是不能进行增加或者删减的,否则会报错,所以首先我们需要在改动数据之后下一帧再执行。
因此在基类中新增加一个方法
Command是List,会再OnGUI渲染完当前帧所有事物(即foreach已完成对数据的使用)后调用。
回到TudoListView中,对构造函数进行更改。
切换界面的时候会调用OnShow的方法,OnShow中会调用下一帧刷新方法
至此,刷新功能完成。但是有非常重要的一点需要注意!刷新界面会导致一些事件失效,因为每一次更改都会刷新界面,刷新界面时会清楚原本所有的东西全部重新渲染,包括绑定的事件,所以需要将一些事件的内容写在刷新的方法内,例如
原本是数据改动之后再判断是什么级别,但是因为改动之后内容已经全部清空,又会变回A,所以需要直接将判断写在方法中。至此,刷新功能完成。
注意点:渲染顺序的冲突解决。
分类管理的功能实现。
先看下布局。
总得来说页面和清单、已完成没有什么不同,但是多了一个窗口。
并且切换页面的时候需要关闭这个窗口。
与todoListView和FinishedListView一样,我们需要创建一个CategoryListView。
功能与上面一样,区别的是点击“+”号弹出新窗口,我们需要创建一个新的window
然后在View层写相关的方法即可。
点击新增或者编辑即会创建页面。
切换页面时会hide,会调用close方法。
这处代码较复杂,很绕。
全局事件消息机制。
顶一个一个字典List<int,Action>,一个枚举,一个注册的方法,一个取消注册的方法,还有调用的方法。
注册时,将传进来的T转换为int,Action添加到字典中,调用时传入int即可。
消息队列机制。
举例:
今天你吃了吗?
是?否?
吃的什么?
米饭?面条?
工作中经常遇到这些问题,如何解决呢?
第一个类,问题类。
有一个Title方法,可以设置标题,即问题。
有一个Menu方法,可以设置答案。
有一个回调,绑定触发下一个问题的方法,在每次选择答案后触发。
第二个类,消息队列类。
有一个Enqueue,里面装所有的问题,
有一个调用的方法,先判断队列中是否还有问题,如果有,则弹出,没有则return,并且将该方法绑定到问题类的回调。
流程,消息队列类调用运行方法,弹出第一个问题,选择答案,通过昂定的回调再次调用运行方法。