一、问题背景
在实验准备面板 UIPreparationPanel 中,页面主体使用了 ScrollRect 做纵向滚动。
实际使用时发现:
- 鼠标放在普通空白区域时,滚轮可以正常滚动页面。
- 鼠标放在步骤表格
StepRow的"试剂"或"器材"区域时,滚轮无法滚动页面。 - 这两个区域上挂了
EventTrigger,用于处理鼠标移入和移出时的高亮显示。
这类问题在 Unity UGUI 中比较常见:某个子 UI 看起来只是做了悬停效果,却意外影响了父级 ScrollRect 的滚轮滚动。
二、问题现象
当前面板结构可以简化理解为:
text
UIPreparationPanel
└── Scroll View
├── Viewport
│ └── Content
│ └── StepRow
│ ├── 试剂 挂了 EventTrigger
│ └── 器材 挂了 EventTrigger
└── Scrollbar Vertical
Scroll View 上的 ScrollRect 配置正常:
Vertical = trueContent已绑定Viewport已绑定Scroll Sensitivity有正常数值
但是鼠标移动到 StepRow/试剂 或 StepRow/器材 上滚动时,父级 ScrollRect 收不到滚轮事件,所以页面不滚。
三、UGUI 鼠标滚轮滚动的基本原理
UGUI 的滚轮滚动不是 ScrollRect 主动一直监听鼠标滚轮,而是由 EventSystem 分发事件。
大致流程是:
text
鼠标滚轮输入
↓
EventSystem 检测鼠标下方的 UI
↓
GraphicRaycaster 找到最上层可被射线命中的 UI
↓
EventSystem 从命中的对象开始查找 IScrollHandler
↓
找到能处理滚轮事件的组件后调用 OnScroll
ScrollRect 能滚动,是因为它实现了 IScrollHandler。
也就是说,正常情况下鼠标滚轮事件最终会调用:
csharp
ScrollRect.OnScroll(PointerEventData eventData)
然后 ScrollRect 根据 eventData.scrollDelta 修改 verticalNormalizedPosition 或 horizontalNormalizedPosition,从而移动 Content。
四、为什么 EventTrigger 会影响 ScrollRect
EventTrigger 是 Unity 提供的通用 UI 事件组件。
它可以在 Inspector 中配置很多事件,例如:
- PointerEnter
- PointerExit
- PointerClick
- BeginDrag
- Drag
- EndDrag
- Scroll
问题点在于:EventTrigger 组件本身实现了多种事件接口,其中包括滚轮相关接口。
所以即使我们只在 EventTrigger 中配置了 PointerEnter 和 PointerExit,它也可能成为事件系统眼中的"可以处理滚轮事件的对象"。
当鼠标下方的对象是 StepRow/试剂 或 StepRow/器材 时,事件链路会变成:
text
鼠标滚轮
↓
命中 StepRow/试剂 或 StepRow/器材
↓
该对象上存在 EventTrigger
↓
EventSystem 认为这里已经有组件能处理滚轮事件
↓
滚轮事件没有继续交给父级 ScrollRect
↓
页面不滚动
这就是"EventTrigger 把滚轮事件吞掉"的常见原因。
五、解决思路
解决这个问题有两类思路:
- 不再使用
EventTrigger,改成只实现需要的事件接口。 - 保留
EventTrigger,但把滚轮事件手动转发给父级ScrollRect。
两种方案都可以使用,选择哪一种取决于当前项目中预制体的维护方式。
六、方案一:删除 EventTrigger,改用专门的 Hover 脚本
如果 EventTrigger 只是为了实现鼠标移入显示、鼠标移出隐藏,那么可以删除 EventTrigger,换成一个只实现 IPointerEnterHandler 和 IPointerExitHandler 的脚本。
这样脚本只处理悬停事件,不处理滚轮事件,滚轮事件就可以继续交给父级 ScrollRect。
示例脚本:
csharp
using UnityEngine;
using UnityEngine.EventSystems;
namespace VirtualLab.UI
{
/// <summary>
/// 鼠标悬停时控制目标对象显示或隐藏。
/// 用于替代 EventTrigger 的 PointerEnter 和 PointerExit,避免 EventTrigger 截断 ScrollRect 的滚轮事件。
/// </summary>
public class UIHoverActive : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
/// <summary>
/// 鼠标进入时显示、鼠标离开时隐藏的目标对象。
/// 例如 StepRow 中"试剂"或"器材"节点下的 Image 高亮图层。
/// </summary>
[SerializeField] private GameObject target;
/// <summary>
/// 鼠标进入当前 UI 区域时显示目标对象。
/// </summary>
public void OnPointerEnter(PointerEventData eventData)
{
target.SetActive(true);
}
/// <summary>
/// 鼠标离开当前 UI 区域时隐藏目标对象。
/// </summary>
public void OnPointerExit(PointerEventData eventData)
{
target.SetActive(false);
}
}
}
使用方式:
text
StepRow.prefab
├── 试剂
│ ├── 删除 EventTrigger
│ ├── 添加 UIHoverActive
│ └── target 绑定子节点 Image
└── 器材
├── 删除 EventTrigger
├── 添加 UIHoverActive
└── target 绑定子节点 Image
这个方案的优点:
- 逻辑清晰,组件只处理自己真正需要的事件。
- 不会截断父级
ScrollRect的滚轮事件。 - 后续维护时更容易判断这个组件的作用。
这个方案的缺点:
- 需要修改挂了
EventTrigger的预制体。 - 如果多个预制体都用了同样的
EventTrigger悬停逻辑,需要逐个替换。
七、方案二:保留 EventTrigger,增加滚轮转发脚本
如果当前项目中有多个预制体已经使用了 EventTrigger,并且不想逐个删除或重做悬停逻辑,可以保留原来的 EventTrigger,额外添加一个滚轮转发脚本。
这个脚本实现 IScrollHandler,当当前节点收到滚轮事件时,主动调用父级 ScrollRect.OnScroll。
示例脚本:
csharp
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace VirtualLab.UI
{
/// <summary>
/// 将当前 UI 节点收到的鼠标滚轮事件转发给父级 ScrollRect。
/// 用于保留 EventTrigger、Button 等交互效果,同时避免它们截断滚动页面的事件。
/// </summary>
public class UIScrollForwarder : MonoBehaviour, IScrollHandler
{
/// <summary>
/// 接收滚轮事件的目标滚动组件。
/// 可以在 Inspector 中手动绑定;不绑定时会从父节点自动查找最近的 ScrollRect。
/// </summary>
[SerializeField] private ScrollRect targetScrollRect;
/// <summary>
/// 初始化时缓存父级 ScrollRect。
/// 这样每次滚轮滚动时不需要重复查找组件。
/// </summary>
private void Awake()
{
if (targetScrollRect == null)
{
targetScrollRect = GetComponentInParent<ScrollRect>();
}
}
/// <summary>
/// 当前节点收到滚轮事件时,将事件转发给目标 ScrollRect。
/// ScrollRect 会继续使用原始 eventData 中的 scrollDelta 来计算滚动距离。
/// </summary>
public void OnScroll(PointerEventData eventData)
{
if (targetScrollRect == null)
{
return;
}
targetScrollRect.OnScroll(eventData);
}
}
}
使用方式:
text
StepRow.prefab
├── 试剂
│ ├── 保留 EventTrigger
│ └── 添加 UIScrollForwarder
└── 器材
├── 保留 EventTrigger
└── 添加 UIScrollForwarder
这个方案的优点:
- 不需要删除原来的
EventTrigger。 - 原来的悬停显示、隐藏逻辑不受影响。
- 对已有预制体侵入较小。
这个方案的缺点:
- 本质上是在补救被截断的事件链。
- 如果有很多类似节点,需要保证这些节点都加了转发组件。
八、为什么不推荐只在 UIPreparationPanel 上写 OnScroll
有时会想到:既然父级页面要滚动,那能不能只在 UIPreparationPanel 上实现 IScrollHandler?
通常不行。
因为滚轮事件是从鼠标命中的 UI 对象开始分发的。如果事件已经在子节点的 EventTrigger 处被处理,父级 UIPreparationPanel 上的 OnScroll 很可能收不到。
也可以在 Update 中直接读取:
csharp
Input.mouseScrollDelta.y
然后手动控制 ScrollRect。
但这种方式绕开了 UGUI 的事件系统,需要自己判断鼠标是否位于目标滚动区域内。如果页面里有多个滚动区域,例如左侧步骤表、右侧搜索结果列表,就容易出现滚动区域互相影响的问题。
所以除非有明确需求,一般不建议用 Update 直接监听滚轮作为首选方案。
九、推荐选择
如果只是一个或少数几个预制体使用了 EventTrigger 做悬停效果,推荐使用方案一:
text
删除 EventTrigger,改用 UIHoverActive
这样最干净,组件职责也最明确。
如果项目中已经有多个预制体用了 EventTrigger,并且不方便逐个替换,推荐使用方案二:
text
保留 EventTrigger,添加 UIScrollForwarder
对当前 UIPreparationPanel 的情况,如果"试剂"和"器材"区域未来还会继续保留原来的 EventTrigger 悬停效果,那么滚轮转发方案改动更小。
十、总结
ScrollRect 鼠标滚轮失效时,不一定是 ScrollRect 配置错误。
排查时可以按这个顺序看:
ScrollRect是否启用了Vertical或Horizontal。Content和Viewport是否正确绑定。- 鼠标所在的子 UI 是否有
EventTrigger、自定义事件脚本、嵌套ScrollRect。 - 子 UI 是否通过
Raycast Target成为了事件命中的对象。 - 滚轮事件是否被子节点截断,导致父级
ScrollRect收不到。
核心经验是:
text
EventTrigger 很方便,但它是一个"通用事件接收器"。
如果只需要某一两个事件,最好写专门的小脚本实现对应接口。
如果必须保留 EventTrigger,就需要在它截断滚轮事件的位置做转发。