在WPF中实现平滑滚动的方法详解

作者:天方 时间:2021-06-17 06:34:16 

WPF实现滚动条还是比较方便的,只要在控件外围加上ScrollViewer即可,但美中不足的是:滚动的时候没有动画效果。在滚动的时候添加过渡动画能给我们的软件增色不少,例如Office 2013的滚动的时候支持动画看起来就舒服多了。 之前倒是研究过如何实现这个平滑滚动,不过网上的方案大部分大多数如下:

  • 通过VisualTree找到ScrollViewer

  • 在ScrollChanged事件中添加动画

这种方案效果并不好,以为我们的滚动很多时候都是一口气滚动好几格滚轮的,这个时候上一个动画还没有结束,下一个动画就来了,反而还出现了卡顿的感觉,并且网上的一些算法大部分还都会导致偏移错位。

趁着这两天有点时间,就研究了一下ScorllViewer,从MSDN文档中看到,它是支持两种滚动方式的:

物理滚动:

系统默认的滚动方案,控件本身啥都不用干,完全由ScrollViewer来实现滚动。这种方式的好处是简单,但也正由于简单,控件本身完全感知不到ScorllViewer的存在,也就无法加以控制了。

逻辑滚动:

将这种方式需要设置ScrollViewer的CanContentScroll为"True"才能生效,同时需要控件实现IScrollInfo接口。此时ScrollViewer只是将滚动事件通过IScrollInfo接口传递给控件,由控件本身自己去实现滚动。同时从IScrollInfo接口中读取相关的属性更新滚动条界面。

也就是说,逻辑滚动才是我们所需要的方案。由于它要求控件实现IScrollInfo接口,自行控制滚动。也就是说我们要实现自己的Panel,并且实现IScrollInfo接口。关于这个接口,MSDN上有一系列文章介绍过如何实现它:

  • IScrollInfo in Avalon part I

  • IScrollInfo in Avalon part II

  • IScrollInfo in Avalon part III

  • IScrollInfo in Avalon part IV

这个接口实现也不算麻烦,我倒没有细看这几篇文章,自己照着最后的一个例子尝试着弄了一阵子也弄出来了。实际上麻烦的地方不在于实现这个接口,而是实现Panel,我这里为了简单,直接继承了WrapPanel类,代码如下: 

class MyWrapPanel : WrapPanel, IScrollInfo
   {
       TranslateTransform _transForm;
       public MyWrapPanel()
       {
           _transForm = new TranslateTransform();
           this.RenderTransform = _transForm;
       }

#region Layout

Size _screenSize;
       Size _totalSize;

protected override Size MeasureOverride(Size availableSize)
       {
           _screenSize = availableSize;

if (Orientation == Orientation.Horizontal)
               availableSize = new Size(availableSize.Width, double.PositiveInfinity);
           else
               availableSize = new Size(double.PositiveInfinity, availableSize.Height);

_totalSize = base.MeasureOverride(availableSize);
           return _totalSize;
       }

protected override Size ArrangeOverride(Size finalSize)
       {
           var size = base.ArrangeOverride(finalSize);
           if (ScrollOwner != null)
           {
               _transForm.Y = -VerticalOffset;
               _transForm.X = -HorizontalOffset;

ScrollOwner.InvalidateScrollInfo();
           }
           return _screenSize;
       }
       #endregion

#region IScrollInfo

public ScrollViewer ScrollOwner { get; set; }
       public bool CanHorizontallyScroll { get; set; }
       public bool CanVerticallyScroll { get; set; }

public double ExtentHeight { get { return _totalSize.Height; } }
       public double ExtentWidth { get { return _totalSize.Width; } }

public double HorizontalOffset { get; private set; }
       public double VerticalOffset { get; private set; }

public double ViewportHeight { get { return _screenSize.Height; } }
       public double ViewportWidth { get { return _screenSize.Width; } }

void appendOffset(double x, double y)
       {
           var offset = new Vector(HorizontalOffset + x, VerticalOffset + y);

offset.Y = range(offset.Y, 0, _totalSize.Height - _screenSize.Height);
           offset.X = range(offset.X, 0, _totalSize.Width - _screenSize.Width);

HorizontalOffset = offset.X;
           VerticalOffset = offset.Y;

InvalidateArrange();
       }

double range(double value, double value1, double value2)
       {
           var min = Math.Min(value1, value2);
           var max = Math.Max(value1, value2);

value = Math.Max(value, min);
           value = Math.Min(value, max);

return value;
       }

const double _lineOffset = 30;
       const double _wheelOffset = 90;

public void LineDown()
       {
           appendOffset(0, _lineOffset);
       }

public void LineUp()
       {
           appendOffset(0, -_lineOffset);
       }

public void LineLeft()
       {
           appendOffset(-_lineOffset, 0);
       }

public void LineRight()
       {
           appendOffset(_lineOffset, 0);
       }

public Rect MakeVisible(Visual visual, Rect rectangle)
       {
           throw new NotSupportedException();
       }

public void MouseWheelDown()
       {
           appendOffset(0, _wheelOffset);
       }

public void MouseWheelUp()
       {
           appendOffset(0, -_wheelOffset);
       }

public void MouseWheelLeft()
       {
           appendOffset(0, _wheelOffset);
       }

public void MouseWheelRight()
       {
           appendOffset(_wheelOffset, 0);
       }

public void PageDown()
       {
           appendOffset(0, _screenSize.Height);
       }

public void PageUp()
       {
           appendOffset(0, -_screenSize.Height);
       }

public void PageLeft()
       {
           appendOffset(-_screenSize.Width, 0);
       }

public void PageRight()
       {
           appendOffset(_screenSize.Width, 0);
       }

public void SetVerticalOffset(double offset)
       {
           this.appendOffset(HorizontalOffset, offset - VerticalOffset);
       }

public void SetHorizontalOffset(double offset)
       {
           this.appendOffset(offset - HorizontalOffset, VerticalOffset);
       }
       #endregion
   }

