zoukankan      html  css  js  c++  java
  • .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 职业技术学院 欢迎大家加入

    知识共享许可协议
    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

  • 相关阅读:
    课程作业(一)
    PTA 5-3 解题报告
    作业(四)
    C#循环语句整理
    C#数组和集合整理
    作业(三)
    1.0总结
    如何在C++中产生随机数
    CLSRSC-400: A system reboot is required to continue installing.
    Upgrade a Non-CDB To a PDB on CDB
  • 原文地址:https://www.cnblogs.com/lindexi/p/8970485.html
Copyright © 2011-2022 走看看