1. 什么是滚动轮劫持#
这篇文章介绍一个很简单的继承自ScrollViewer的控件:
public class ExtendedScrollViewer : ScrollViewer
{
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if (ViewportHeight + VerticalOffset >= ExtentHeight && e.Delta <= 0)
return;
if (VerticalOffset == 0 && e.Delta >= 0)
return;
base.OnMouseWheel(e);
}
}
所有代码就这么多,这个ExtendedScrollViewer 只是用来解决滚动轮劫持(scroll-wheel-hijack)的问题。所谓的滚动轮劫持,简单来说即是在一个可以滚动的页面使用鼠标滚轮滚动页面的过程中鼠标进入某个可以滚动的子元素导致只在这个子元素中滚动而整个页面想滚滚不动了。
具体看看这个例子:
这个情况相信很多人都遇到过,滚轮被“劫持”后索性去拖动滚动条。有次我遇到个内嵌了很多ScrollViewer的长页面,使用起来真的很恼人,所以我使用ExtendedScrollViewer 解决了这个问题。当然还有另外很多种情况的滚动轮劫持,也有很多解决方案,这篇文章只介绍我遇到的情况和我的解决方案。
2. 实现#
在WPF中要禁止ScrollViewer捕获鼠标滚动时间,可以重写OnMouseWheel
成一个空的方法:
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
}
OnMouseWheel方法用于响应鼠标滚轮的事件,将它重载成空方法即不再处理鼠标滚利事件。注意在这种情况下不可以使用e.Handled = true
,因为我们的目标是让外层的ScrollViewer可以接收到鼠标滚轮事件,所以不能更改MouseWheelEventArgs 的Handled。
当然我们不满足于无脑禁用鼠标滚轮,我们应该更智能些,先让ScrollViewer滚到底,再交由外层的ScrollViewer滚下去。这里面用到几个属性:
MouseWheelEventArgs中的Delta表示鼠标滚轮的变更量,当这个值为正数时表示滚轮向上。
ExtentHeight,获取ScrollViewer内容的实际高度。
ViewportHeight,获取当前可视区域的高度。
VerticalOffset,包含滚动内容对应于页首的垂直偏移量的值,有效值介于 0 与 ExtentHeight 减去 ViewportHeight 所得的数值之间。
熟悉了上面几个属性的作用后我们可以更好地控制鼠标滚轮的行为,当鼠标向上滚动时,判断现在是否已经滚到顶了,如果是就不处理鼠标滚轮事件:
if (VerticalOffset == 0 && e.Delta >= 0)
return;
而当鼠标向下滚动时,需要根据ViewportHeight
、VerticalOffset
和ExtentHeight
判断当前是否已经滚动到底,如果是就不处理鼠标滚轮事件:
if (ViewportHeight + VerticalOffset >= ExtentHeight && e.Delta <= 0)
return;
3. 其他ScrollViewer方案#
ScrollViewer还有很多中玩法,但我工作中不常用到所以就没做。如果觉得不满足还可以参考HandyControl的ScrollViewer,它直接提供了一个CanMouseWheel
属性用于控制是否响应鼠标滚轮,另外还支持了滚动等功能。
4. 参考#
ScrollViewer.OnMouseWheel(MouseWheelEventArgs) Method (System.Windows.Controls) Microsoft Docs
MouseWheelEventArgs.Delta Property (System.Windows.Input) Microsoft Docs
ScrollViewer.ExtentHeight Property (System.Windows.Controls) Microsoft Docs
ScrollViewer.ViewportHeight Property (System.Windows.Controls) Microsoft Docs
ScrollViewer.VerticalOffset Property (System.Windows.Controls) Microsoft Docs