在WPF中實(shí)現(xiàn)平滑滾動(dòng)的方法詳解
WPF實(shí)現(xiàn)滾動(dòng)條還是比較方便的,只要在控件外圍加上ScrollViewer即可,但美中不足的是:滾動(dòng)的時(shí)候沒有動(dòng)畫效果。在滾動(dòng)的時(shí)候添加過渡動(dòng)畫能給我們的軟件增色不少,例如Office 2013的滾動(dòng)的時(shí)候支持動(dòng)畫看起來就舒服多了。 之前倒是研究過如何實(shí)現(xiàn)這個(gè)平滑滾動(dòng),不過網(wǎng)上的方案大部分大多數(shù)如下:
通過VisualTree找到ScrollViewer
在ScrollChanged事件中添加動(dòng)畫
這種方案效果并不好,以為我們的滾動(dòng)很多時(shí)候都是一口氣滾動(dòng)好幾格滾輪的,這個(gè)時(shí)候上一個(gè)動(dòng)畫還沒有結(jié)束,下一個(gè)動(dòng)畫就來了,反而還出現(xiàn)了卡頓的感覺,并且網(wǎng)上的一些算法大部分還都會(huì)導(dǎo)致偏移錯(cuò)位。
趁著這兩天有點(diǎn)時(shí)間,就研究了一下ScorllViewer,從MSDN文檔中看到,它是支持兩種滾動(dòng)方式的:
物理滾動(dòng):
系統(tǒng)默認(rèn)的滾動(dòng)方案,控件本身啥都不用干,完全由ScrollViewer來實(shí)現(xiàn)滾動(dòng)。這種方式的好處是簡單,但也正由于簡單,控件本身完全感知不到ScorllViewer的存在,也就無法加以控制了。
邏輯滾動(dòng):
將這種方式需要設(shè)置ScrollViewer的CanContentScroll為"True"才能生效,同時(shí)需要控件實(shí)現(xiàn)IScrollInfo接口。此時(shí)ScrollViewer只是將滾動(dòng)事件通過IScrollInfo接口傳遞給控件,由控件本身自己去實(shí)現(xiàn)滾動(dòng)。同時(shí)從IScrollInfo接口中讀取相關(guān)的屬性更新滾動(dòng)條界面。
也就是說,邏輯滾動(dòng)才是我們所需要的方案。由于它要求控件實(shí)現(xiàn)IScrollInfo接口,自行控制滾動(dòng)。也就是說我們要實(shí)現(xiàn)自己的Panel,并且實(shí)現(xiàn)IScrollInfo接口。關(guān)于這個(gè)接口,MSDN上有一系列文章介紹過如何實(shí)現(xiàn)它:
這個(gè)接口實(shí)現(xiàn)也不算麻煩,我倒沒有細(xì)看這幾篇文章,自己照著最后的一個(gè)例子嘗試著弄了一陣子也弄出來了。實(shí)際上麻煩的地方不在于實(shí)現(xiàn)這個(gè)接口,而是實(shí)現(xiàn)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>需要注意的是,這兒需要設(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)的,這里我是通過設(shè)置Panel的RenderTransFrom的X,Y偏移來實(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接口,也就是說它本身就有自身的自繪制滾動(dòng)的方案,并且沒有提供接口在覆蓋自身的自繪制滾動(dòng),因此我們需要自己寫一個(gè)StackPanel,好在實(shí)現(xiàn)StackPanel并不難,由于篇幅有限,這里我懶得繼續(xù)寫了,讀者朋友自己實(shí)現(xiàn)吧。至于那些非Panel的控件,實(shí)現(xiàn)就更簡單了,也留著讀者朋友自己實(shí)現(xiàn)吧。
到此這篇關(guān)于WPF實(shí)現(xiàn)平滑滾動(dòng)的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C#多線程編程之使用ReaderWriterLock類實(shí)現(xiàn)多用戶讀與單用戶寫同步的方法
這篇文章主要介紹了C#多線程編程之使用ReaderWriterLock類實(shí)現(xiàn)多用戶讀與單用戶寫同步的方法,涉及C#多線程操作讀寫鎖定的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
利用C#/VB.NET實(shí)現(xiàn)PPT轉(zhuǎn)換為HTML
利用PowerPoint可以很方便的呈現(xiàn)多媒體信息,且信息形式多媒體化,表現(xiàn)力強(qiáng)。但難免在某些情況下我們會(huì)需要將PowerPoint轉(zhuǎn)換為HTML格式,本文就為大家整理了轉(zhuǎn)換方法,希望對(duì)大家有所幫助2023-05-05
跳一跳自動(dòng)跳躍C#代碼實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了跳一跳自動(dòng)跳躍C#代碼實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
C#從數(shù)據(jù)庫讀取數(shù)據(jù)到DataSet并保存到xml文件的方法
這篇文章主要介紹了C#從數(shù)據(jù)庫讀取數(shù)據(jù)到DataSet并保存到xml文件的方法,涉及C#操作DataSet保存到XML文件的技巧,需要的朋友可以參考下2015-04-04
C#實(shí)現(xiàn)對(duì)字符串進(jìn)行大小寫切換的方法
這篇文章主要介紹了C#實(shí)現(xiàn)對(duì)字符串進(jìn)行大小寫切換的方法,涉及C#操作字符串的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03

