UI框架从0到1第五节:【事件消息体】不仅知道你点了啥,还知道你改了啥

到目前为止,我们的框架已经支持:

  • 多个 UI 控件自动触发事件;

  • 所有事件汇总进 PanelBase.ProcessEvent()

  • 页面之间逻辑独立、结构清晰。

但问题还远未解决。


❓ 如果我们想知道 Toggle 的勾选状态?或者 InputField 输入了什么?我们该怎么做?

我们无法仅靠 sender.name 来判断控件类型或获取其值 ------ 这就是我们今天要补上的最大漏洞。


🎯 目标:为事件附加"上下文信息"

我们需要一种通用机制,让控件在触发事件时携带额外的数据(比如 bool 值、string 文本),并在 PanelBase 中便捷地读取这些数据。


📦 定义事件上下文接口

我们从一个空接口 IEventContext 开始,用作所有"事件附带数据"的统一标识。

kotlin 复制代码
public interface IEventContext { }

然后我们定义一个通用的数据结构 ValueChangedContext<T>,用于描述"旧值 → 新值"的变更:

ini 复制代码
public class ValueChangedContext<T> : IEventContext
{
    public T OldValue;
    public T NewValue;

    public ValueChangedContext(T oldVal, T newVal)
    {
        OldValue = oldVal;
        NewValue = newVal;
    }
}

🔍 为啥用 class,不用 struct?

你可能会好奇,为啥不用 struct?不是更轻量吗?

答:这是一种有意识的取舍。

  • ✅ 我们需要支持多态 ,而 struct 不擅长这个场景;

  • ✅ 如果结构体实现接口,在使用接口接收时会发生装箱,反而导致 GC;

  • ✅ 有些值本身就是引用类型(如 stringGameObject),用 struct 也没性能提升;

结论:这里明确使用类,是为了让事件上下文系统更加灵活与安全。


🔄 控件发送事件时传入上下文

我们修改 EventUIBase,加入 Send() 方法重载,统一事件发射接口:

csharp 复制代码
protected void Send(string eventName, IEventContext context)
{
    targetPanel?.ProcessEvent(eventName, this, context);
}

protected void Send(string eventName)
{
    targetPanel?.ProcessEvent(eventName, this);
}

🧪 示例:改造 Button 与 Toggle 的 Custom 脚本

ButtonCustom.cs

按钮没有附加值,仅发送事件名即可。

csharp 复制代码
public class ButtonCustom : EventUIBase
{
    private Button btn;

    protected override void Init()
    {
        base.Init();
        btn = GetComponent<Button>();
        btn.onClick.AddListener(() =>
        {
            Send("OnClick");
            Debug.Log($"{name} 被点击");
        });
    }
}

ToggleCustom.cs

开关需要传递勾选状态变化。

ini 复制代码
public class ToggleCustom : EventUIBase
{
    private Toggle tog;
    private bool previousValue;

    protected override void Init()
    {
        base.Init();
        tog = GetComponent<Toggle>();
        previousValue = tog.isOn;
        tog.onValueChanged.AddListener(newValue =>
        {
            Send("OnToggleValueChanged", new ValueChangedContext<bool>(previousValue, newValue));
            previousValue = newValue;
        });
    }
}

📬 PanelBase 中统一处理事件 + 解析上下文

现在我们来改造 PanelBase,让它支持事件分发与类型识别:

csharp 复制代码
public class PanelBase : MonoBehaviour
{
    public virtual void ProcessEvent(string eventName, EventUIBase sender, IEventContext context = null)
    {
        Debug.Log($"[{name}] 收到事件: {eventName}");

        Dispatch<bool>(eventName, "IsShow", context, (oldVal, newVal) =>
        {
            Debug.Log($"IsShow 从 {oldVal} → {newVal}");
            if (newVal) OnShow(); else OnClose();
        });
    }

    protected void Dispatch<T>(string eventName, string targetEvent, IEventContext context, Action<T, T> callback)
    {
        if (eventName == targetEvent && context is ValueChangedContext<T> valCtx)
        {
            callback?.Invoke(valCtx.OldValue, valCtx.NewValue);
        }
    }

    protected void Dispatch(string eventName, string targetEvent, Action callback)
    {
        if (eventName == targetEvent)
        {
            callback?.Invoke();
        }
    }

    protected virtual void OnShow() => transform.localScale = Vector3.one;
    protected virtual void OnClose() => transform.localScale = Vector3.up;
}

🧠 示例:HelloWorldPanel 的完整事件处理

csharp 复制代码
public class HelloWorldPanel : PanelBase
{
    public override void ProcessEvent(string eventName, EventUIBase sender, IEventContext context = null)
    {
        base.ProcessEvent(eventName, sender, context);

        Dispatch(eventName, "OnClick", () =>
        {
            if (sender.name == "按钮1")
            {
                Debug.Log("处理按钮1逻辑");
            }
        });

        Dispatch<bool>(eventName, "OnToggleValueChanged", context, (oldVal, newVal) =>
        {
            if (sender.name == "toggle1")
            {
                Debug.Log($"切换状态变更为:{newVal}");
            }
        });
    }
}

💡 Tips:Dispatch 的本质是"事件判断器 + 执行器"

javascript 复制代码
Dispatch("OnClick", "OnClick", () => Debug.Log("执行了按钮逻辑"));

其实就是:

ini 复制代码
if (eventName == "OnClick")
{
    // 执行逻辑
}

这就是事件驱动编程的核心思想:"把要做的事交给框架,在恰当的时机自动触发"


🔚 本节总结

能力

是否完成

控件支持传递自定义值

✅ 是

面板能够类型安全地解析数据

✅ 是

不再依赖 sender.name 获取数据

✅ 是

同名按钮是否区分?

✅ 可区分控件类型

同类控件是否仍不能重名?

❌ 还没解决(将在后面处理)

🔮 下一节预告:我们真的需要这么复杂的上下文吗?

现在你可能在想:

"按钮不就一个点击事件?Toggle 也就传个 bool,有必要搞这么多类、泛型、接口吗?"

下节我们就来探讨这个问题,并通过一个"类 MVVM"风格的 View → Logic 数据派发例子,让你真正理解这个系统背后的设计哲学。

相关推荐
kirayoshikake21 小时前
UGUI 性能优化系列:第二篇——Canvas 与 UI 元素管理
unity3d
kirayoshikake21 小时前
UI框架从0到1第九节:【脚本模板生成】怎么用代码生成代码?
unity3d
kirayoshikake21 小时前
Unity入门教程之异步篇第五节:对UniTask的高级封装
unity3d
kirayoshikake21 小时前
UI框架从0到1第二节:【全控件适配】把 Toggle、InputField 全都拉进事件系统
unity3d
kirayoshikake21 小时前
UI框架从0到1第六节:【轻量 MVVM】用属性驱动 UI,彻底抛弃命令式调用
unity3d
kirayoshikake21 小时前
Unity入门教程之异步篇第一节:协程基础扫盲--非 Mono 类如何也能启动协程?
unity3d
kirayoshikake21 小时前
响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!
unity3d
kirayoshikake21 小时前
响应式编程入门教程第六节:进阶?Combine、Merge、SelectMany 与错误处理
unity3d
kirayoshikake21 小时前
对象池由浅入深第三节:基于 ObjectPoolPro 扩展 Unity 对象池--泛型对象池、自动回收和场景隔离
unity3d