1. 程式人生 > >.net Framework 源代碼 · ScrollViewer

.net Framework 源代碼 · ScrollViewer

auto viewer 是的 archive depend line enter panel 源代碼分析

本文是分析 .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 職業技術學院 歡迎大家加入

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

.net Framework 源代碼 · ScrollViewer