WPF实现滚动条还是比较方便的,只要在控件外围加上ScrollViewer即可,但美中不足的是:滚动的时候没有动画效果。在滚动的时候添加过渡动画能给我们的软件增色不少,例如Office 2013的滚动的时候支持动画看起来就舒服多了。 之前倒是研究过如何实现这个平滑滚动,不过网上的方案大部分大多数如下:
-
通过VisualTree找到ScrollViewer
-
在ScrollChanged事件中添加动画
这种方案效果并不好,以为我们的滚动很多时候都是一口气滚动好几格滚轮的,这个时候上一个动画还没有结束,下一个动画就来了,反而还出现了卡顿的感觉,并且网上的一些算法大部分还都会导致偏移错位。
趁着这两天有点时间,就研究了一下ScorllViewer,从MSDN文档中看到,它是支持两种滚动方式的:
物理滚动:
系统默认的滚动方案,控件本身啥都不用干,完全由ScrollViewer来实现滚动。这种方式的好处是简单,但也正由于简单,控件本身完全感知不到ScorllViewer的存在,也就无法加以控制了。
逻辑滚动:
将这种方式需要设置ScrollViewer的CanContentScroll为"True"才能生效,同时需要控件实现IScrollInfo接口。此时ScrollViewer只是将滚动事件通过IScrollInfo接口传递给控件,由控件本身自己去实现滚动。同时从IScrollInfo接口中读取相关的属性更新滚动条界面。
也就是说,逻辑滚动才是我们所需要的方案。由于它要求控件实现IScrollInfo接口,自行控制滚动。也就是说我们要实现自己的Panel,并且实现IScrollInfo接口。关于这个接口,MSDN上有一系列文章介绍过如何实现它:
这个接口实现也不算麻烦,我倒没有细看这几篇文章,自己照着最后的一个例子尝试着弄了一阵子也弄出来了。实际上麻烦的地方不在于实现这个接口,而是实现Panel,我这里为了简单,直接继承了WrapPanel类,代码如下:
1 class MyWrapPanel : WrapPanel, IScrollInfo 2 { 3 TranslateTransform _transForm; 4 public MyWrapPanel() 5 { 6 _transForm = new TranslateTransform(); 7 this.RenderTransform = _transForm; 8 } 9 10 #region Layout 11 12 Size _screenSize; 13 Size _totalSize; 14 15 protected override Size MeasureOverride(Size availableSize) 16 { 17 _screenSize = availableSize; 18 19 if (Orientation == Orientation.Horizontal) 20 availableSize = new Size(availableSize.Width, double.PositiveInfinity); 21 else 22 availableSize = new Size(double.PositiveInfinity, availableSize.Height); 23 24 _totalSize = base.MeasureOverride(availableSize); 25 return _totalSize; 26 } 27 28 protected override Size ArrangeOverride(Size finalSize) 29 { 30 var size = base.ArrangeOverride(finalSize); 31 if (ScrollOwner != null) 32 { 33 _transForm.Y = -VerticalOffset; 34 _transForm.X = -HorizontalOffset; 35 36 ScrollOwner.InvalidateScrollInfo(); 37 } 38 return _screenSize; 39 } 40 #endregion 41 42 #region IScrollInfo 43 44 public ScrollViewer ScrollOwner { get; set; } 45 public bool CanHorizontallyScroll { get; set; } 46 public bool CanVerticallyScroll { get; set; } 47 48 public double ExtentHeight { get { return _totalSize.Height; } } 49 public double ExtentWidth { get { return _totalSize.Width; } } 50 51 public double HorizontalOffset { get; private set; } 52 public double VerticalOffset { get; private set; } 53 54 public double ViewportHeight { get { return _screenSize.Height; } } 55 public double ViewportWidth { get { return _screenSize.Width; } } 56 57 void appendOffset(double x, double y) 58 { 59 var offset = new Vector(HorizontalOffset + x, VerticalOffset + y); 60 61 offset.Y = range(offset.Y, 0, _totalSize.Height - _screenSize.Height); 62 offset.X = range(offset.X, 0, _totalSize.Width - _screenSize.Width); 63 64 HorizontalOffset = offset.X; 65 VerticalOffset = offset.Y; 66 67 InvalidateArrange(); 68 } 69 70 double range(double value, double value1, double value2) 71 { 72 var min = Math.Min(value1, value2); 73 var max = Math.Max(value1, value2); 74 75 value = Math.Max(value, min); 76 value = Math.Min(value, max); 77 78 return value; 79 } 80 81 82 const double _lineOffset = 30; 83 const double _wheelOffset = 90; 84 85 public void LineDown() 86 { 87 appendOffset(0, _lineOffset); 88 } 89 90 public void LineUp() 91 { 92 appendOffset(0, -_lineOffset); 93 } 94 95 public void LineLeft() 96 { 97 appendOffset(-_lineOffset, 0); 98 } 99 100 public void LineRight() 101 { 102 appendOffset(_lineOffset, 0); 103 } 104 105 public Rect MakeVisible(Visual visual, Rect rectangle) 106 { 107 throw new NotSupportedException(); 108 } 109 110 public void MouseWheelDown() 111 { 112 appendOffset(0, _wheelOffset); 113 } 114 115 public void MouseWheelUp() 116 { 117 appendOffset(0, -_wheelOffset); 118 } 119 120 public void MouseWheelLeft() 121 { 122 appendOffset(0, _wheelOffset); 123 } 124 125 public void MouseWheelRight() 126 { 127 appendOffset(_wheelOffset, 0); 128 } 129 130 public void PageDown() 131 { 132 appendOffset(0, _screenSize.Height); 133 } 134 135 public void PageUp() 136 { 137 appendOffset(0, -_screenSize.Height); 138 } 139 140 public void PageLeft() 141 { 142 appendOffset(-_screenSize.Width, 0); 143 } 144 145 public void PageRight() 146 { 147 appendOffset(_screenSize.Width, 0); 148 } 149 150 public void SetVerticalOffset(double offset) 151 { 152 this.appendOffset(HorizontalOffset, offset - VerticalOffset); 153 } 154 155 public void SetHorizontalOffset(double offset) 156 { 157 this.appendOffset(offset - HorizontalOffset, VerticalOffset); 158 } 159 #endregion 160 }
基本上从代码中也能看出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的控件,实现就更简单了,也留着读者朋友自己实现吧。