改进 WPF 鼠标滚轮处理






4.91/5 (29投票s)
如何快速改进您的 WPF 应用程序,
目录
简介
Windows Presentation Foundation (简称 WPF) 是一个出色的图形框架,用于在 .NET 中创建现代且吸引人的应用程序。目前,它对鼠标滚轮的支持不足,例如缺少水平、平滑和嵌套滚动,以及高分辨率模式。
我将在此介绍的WPF 鼠标滚轮库 (MWLib) 旨在以最小的努力为您克服这些限制。它用 C# 编写,并使用 .NET 3.5 Service Pack 1 客户端配置文件进行编译 (随 Windows 7 一起打包)。您可以轻松地将此库集成到任何现有的 WPF 应用程序中,让用户在使用鼠标滚轮时获得极佳的流畅感。除了通过水平、平滑和嵌套滚动改进滚动体验外,该库还支持任何分辨率的鼠标滚轮,实现了运动去抖以及平滑和嵌套缩放。它还提供了支持在任何范围类控件上启用鼠标滚轮的功能。
路线图
对于那些渴望在阅读完文章之前就开始动手实践的人,我们将从一个快速入门指南开始。然后在提供一些背景信息并概述MWLib 功能后,我们将通过一个小教程进行讲解:您将创建一个 WPF 应用程序,找出开箱即用的鼠标滚轮支持的局限性,并使用 MWLib 来修复它们。最后,我们将通过玩转两个演示应用程序来深入探讨鼠标滚轮的局限性和改进之处。这将使我们能够比较在集成该库之前和之后,滚轮在各种情况下的行为。
快速入门
对于希望立即受益于 MWLib 的用户,请
- 下载本文档顶部提供的库,将其包含在您的解决方案中,并在您的项目中引用它。
- 将以下代码添加到您的主窗口 XAML
<Window ... xmlns:i="clr-namespace:Lada.Windows.Input;assembly=WpfMouseWheelLib" i:MouseWheel.Enhanced="True">
- (可选) 如果您希望所有控件都能在任何方向上平滑滚动,请添加以下代码
<Window ... xmlns:i="clr-namespace:Lada.Windows.Input;assembly=WpfMouseWheelLib" i:MouseWheel.Enhanced="True" i:Mousewheel.Scrollmode="Physical">
警告:如果您的应用程序包含ListBox
、ListView
或DataGrid
等控件,这样做可能会对性能产生负面影响 - 请参阅逻辑滚动 vs 物理滚动
…这样就完成了!
如果您时间紧迫但仍想看到 MWLib 的实际效果,请查看“之后”演示应用程序并进行尝试。
背景
在继续深入之前,我建议您浏览 Tanvi Shah K & H Steve Davis 的文章“在应用程序中处理增强型鼠标滚轮”。它描述了滚轮的简史、其描述、高分辨率模式以及与原生 Windows 的接口。
我同样建议下载随上述文章提供的增强型滚轮模拟器应用程序。它将在教程和随后的演示中非常有价值。
可选地,您可以查阅 MSDN 上的附加属性概述和滚动视图概述文章。我也建议您回顾 WPF 的路由事件模型。Josh Smith 有一篇关于该主题的精彩文章。
Windows 中的滚轮模型
微软将鼠标滚轮建模为由离散且均匀分布的刻度组成。当您旋转滚轮时,每次遇到一个刻度时,都会报告一个“滚轮增量”。最初,一个刻度被设计为滚轮旋转测量单位。
微软将其值设置为120,以便供应商能够构建“增强型”滚轮,通过报告较低的“滚轮增量”来更精确地测量其旋转量。这类滚轮可以从标准模式切换到高分辨率模式(DPI 切换)。
nativeResolution = 120 / abs(wheelDelta)
应用领域
最初,“滚轮”主要用于滚动,因此得名。但在 WPF 中,按住键盘修饰键会改变其应用领域:如果您按住 Control 键同时旋转滚轮,某些 Web 浏览器中的文本大小会增加或减小;另一方面,图像编辑程序中的图像会放大或缩小。如果您按住 Shift 键,其他应用程序会水平滚动。
WPF 问题
在 WPF 实现中,以下图形元素的鼠标滚轮事件处理程序 (UIElement.OnMouseWheel
) 被覆盖
(1) 和 (2) 实现垂直滚动,(3) 和 (4) 同时实现垂直滚动和缩放,(4) 按页滚动。WPF 既不支持滚轮水平滚动也不支持平滑滚动。
以下是几个其他缺失的功能
不支持开箱即用的高分辨率模式
上述所有鼠标滚轮事件处理程序仅使用滚轮增量的符号来确定滚动或缩放方向:未计算分辨率。因此,当增强型滚轮进入高分辨率模式时,无法适应滚轮运动。结果是过度灵敏的移动(见下文),而不是更精细的运动控制,从而适得其反,而不是带来优势。
不一致的嵌套滚动
在 WPF 中,嵌套滚动仅在内部元素是 TextBoxBase
的子类型时才有效。
然而,对于 ScrollViewer
或 FlowDocumentScrollViewer
实例,它无效。原因是它们的 OnMouseWheel
处理程序始终将 RoutedEventArgs.Handled
设置为 True
。但当该元素达到滚动限制之一时,此属性应设置为 False
,以便滚动继续。
另一方面,FlowDocumentPageViewer
始终将 RoutedEventArgs.Handled
设置为 False
,导致内部(翻页)和外部元素同时滚动。
应用领域不匹配
在嵌套滚动的上下文中,您期望鼠标滚轮应用领域具有一致性。
例如,假设光标位于 ScrollViewer
内部元素之上,而外部元素是 FlowDocumentScrollViewer
。如果您按住 Control 键同时旋转滚轮,您期望执行缩放操作(在外部元素上),但实际上会发生滚动操作(在内部元素上)。这是内部元素 OnMouseWheel
处理程序中键盘修饰键处理不当的后果:ScrollViewer
不处理缩放,而是滚动,无论使用什么修饰键。
MWLib 功能
MWLib 旨在克服上述 WPF 问题。
支持开箱即用的高分辨率模式
用户不再面临过度灵敏的运动。
取而代之的是,用户获得了更精细的控制,从而提高了响应能力。
一致的嵌套滚动
嵌套滚动现在对所有应用领域都完全可用且一致。此外,内部和外部可滚动元素之间的过渡是平滑的,即使内部元素是基于项的(例如 ListBox
),而外部元素是基于像素的(例如 Image
嵌套在 ScrollViewer
中)。
应用领域区分
现在可以将键盘修饰键分配给所需的应用领域。已修复预期操作和观察到的结果之间的不匹配。
更重要的是,MWLib 还提供了
附加应用领域
- 水平滚动(默认修饰键为 Shift 键)。
- 适应:鼠标滚轮现在可以作用于任何范围类控件(例如,移动
Slider
光标)。
倾斜滚轮支持
您现在可以使用倾斜滚轮水平滚动。
运动平滑
可以在任何应用领域的任何 UI 元素上启用平滑位移。
运动去抖
对于增强型滚轮,可能需要一个去抖算法来过滤掉因手指无意移动而产生的抖动。当缓慢滚动、手指抬离滚轮时… 可能会发生这种情况。
供应商通常实现此类算法来模拟标准分辨率滚轮,当高分辨率模式关闭时。MWLib 内置了针对高分辨率的去抖功能。
多样的滚动增量
现在可以根据系统设置(请参阅鼠标滚轮控制面板)绝对或相对地调整滚动增量。
功能传播
您可以隐式地在 UI 元素树中的任何位置受益于 MWLib 功能(得益于 WPF 附加属性继承),或在本地覆盖它们。控件模板也继承了这些功能,无需额外代码。
适用于多种控件
MWLib 已在多种控件上进行了测试,包括:Image
、ItemsControl
、ListBox
、ListView
、DataGrid
、ComboBox
、TextBox
、RichTextBox
、FlowDocumentScrollViewer
和 FlowDocumentPageViewer
。
您的第一个平滑滚动应用程序
在本节中,我们将从头开始创建一个简单的 WPF 应用程序,以便
- 找出开箱即用的滚动局限性。
- 应用 MWLib 来修复它们。
在 Visual Studio 中创建一个新解决方案。添加一个新的 WPF 应用程序项目:例如,称其为“SmoothScroll”。在项目属性的“应用程序”选项卡下,选择“NET Framework 3.5 Client Profile”作为目标框架。构建并运行。
现在我们可以为应用程序添加新功能了。首先,我们将在主窗口中嵌入一个 ListBox
,其中包含足够多的项来测试垂直滚动,并包含足够宽的数据项来测试水平滚动。以下是相应的 XAML 代码
<Window
x:Class="SmoothScroll.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Native Scroll" Height="300" Width="200">
<ListBox>
<ListBoxItem Content="00 Test item, Test item, Test item, Test item, Test item, ..." />
<ListBoxItem Content="01 Test item, Test item, Test item, Test item, Test item, ..." />
<ListBoxItem Content="02 Test item, Test item, Test item, Test item, Test item, ..." />
...
</ListBox>
</Window>
粗体部分显示了已更新的代码。一些重复的文本已被省略号跳过。让我们执行它。
开箱即用体验 (无 MWLib)
垂直滚动
将鼠标光标放在列表框中,然后将滚轮向您滚动一格:列表框应滚动 3 项。如果不是这样,请启动“鼠标控制面板”小程序,激活“滚轮”选项卡,将每格的行数修改为 3,然后按“确定”。
水平滚动
不幸的是,WPF 当前版本仍未实现鼠标滚轮水平滚动。这是 MWLib 解决的问题之一。
高分辨率模式
让我们测试一下高分辨率模式下的增强型滚轮。如果您没有此类滚轮或此模式未启用,请启动增强型滚轮模拟器应用程序,将事件间隔从 8 更改为 32 毫秒,然后按“应用”。请注意,如果您退出此应用程序,将恢复标准分辨率。
重复垂直滚动测试:粒度已从每格 3 项增加到 8 x 3 = 24 项……我们并没有获得更精细的滚动控制,反而得到了一个过于灵敏的滚轮!事实上,WPF 完全没有考虑滚轮增量的值,每格会引发 8 个事件。
改进体验 (使用 MWLib)
我们将修复上述问题。请添加对 MWLib 的引用,然后像这样更新主窗口 XAML
<!-- MainWindow.xaml -->
<Window
x:Class="SmoothScroll.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:Lada.Windows.Input;assembly=WpfMouseWheelLib"
Title="Enhanced Scroll" Height="300" Width="200"
i:MouseWheel.Enhanced="True">
<ListBox>
...
</ListBox>
</Window>
构建、运行并像以前一样测试新代码。
- 在标准分辨率模式下,您不会看到垂直滚动方面的任何差异。粒度仍然是每格 3 项,但是水平滚动现在可用了。要测试它,只需倾斜滚轮或按住 Shift 键并滚动滚轮。作为奖励,水平滚动是平滑的。
- 在高分辨率模式下,请注意垂直滚动粒度不再是每格 24 项,而是 3 项。太棒了!与标准模式相比,有一个小区别:列表滚动得更平滑,一次滚动一个元素。每格会引发八个事件,这些事件由库转换为三个事件。
逻辑滚动 vs. 物理滚动
默认情况下,列表框的垂直滚动不是平滑的。要找出原因,我们需要更深入地研究 MSDN。
正如“滚动视图概述”中所述,WPF 实现两种滚动模式
-
物理滚动模式
平滑滚动仅在物理模式下可用,在该模式下可以按像素级别控制位移。 -
逻辑滚动模式
也称为基于项的滚动。在此模式下,UI 虚拟化已启用。
“UI 虚拟化是指一种技术,其中 UI 元素子集根据屏幕上可见的项从大量数据项生成。当屏幕上可能只有少量元素时生成许多 UI 元素可能会对应用程序的性能产生不利影响”(VirtualizingStackPanel 类,备注部分)。
一些控件,如 ListBox
、ListView
和 DataGrid
,默认处于逻辑模式。为了保持应用程序性能,MWLib 默认将其保留在此模式下。尽管如此,我们可以轻松地将这些控件切换到物理模式,这将启用平滑滚动。
切换到物理模式
要将列表框从逻辑模式切换到物理模式,您有两种选择
- 修改
ScrollMode
附加属性的值
i:MouseWheel.ScrollMode="Physical"
此属性应用于被装饰的元素及其所有后代。如果我们希望滚动模式应用于特定方向,我们可以使用 VScrollMode
和 HScrollMode
属性代替。
- 或者修改
CanContentScroll
附加属性的值
ScrollViewer.CanContentScroll="False"
此属性仅应用于被装饰的元素。
让我们从第一个选项开始。
像这样修改主窗口 XAML 代码
<!-- MainWindow.xaml -->
<Window
...
i:MouseWheel.Enhanced="True"
i:MouseWheel.ScrollMode="Physical">
<ListBox>
...
</ListBox>
</Window>
让我们执行此代码。请注意,现在垂直和水平滚动都是平滑的。
可选地,我们可以尝试第二种方法。
<!-- MainWindow.xaml -->
<Window
...
i:MouseWheel.Enhanced="True">
<ListBox
ScrollViewer.CanContentScroll="False">
...
</ListBox>
</Window>
您应该会观察到与之前相同的行为。
演示应用程序
本文档附带两个示例应用程序:“之前”和“之后”使用 MWLib。
MWLib '之前' (WpfMouseWheelNative.exe)
此应用程序允许您体验 WPF 鼠标滚轮处理的本地行为。用户界面如下所示
它组织为一个选项卡项的树,其中每个叶子都是一个小的“实验室”,您可以在其中测试滚轮在各种 WPF 控件上的行为。我建议您浏览树并使用滚轮进行尝试,同时观察滚动和缩放。
- 在 FlowDocumentxxx 选项卡中,按住 Control 修饰键并旋转滚轮即可测试缩放。
- 在原生 WPF 中尚未实现水平滚动。
我们可以注意到,对于高分辨率模式下的滚轮、嵌套滚动或缩放选项卡,用户体验尤其糟糕。
MWLib '之后' (WpfMouseWheelEnhanced.exe)
在这里,您可以看到 MWLib 带来的改进。
除了“之前”应用程序中已有的各种选项卡外,“之后”应用程序还提供了一个新选项卡:适应,其中鼠标滚轮控制滑块的光标。
此外,一个选项面板允许您自定义滚轮运动的各个方面。
应用领域
在 WPF 中,滚动由 ScrollViewer
和 FlowDocumentPageViewer
支持。缩放由 FlowDocumentScrollViewer
和 FlowDocumentPageViewer
支持。
MWLib 引入了另一个我们称之为“适应”的应用领域,其中滚轮可以作用于任何范围类控件。‘之后’演示应用程序提供了一个在 Slider
上进行适应的示例。
选项面板
它显示了可用滚轮的列表,包括它们的名称和分辨率,以及 MouseWheel
附加属性的可视化表示。
在继续深入之前,值得注意的是其中一些属性参与了值继承。
视觉选项与 MouseWheel
附加属性之间的数据绑定遵循MVVM 模式:视图的属性 - MouseWheelOptionsView
- 绑定到其视图模型 - MouseWheelOptions
的属性,该视图模型被设置为应用程序主窗口的数据上下文。然后,MouseWheelOptions
的滚动和缩放属性被绑定到 MouseWheel
的相应附加属性。
<!-- MainWindow.xaml -->
<Window.Resources>
<ObjectDataProvider x:Key="_options" ObjectType="{x:Type vm:MouseWheelOptions}" />
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource _options}" />
</Window.DataContext>
<vw:WorkspaceView Grid.Column="2"
i:MouseWheel.Enhanced ="{Binding Enhanced}"
i:MouseWheel.VScrollMode ="{Binding ScrollOptions.Y.ScrollMode}"
i:MouseWheel.HScrollMode ="{Binding ScrollOptions.X.ScrollMode}"
i:MouseWheel.VScrollSmoothing ="{Binding ScrollOptions.Y.Smoothing}"
i:MouseWheel.HScrollSmoothing ="{Binding ScrollOptions.X.Smoothing}"
i:MouseWheel.NestedVScroll ="{Binding ScrollOptions.Y.NestedScroll}"
i:MouseWheel.NestedHScroll ="{Binding ScrollOptions.X.NestedScroll}"
i:MouseWheel.PhysicalVScrollDebouncing ="{Binding ScrollOptions.Y.Physical.Debouncing}"
i:MouseWheel.PhysicalHScrollDebouncing ="{Binding ScrollOptions.X.Physical.Debouncing}"
i:MouseWheel.LogicalVScrollDebouncing ="{Binding ScrollOptions.Y.Logical.Debouncing}"
i:MouseWheel.LogicalHScrollDebouncing ="{Binding ScrollOptions.X.Logical.Debouncing}"
i:MouseWheel.PhysicalVScrollIncrement ="{Binding ScrollOptions.Y.Physical...SelectedItem}"
i:MouseWheel.PhysicalHScrollIncrement ="{Binding ScrollOptions.X.Physical...SelectedItem}"
i:MouseWheel.LogicalVScrollIncrement ="{Binding ScrollOptions.Y.Logical...SelectedItem}"
i:MouseWheel.LogicalHScrollIncrement ="{Binding ScrollOptions.X.Logical...SelectedItem}"
i:MouseWheel.ZoomSmoothing ="{Binding ZoomOptions.Smoothing}"
i:MouseWheel.ZoomDebouncing ="{Binding ZoomOptions.Debouncing}"
i:MouseWheel.NestedZoom ="{Binding ZoomOptions.NestedZoom}"
/>
在上述代码中,增强型绑定以及滚动和缩放绑定被应用于应用程序的左工作区面板 - WorkspaceView
。所有这些都是可继承的,因此可用于 WorkspaceView
可视树的所有元素。
适应选项不可继承,并直接应用于 Slider
控件,如下所示:
<!-- WorkspaceResources.xaml -->
<Slider x:Key="L0-Slider" x:Shared="False"
...
i:MouseWheel.Smoothing ="{Binding CustomOptions.Smoothing}"
i:MouseWheel.Debouncing ="{Binding CustomOptions.Debouncing}"
i:MouseWheel.NestedMotion ="{Binding CustomOptions.NestedMotion}"
i:MouseWheel.Minimum ="{Binding RelativeSource={RelativeSource Self}, Mode=TwoWay, Path=Minimum}"
i:MouseWheel.Maximum ="{Binding RelativeSource={RelativeSource Self}, Mode=TwoWay, Path=Maximum}"
i:MouseWheel.Increment ="{Binding RelativeSource={RelativeSource Self}, Mode=TwoWay, Path=LargeChange}"
i:MouseWheel.Value ="{Binding RelativeSource={RelativeSource Self}, Mode=TwoWay, Path=Value}"
...
/>
启用 MWLib
增强型复选框绑定到 MouseWheel.Enhanced
附加属性。必须将其打开才能使 MWLib 功能生效。
请注意,您可以将此属性声明为可视树根元素的属性,或从希望启用增强功能的级别开始。
如果 MouseWheel.Enhanced
在某个元素上设置为 False,人们可能会想知道会发生什么。我们可以通过在“选项”面板中取消选中“增强型”来尝试它。滚轮的整体行为将模仿我们在“之前”应用程序中看到的本地行为。
覆盖可继承属性
这些属性可以在 WorkspaceView 的任何可视化树元素中被覆盖。滚动 \ 覆盖选项卡项展示了两个学术示例
第一个示例展示了如何自定义选项卡项可视化树,以便嵌套控件在两个方向上每格滚动 64 像素。
<!-- WorkspaceView.xaml -->
<TabItem
Header="ItemsControl - Physical(64)"
Background="{StaticResource PhysicalModeBrush}"
i:MouseWheel.ScrollMode="Physical"
i:MouseWheel.ScrollSmoothing="None"
i:MouseWheel.PhysicalScrollIncrement="64"
Content="{StaticResource L0-ItemsControl}">
</TabItem>
在第二个示例中,选项卡的嵌套控件表现如下
- 垂直滚动是逻辑的,滚动增量是垂直滚动部分中为滚轮控制面板选择的行数的两倍(2*)。
- 水平滚动是物理的,具有平滑的运动,滚动增量是水平部分中为滚轮控制面板选择的字符数的 16 倍(16*)。
<!-- WorkspaceView.xaml -->
<TabItem
Header="ListBox - V:Auto(2*) H:Smooth(16*)"
Background="{StaticResource LogicalModeBrush}"
i:MouseWheel.VScrollMode="Auto"
i:MouseWheel.HScrollMode="Physical"
i:MouseWheel.HScrollSmoothing="Smooth"
i:MouseWheel.LogicalVScrollIncrement="2*"
i:MouseWheel.PhysicalHScrollIncrement="16*"
Content="{StaticResource L0-ListBox}">
</TabItem>
关注几个选项
以下段落描述了重要选项。
滚动模式
它可以取以下值
- 自动:在此模式下,该库使用一些启发式方法(
CreateEnhancedAutoBehavior
)来决定应用逻辑模式还是物理模式。实际上,对于支持基于项的滚动元素,会使用逻辑模式,除非这些元素包含嵌套的可滚动元素。在这种情况下,以及在所有其他情况下,都会使用物理模式。 - 物理:此模式可应用于任何可滚动元素类型。它支持平滑运动控制(
CreateEnhancedPhysicalBehavior
)。
// MouseWheelScrollClient.cs
private IMouseWheelInputListener CreateEnhancedAutoBehavior()
{
if (LogicalScrollEnabled)
{
if (ScrollViewer.HasNestedScrollFrames() || HostImplementsMouseWheelEvent)
return CreateEnhancedPhysicalBehavior();
else
return CreateEnhancedLogicalBehaviorItem();
}
else
return CreateEnhancedPhysicalBehavior();
}
private IMouseWheelInputListener CreateEnhancedPhysicalBehavior()
{
switch (Smoothing)
{
case MouseWheelSmoothing.None: return CreateEnhancedPhysicalBehaviorItem();
case MouseWheelSmoothing.Linear: return CreateEnhancedLinearBehaviorItem();
case MouseWheelSmoothing.Smooth: return CreateEnhancedSmoothBehaviorItem();
default: throw new NotImplementedException();
}
}
滚动增量
它是当您将滚轮向自己或远离自己滚动一格时,滚动位置增加或减少的值。
滚动增量实现为自定义类型(ScrollIncrement
),而不是 Double
。这样,我们可以轻松定义一个按用户在鼠标滚轮控制面板中选择的垂直滚动行数或水平滚动字符数缩放的值(星号表示法)。
例如,用以下代码修改您的第一个平滑滚动应用程序
<Window
...
i:MouseWheel.Enhanced="True"
i:MouseWheel.LogicalScrollIncrement="2*">
...
</Window>
结果的垂直滚动粒度将是控制面板中行数的两倍。如果您更改此值,粒度将相应调整。
平滑滚动
可以通过两个步骤在任何元素上激活它
- 将
MouseWheel.ScrollMode
属性设置为Physical
。或者,您可以使用VScrollMode
和HScrollMode
属性来更精确地控制垂直或水平方向。 - 将
MouseWheel.ScrollSmoothing
属性设置为Smooth
:这是物理滚动模式下元素的默认设置。与以前一样,VScrollSmoothing
和HScrollSmoothing
属性可用于控制所需方向。
平滑缩放
我不建议您启用平滑缩放:它会消耗大量 CPU 时间,并且与线性缩放(默认值)相比,并不能显著增强用户体验。如果您仍然想启用它,请使用 MouseWheel.ZoomSmoothing
附加属性。
去抖
MWLib 的去抖可以通过各种属性(如 LogicalVScrollDebouncing
、ZoomDebouncing
等)激活,这些属性可以取以下值之一
- 自动 MWLib 选择最合适的去抖函数。
- 无 禁用去抖。
- 单次 每格有一个去抖单元。 滚轮的行为就像在标准分辨率模式下一样。
修订历史
2011 年 7 月 8 日
- 原始文章
2016 年 6 月 7 日
- 添加了倾斜滚轮支持
- 在github上发布了源代码