天天看點

.net Framework 源代碼 · ScrollViewer

本文是分析 .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;
        }
           

在輸入的時候可能輸入太快,而布局不是立刻進行布局,從代碼可以看到,移動的業務就是在布局修改值,但是布局修改不是優先級很高的,但是輸入的優先級是很高的,可能在布局的過程就不停輸入。是以就需要把輸入的指令放入,使用一個函數一個個拿出來,對不同的指令處理,最後再布局。

參見:

在WPF中實作平滑滾動 - 天方 - 部落格園

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

.net源碼分析 – List - 布魯克石 - 部落格園

一站式WPF--依賴屬性(DependencyProperty)一 - 周永恒 - 部落格園

我搭建了自己的部落格 https://lindexi.gitee.io/ 歡迎大家通路,裡面有很多新的部落格。隻有在我看到部落格寫成熟之後才會放在csdn或部落格園,但是一旦釋出了就不再更新

如果在部落格看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入

.net Framework 源代碼 · ScrollViewer

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。歡迎轉載、使用、重新釋出,但務必保留文章署名林德熙(包含連結:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯系。

部落格園部落格隻做備份,部落格釋出就不再更新,如果想看最新部落格,請到 https://blog.lindexi.com/

.net Framework 源代碼 · ScrollViewer

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。歡迎轉載、使用、重新釋出,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含連結:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我[聯系](mailto:[email protected])。