WPF 多个 ScrollViewer 滚动同步

0、前言

在进行 WPF 程序开发时,有时界面内容分别放在不同的滚动区域中,也就是在不同的 ScrollViewer 控件中,默认情况下,它们各自的内容滚动是互不影响的,这也符合大部分的场景。但是偏偏就有这么一些场景,需要让它们虽然身处不同区域,但是其中一个区域内容滚动后,另外的一个或几个滚动区域也要同步滚动,以达到 具有关联性的内容同时出现或消失在视野中 的效果。那么如何实现呢?本文将分两个方面介绍,二者相辅相成,共同达成目标,和本文的主题有异曲同工之妙。.

1、ScrollChanged 事件控制同步滚动

可以看到界面上有 3 个滚动区域,分别是左侧滚动区 leftScrollViewer(垂直滚动条隐藏,水平滚动条禁用)、顶部滚动区 topScrollViewer(垂直滚动条禁用,水平滚动条隐藏)、以及中部滚动区 centerScrollViewer(水平和垂直滚动条都设为自动),最主要的是,三者都各自设置了 ScrollChanged 事件处理方法:

WPF 多个 ScrollViewer 滚动同步

三者的滚动事件处理方法如下,简而言之就是:左侧的垂直滚动区带动中部滚动区垂直滚动,顶部水平滚动区带动中部滚动区水平滚动,中部滚动区带动左侧和顶部滚动区相应地滚动,使用的参数都是各自方向的偏移量。

WPF 多个 ScrollViewer 滚动同步

可以看到,滚动后,三个区域的内容是相互对齐的:

WPF 多个 ScrollViewer 滚动同步

但是当中部滚动区在垂直或水平方向上分别滚到底时就露馅了,与左侧和顶部内容产生了错位:

WPF 多个 ScrollViewer 滚动同步

可以通过一个动图来更直观地展示:

WPF 多个 ScrollViewer 滚动同步

那么这是什么情况呢?其实就是滚动条导致的。因为既然要求三个区域内容对齐,那么无论是在行还是列上,单个内容的数目和尺寸都是一样的,那么在一个方向上内容的总体尺寸(宽度或高度)也是一致的;此时一方有滚动条,一方没有,就导致了总体尺寸的不一致(猜测是有滚动条的一方会给内部内容加上滚动条的 宽度 / 高度 这样一个冗余尺寸,不然不就被挡住了嘛);最终就在有滚动条的一方滚动到最远端时,没有滚动条的一方无法满足相应的偏移量(按照之前的猜测,就是由于没被加上冗余尺寸),产生了错位。

2、一方隐藏滚动条后错位的解决方法

既然猜测是由于没有(应该说是被隐藏了)滚动条的一方未被加上冗余尺寸(滚动条的 宽度 / 高度)导致的错位,那我们就对症下药,分别在对应的区域末尾加上这么一个冗余尺寸即可。

将滚动区域原先的内容放在 StackPanel 中,再在 StackPanel 内部末尾添加一个 Border 元素,宽度或高度设为显示了滚动条一方的滚动条的 宽度 (垂直滚动条) 或 高度 (水平滚动条):

WPF 多个 ScrollViewer 滚动同步

滚动条的高度或宽度获取方式如下(使用了 CalcBinding):

xmlns:calc="clr-namespace:CalcBinding;assembly=CalcBinding"
<Border Width="{calc:Binding 'ActualWidth-ViewportWidth > 0 ? ActualWidth-ViewportWidth : 0', ElementName=centerScrollViewer}"/><Border Height="{calc:Binding 'ActualHeight-ViewportHeight > 0 ? ActualHeight-ViewportHeight : 0', ElementName=centerScrollViewer}"/>

解释(以宽度为例):滚动区域的总体宽度(包括了垂直滚动条的宽度)就是 ActualWidth,内容区域宽度就是 ViewportWidth,二者相减即为垂直滚动条的宽度。一开始只绑定了 'ActualWidth-ViewportWidth',观察发现在改变窗口尺寸时(应该是滚动条刚出现时),会有负数的情况,导致出现绑定错误,虽然不会使程序出错,不过还是处理掉比较好,所以加了个三元表达式过滤掉负数。

吐槽:框架竟然没有提供直接能获取滚动条 宽度 / 高度 的属性!(当然也有可能是我没找到)

总之,通过这种方式加上冗余尺寸后,问题就解决了,效果如下:

WPF 多个 ScrollViewer 滚动同步

为了看得更清楚,可以给该区域加上红色背景:

WPF 多个 ScrollViewer 滚动同步

大家可以自己运行一下试试,代码地址:

https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20230531

全文完,感谢阅读。