本文是分析 .net Framework 源代碼的系列,主要告訴大家微軟做 ScrollViewer 的思路,分析很簡單 看完本文,可以學會如何寫一個 ScrollViewer ,如何定義一個 IScrollInfo 或者給他滾動添加動畫 ...
本文是分析 .net Framework 源代碼的系列,主要告訴大家微軟做 ScrollViewer 的思路,分析很簡單
看完本文,可以學會如何寫一個 ScrollViewer ,如何定義一個 IScrollInfo 或者給他滾動添加動畫
使用
下麵告訴大家如何簡單使用 ScrollViewer ,一般在需要滾動的控制項外面放一個 ScrollViewer 就可以實現滾動。
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<TextBlock TextWrapping="Wrap" Margin="0,0,0,20">Scrolling is enabled when it is necessary.
Resize the window, making it larger and smaller.</TextBlock>
<Rectangle Fill="Red" Width="500" Height="500"></Rectangle>
</StackPanel>
</ScrollViewer>
但不是所有的控制項外面放一個 ScrollViewer 都能實現滾動,因為滾動實際上需要控制項自己做。
原理
下麵來告訴大家滾動是如何做的。
一個最簡單的方法是設置元素的 transForm.Y
通過這個方式進行滾動是最簡單的方法,但是缺點是其他控制項不能做其他的移動。
在 ScrollViewer 存在兩個滾動方式,物理滾動 和 邏輯滾動,如果使用 物理滾動 那麼滾動就是ScrollViewer做的,如何使用邏輯滾動,那麼滾動就是控制項自己做的。
那麼我從 ScrollViewer 接收輸入開始講起
輸入
如果大家使用 ScrollViewer 進行滾動,那麼也許會遇到一個神奇的需求,如何在觸摸下滾動。是的,如果使用一個簡單的 ScrollViewer 是無法使用觸摸滾動
請看代碼,寫一個簡單的 ScrollViewer 裡面有一些矩形,可以看到這時可以進行滑鼠滾動,但是觸摸是無法滾動。
<Grid>
<ScrollViewer>
<StackPanel x:Name="HcrkKmqnnfzo"></StackPanel>
</ScrollViewer>
</Grid>
在後臺遍歷顏色然後添加
public MainWindow()
{
InitializeComponent();
foreach (var temp in typeof(Brushes)
.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
.Select(temp => temp.GetValue(null, null)))
{
var rectangle = new Rectangle
{
Height = 20,
Fill = (Brush)temp
};
HcrkKmqnnfzo.Children.Add(rectangle);
}
}
代碼:WPF ScrollView 代碼解釋 1.1-CSDN下載
如果沒有csdn積分,嘗試使用 我的網盤,但是我的網盤如果過期請告訴我
如果需要在觸摸使用滾動,那麼需要設置PanningMode
,可以設置支持垂直拖動。
如果這時設置了PanningMode
,就會發現拖動時讓視窗抖動,這時需要在視窗重寫 OnManipulationBoundaryFeedback ,請看下麵代碼。函數裡面什麼都不要寫,詳細請看 https://stackoverflow.com/a/6918131/6116637
protected override void OnManipulationBoundaryFeedback(ManipulationBoundaryFeedbackEventArgs e)
{
}
修改後的代碼:WPF ScrollView 代碼解釋 1.2-CSDN下載
那麼在滑鼠滾動是如何收到滾動?
從微軟源代碼可以看到 ScrollViewer 繼承 ContentControl,所以可以重寫 OnMouseWheel ,請看他的代碼
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if (e.Handled) { return; }
if (!HandlesMouseWheelScrolling)
{
return;
}
if (ScrollInfo != null)
{
if (e.Delta < 0) { ScrollInfo.MouseWheelDown(); }
else { ScrollInfo.MouseWheelUp(); }
}
e.Handled = true;
}
實際上 ScrollViewer 是不做滾動的,實際的滾動是 ScrollInfo 進行滾動。
ScrollInfo
那麼 ScrollInfo 是什麼,實際上他是一個介面,在 ScrollViewer 裡面放的控制項實際上不是直接放在 ScrollViewer 里,控制項是放在 ScrollContentPresenter
,而 ScrollContentPresenter 是寫在 ScrollViewer 的 Style 里,在 ScrollViewer 可以看到這個代碼
[TemplatePart(Name = "PART_ScrollContentPresenter", Type = typeof(ScrollContentPresenter))]
但是從垃圾微軟的代碼可以看到,沒有屬性直接使用這個,而是在使用的地方這樣寫GetTemplateChild(ScrollContentPresenterTemplateName) as ScrollContentPresenter;
這樣寫的性能是比較差的。
那麼他是如何給 ScrollInfo 賦值?實際上在這個類的 HookupScrollingComponents 就是給 ScrollInfo 賦值,在 HookupScrollingComponents 調用的地方就是 OnApplyTemplate 所以大家可以看到,在初始化的時候就已經知道了控制項。
從垃圾微軟的源代碼可以看到 HookupScrollingComponents 的邏輯,首先是判斷屬性CanContentScroll
判斷元素里的控制項是否可以滾動,如果元素里的控制項可以滾動,那麼再判斷元素里的控制項是不是繼承IScrollInfo
如果是的話,嗯,沒了,就把 ScrollInfo 賦值。如果裡面的控制項不是繼承IScrollInfo
,那麼判斷一下他是不是處於列表,如果是的話就拿列表ItemsPresenter
作為ScrollInfo。如果還是拿不到,只好用自己作為ScrollInfo
從這裡可以看到 CanContentScroll 如果沒有設置,就直接使用這個類,也就是物理滾動就是這個類做的。如果一個元素不在列表內,不繼承 IScrollInfo 那麼即使設置使用邏輯滾動,實際上也是物理滾動。物理滾動就是元素不知道滾動,所有的移動都是元素無法控制。和物理滾動不同,邏輯的就是元素控制所有滾動。
物理滾動
下麵來告訴大家,物理滾動是如何做,實際上的滾動就是在佈局中使用下麵的代碼,讓元素佈局在滾動的地方,所以看起來就是元素滾動
Rect childRect = new Rect(child.DesiredSize);
if (IsScrollClient)
{
childRect.X = -HorizontalOffset;
childRect.Y = -VerticalOffset;
}
//this is needed to stretch the child to arrange space,
childRect.Width = Math.Max(childRect.Width, arrangeSize.Width);
childRect.Height = Math.Max(childRect.Height, arrangeSize.Height);
child.Arrange(childRect);
可以看到佈局設置反過來的 HorizontalOffset 作為元素的 x 移動,通過這樣就可以讓元素移動
但是元素如果移動在 ScrollViewer 外面,如何裁剪?實際上就是使用重寫了 GetLayoutClip 進行裁剪
return new RectangleGeometry(new Rect(RenderSize));
從代碼可以知道,實際上的 ScrollViewer 是不會滾動元素的,滾動元素的是 ScrollViewer 裡面的元素,滾動的方式一般都使用在佈局的時候設置元素的 X、Y 來讓元素滾動。我看了 StackPanel 和其他幾個類,都是使用這個方式,因為對比 Translate 的方式,這個方法不會用到 Translate 也就不會在用戶修改 Translate 的時候無法移動。另外這個方法是在佈局做的,直接計算,如果修改 Translate 還需要在佈局重新計算,所以這個方法的性能會比較高。
觸摸輸入
那麼 ScrollViewer 是如何在觸摸的時候獲得輸入?實際上在觸摸的時候用的是 Manipulation ,在判斷 PanningMode 給值
if (panningMode == PanningMode.HorizontalOnly)
{
e.Mode = ManipulationModes.TranslateX;
}
else if (panningMode == PanningMode.VerticalOnly)
{
e.Mode = ManipulationModes.TranslateY;
}
else
{
e.Mode = ManipulationModes.Translate;
}
所以在 ManipulationDelta 可以拿到移動的值,因為直接拿到的值就是用戶希望的路徑所以直接設置不需要計算
但是需要倍數 PanningRatio ,如果需要慣性,那麼只需要設置慣性就可以。
大概整個源代碼只有這些,很多的代碼都是在判斷邊界,還有處理一些用戶輸入。
在觸摸的時候,核心的代碼是 ManipulateScroll ,傳入了當前的移動和累計的移動、是否水平移動。通過判斷當前的移動是否有移動然後乘以倍數,然後通過設置 HorizontalOffset 這幾個屬性的值,重新佈局就可以。
所以所有的代碼實際上就是獲得輸入,然後傳入給對應的 ScrollInfo ,通過 ScrollInfo 實現的方法做具體的業務。
不過 ScrollViewer 不是直接傳入 ScrollInfo 需要移動的,而且發送命令
public void ScrollToHorizontalOffset(double offset)
{
double validatedOffset = ScrollContentPresenter.ValidateInputOffset(offset, "offset");
// Queue up the scroll command, which tells the content to scroll.
// Will lead to an update of all offsets (both live and deferred).
EnqueueCommand(Commands.SetHorizontalOffset, validatedOffset, null);
}
然後在具體的函數 ExecuteNextCommand 拿出一個個的命令,進行移動
private bool ExecuteNextCommand()
{
IScrollInfo isi = ScrollInfo;
Command cmd = _queue.Fetch();
switch(cmd.Code)
{
case Commands.LineUp: isi.LineUp(); break;
case Commands.LineDown: isi.LineDown(); break;
case Commands.LineLeft: isi.LineLeft(); break;
case Commands.LineRight: isi.LineRight(); break;
//去掉差不多的代碼
case Commands.Invalid: return false;
}
return true;
}
在輸入的時候可能輸入太快,而佈局不是立刻進行佈局,從代碼可以看到,移動的業務就是在佈局修改值,但是佈局修改不是優先順序很高的,但是輸入的優先順序是很高的,可能在佈局的過程就不停輸入。所以就需要把輸入的命令放入,使用一個函數一個個拿出來,對不同的命令處理,最後再佈局。
參見:
IScrollInfo in Avalon part I – BenCon's WebLog
IScrollInfo in Avalon part II – BenCon's WebLog
IScrollInfo in Avalon part III – BenCon's WebLog
IScrollInfo tutorial part IV – BenCon's WebLog
其他源代碼分析
.net Framework 源代碼 · ScrollViewer
一站式WPF--依賴屬性(DependencyProperty)一 - 周永恆 - 博客園
我搭建了自己的博客 https://lindexi.gitee.io/ 歡迎大家訪問,裡面有很多新的博客。只有在我看到博客寫成熟之後才會放在csdn或博客園,但是一旦發佈了就不再更新
如果在博客看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名林德熙(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我聯繫。