在WPF中实现平滑滚动
WPF實(shí)現(xiàn)滾動(dòng)條還是比較方便的,只要在控件外圍加上ScrollViewer即可,但美中不足的是:滾動(dòng)的時(shí)候沒有動(dòng)畫效果。在滾動(dòng)的時(shí)候添加過(guò)渡動(dòng)畫能給我們的軟件增色不少,例如Office 2013的滾動(dòng)的時(shí)候支持動(dòng)畫看起來(lái)就舒服多了。 之前倒是研究過(guò)如何實(shí)現(xiàn)這個(gè)平滑滾動(dòng),不過(guò)網(wǎng)上的方案大部分大多數(shù)如下:
這種方案效果并不好,以為我們的滾動(dòng)很多時(shí)候都是一口氣滾動(dòng)好幾格滾輪的,這個(gè)時(shí)候上一個(gè)動(dòng)畫還沒有結(jié)束,下一個(gè)動(dòng)畫就來(lái)了,反而還出現(xiàn)了卡頓的感覺,并且網(wǎng)上的一些算法大部分還都會(huì)導(dǎo)致偏移錯(cuò)位。
趁著這兩天有點(diǎn)時(shí)間,就研究了一下ScorllViewer,從MSDN文檔中看到,它是支持兩種滾動(dòng)方式的:
物理滾動(dòng):
系統(tǒng)默認(rèn)的滾動(dòng)方案,控件本身啥都不用干,完全由ScrollViewer來(lái)實(shí)現(xiàn)滾動(dòng)。這種方式的好處是簡(jiǎn)單,但也正由于簡(jiǎn)單,控件本身完全感知不到ScorllViewer的存在,也就無(wú)法加以控制了。
邏輯滾動(dòng):
將這種方式需要設(shè)置ScrollViewer的CanContentScroll為"True"才能生效,同時(shí)需要控件實(shí)現(xiàn)IScrollInfo接口。此時(shí)ScrollViewer只是將滾動(dòng)事件通過(guò)IScrollInfo接口傳遞給控件,由控件本身自己去實(shí)現(xiàn)滾動(dòng)。同時(shí)從IScrollInfo接口中讀取相關(guān)的屬性更新滾動(dòng)條界面。
也就是說(shuō),邏輯滾動(dòng)才是我們所需要的方案。由于它要求控件實(shí)現(xiàn)IScrollInfo接口,自行控制滾動(dòng)。也就是說(shuō)我們要實(shí)現(xiàn)自己的Panel,并且實(shí)現(xiàn)IScrollInfo接口。關(guān)于這個(gè)接口,MSDN上有一系列文章介紹過(guò)如何實(shí)現(xiàn)它:
- IScrollInfo in Avalon part I
- IScrollInfo in Avalon part II
- IScrollInfo in Avalon part III
- IScrollInfo in Avalon part IV
這個(gè)接口實(shí)現(xiàn)也不算麻煩,我倒沒有細(xì)看這幾篇文章,自己照著最后的一個(gè)例子嘗試著弄了一陣子也弄出來(lái)了。實(shí)際上麻煩的地方不在于實(shí)現(xiàn)這個(gè)接口,而是實(shí)現(xiàn)Panel,我這里為了簡(jiǎn)單,直接繼承了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 } View Code基本上從代碼中也能看出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>需要注意的是,這兒需要設(shè)置<ScrollViewer CanContentScroll="True">,否則使用的不是邏輯滾動(dòng)。
數(shù)據(jù)源代碼如下:
var brushes = from property in typeof(Brushes).GetProperties()let value = property.GetValue(null)select value;this.DataContext = brushes.Take(100).ToArray();
由于使用了IscrollInfo接口,所有的滾動(dòng)操作是自己實(shí)現(xiàn)的,這里我是通過(guò)設(shè)置Panel的RenderTransFrom的X,Y偏移來(lái)實(shí)現(xiàn)滾動(dòng)操作的。運(yùn)行后看上去上和WrapPanel沒有什么區(qū)別,但是由于是自己控制的滾動(dòng),加上動(dòng)畫效果也只是分分鐘的事情了,把上面代碼的RenderTransFrom的X,Y硬切換改成動(dòng)畫切換即可:
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;}
對(duì)于其它的Panel,如Grid,DockPanel等,基本上也可以按照這種方式實(shí)現(xiàn),IScrollInfo接口處基本上可以保持不變,只需要重寫MeasureOverride和ArrangeOverride兩個(gè)函數(shù)即可。一個(gè)特殊的控件是StackPanel,由于它本身已經(jīng)實(shí)現(xiàn)了IScrollInfo接口,也就是說(shuō)它本身就有自身的自繪制滾動(dòng)的方案,并且沒有提供接口在覆蓋自身的自繪制滾動(dòng),因此我們需要自己寫一個(gè)StackPanel,好在實(shí)現(xiàn)StackPanel并不難,由于篇幅有限,這里我懶得繼續(xù)寫了,讀者朋友自己實(shí)現(xiàn)吧。至于那些非Panel的控件,實(shí)現(xiàn)就更簡(jiǎn)單了,也留著讀者朋友自己實(shí)現(xiàn)吧。
總結(jié)
以上是生活随笔為你收集整理的在WPF中实现平滑滚动的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 解析solidity的event log
- 下一篇: 解决WPF中重载Window.OnRen