基本上从代码中也能看出IScrollInfo接口的交互流程,这里就不多介绍了。

主界面代码如下: 

<ItemsControl ItemsSource="{Binding}" >
       <ItemsControl.ItemTemplate>
           <DataTemplate>
               <Border BorderThickness="1" BorderBrush="Black" Margin="8" Width="150" Height="50">
                   <Rectangle Fill="{Binding}"  />
               </Border>
           </DataTemplate>
       </ItemsControl.ItemTemplate>
       <ItemsControl.ItemsPanel>
           <ItemsPanelTemplate>
               <local:MyWrapPanel />
           </ItemsPanelTemplate>
       </ItemsControl.ItemsPanel>
       <ItemsControl.Template>
           <ControlTemplate>
               <ScrollViewer CanContentScroll="True">
                   <ItemsPresenter />
               </ScrollViewer>
           </ControlTemplate>
       </ItemsControl.Template>
   </ItemsControl>

需要注意的是,这儿需要设置<ScrollViewer CanContentScroll="True">,否则使用的不是逻辑滚动。

数据源代码如下:

var brushes = from property in typeof(Brushes).GetProperties()
                   let value = property.GetValue(null)
                   select value;

this.DataContext = brushes.Take(100).ToArray();

由于使用了IscrollInfo接口,所有的滚动操作是自己实现的,这里我是通过设置Panel的RenderTransFrom的X,Y偏移来实现滚动操作的。运行后看上去上和WrapPanel没有什么区别,但是由于是自己控制的滚动,加上动画效果也只是分分钟的事情了,把上面代码的RenderTransFrom的X,Y硬切换改成动画切换即可:

protected override Size ArrangeOverride(Size finalSize)
   {
       var size = base.ArrangeOverride(finalSize);
       if (ScrollOwner != null)
       {
           var yOffsetAnimation = new DoubleAnimation() { To = -VerticalOffset, Duration = TimeSpan.FromSeconds(0.3) };
           _transForm.BeginAnimation(TranslateTransform.YProperty, yOffsetAnimation);

var xOffsetAnimation = new DoubleAnimation() { To = -HorizontalOffset, Duration = TimeSpan.FromSeconds(0.3) };
           _transForm.BeginAnimation(TranslateTransform.XProperty, xOffsetAnimation);

ScrollOwner.InvalidateScrollInfo();
       }
       return _screenSize;
   }

对于其它的Panel,如Grid,DockPanel等,基本上也可以按照这种方式实现,IScrollInfo接口处基本上可以保持不变,只需要重写MeasureOverride和ArrangeOverride两个函数即可。一个特殊的控件是StackPanel,由于它本身已经实现了IScrollInfo接口,也就是说它本身就有自身的自绘制滚动的方案,并且没有提供接口在覆盖自身的自绘制滚动,因此我们需要自己写一个StackPanel,好在实现StackPanel并不难,由于篇幅有限,这里我懒得继续写了,读者朋友自己实现吧。至于那些非Panel的控件,实现就更简单了,也留着读者朋友自己实现吧。

来源:https://www.cnblogs.com/TianFang/p/4198731.html

标签:WPF,平滑,滚动
0
投稿

猜你喜欢

  • Springboot Mybatis Plus自动生成工具类详解代码

    2022-09-17 12:01:57
  • JAVA格式化时间日期的简单实例

    2022-10-06 09:14:58
  • 浅谈Java slf4j日志简单理解

    2021-07-07 15:49:15
  • Java如何使用Query动态拼接SQL详解

    2022-12-31 09:40:14
  • Spring中集成Groovy的四种方式(小结)

    2023-07-11 16:36:53
  • 多线程(多窗口卖票实例讲解)

    2021-09-02 02:47:26
  • Spring学习笔记之bean的基础知识

    2021-09-08 10:09:27
  • SpringBoot整合Elasticsearch并实现CRUD操作

    2021-10-28 07:27:31
  • java 多态性详解及简单实例

    2021-10-06 23:29:50
  • 基于SPRINGBOOT配置文件占位符过程解析

    2021-06-27 04:25:12
  • Springboot整合支付宝支付功能

    2023-07-02 17:38:09
  • Unity3D运行报DllNotFoundException错误的解决方案

    2021-09-13 00:22:35
  • Java8 使用工厂方法supplyAsync创建CompletableFuture实例

    2023-02-14 03:57:22
  • java struts2学习笔记之线程安全

    2022-08-07 00:13:07
  • mybatis-plus 扩展批量新增的实现

    2023-07-14 14:24:13
  • 浅谈java常用的几种线程池比较

    2021-07-24 01:28:23
  • java实现多个文件压缩成压缩包

    2022-12-06 17:45:54
  • SpringBoot2.0 ZipKin示例代码

    2022-11-25 00:24:40
  • 快速解决Android适配底部返回键等虚拟键盘的问题

    2021-10-25 14:50:23
  • 深入Unix时间戳与C# DateTime时间类型互换的详解

    2023-06-04 21:48:11
  • asp之家 软件编程 m.aspxhome.com