zoukankan      html  css  js  c++  java
  • 在WPF中实现平滑滚动

    WPF实现滚动条还是比较方便的,只要在控件外围加上ScrollViewer即可,但美中不足的是:滚动的时候没有动画效果。在滚动的时候添加过渡动画能给我们的软件增色不少,例如Office 2013的滚动的时候支持动画看起来就舒服多了。 之前倒是研究过如何实现这个平滑滚动,不过网上的方案大部分大多数如下:

    1. 通过VisualTree找到ScrollViewer
    2. 在ScrollChanged事件中添加动画

    这种方案效果并不好,以为我们的滚动很多时候都是一口气滚动好几格滚轮的,这个时候上一个动画还没有结束,下一个动画就来了,反而还出现了卡顿的感觉,并且网上的一些算法大部分还都会导致偏移错位。

    趁着这两天有点时间,就研究了一下ScorllViewer,从MSDN文档中看到,它是支持两种滚动方式的:

    物理滚动:

    系统默认的滚动方案,控件本身啥都不用干,完全由ScrollViewer来实现滚动。这种方式的好处是简单,但也正由于简单,控件本身完全感知不到ScorllViewer的存在,也就无法加以控制了。

    逻辑滚动:

    将这种方式需要设置ScrollViewer的CanContentScroll"True"才能生效,同时需要控件实现IScrollInfo接口。此时ScrollViewer只是将滚动事件通过IScrollInfo接口传递给控件,由控件本身自己去实现滚动。同时从IScrollInfo接口中读取相关的属性更新滚动条界面。

    也就是说,逻辑滚动才是我们所需要的方案。由于它要求控件实现IScrollInfo接口,自行控制滚动。也就是说我们要实现自己的Panel,并且实现IScrollInfo接口。关于这个接口,MSDN上有一系列文章介绍过如何实现它:

    这个接口实现也不算麻烦,我倒没有细看这几篇文章,自己照着最后的一个例子尝试着弄了一阵子也弄出来了。实际上麻烦的地方不在于实现这个接口,而是实现Panel,我这里为了简单,直接继承了WrapPanel类,代码如下: 

      1     class MyWrapPanel : WrapPanel, IScrollInfo
      2     {
      3         TranslateTransform _transForm;
      4         public MyWrapPanel()
      5         {
      6             _transForm = new TranslateTransform();
      7             this.RenderTransform = _transForm;
      8         }
      9 
     10         #region Layout
     11 
     12         Size _screenSize;
     13         Size _totalSize;
     14 
     15         protected override Size MeasureOverride(Size availableSize)
     16         {
     17             _screenSize = availableSize;
     18 
     19             if (Orientation == Orientation.Horizontal)
     20                 availableSize = new Size(availableSize.Width, double.PositiveInfinity);
     21             else
     22                 availableSize = new Size(double.PositiveInfinity, availableSize.Height);
     23 
     24             _totalSize = base.MeasureOverride(availableSize);
     25             return _totalSize;
     26         }
     27 
     28         protected override Size ArrangeOverride(Size finalSize)
     29         {
     30             var size = base.ArrangeOverride(finalSize);
     31             if (ScrollOwner != null)
     32             {
     33                 _transForm.Y = -VerticalOffset;
     34                 _transForm.X = -HorizontalOffset;
     35                 
     36                 ScrollOwner.InvalidateScrollInfo();
     37             }
     38             return _screenSize;
     39         }
     40         #endregion
     41 
     42         #region IScrollInfo
     43 
     44         public ScrollViewer ScrollOwner { get; set; }
     45         public bool CanHorizontallyScroll { get; set; }
     46         public bool CanVerticallyScroll { get; set; }
     47 
     48         public double ExtentHeight { get { return _totalSize.Height; } }
     49         public double ExtentWidth { get { return _totalSize.Width; } }
     50 
     51         public double HorizontalOffset { get; private set; }
     52         public double VerticalOffset { get; private set; }
     53 
     54         public double ViewportHeight { get { return _screenSize.Height; } }
     55         public double ViewportWidth { get { return _screenSize.Width; } }
     56 
     57         void appendOffset(double x, double y)
     58         {
     59             var offset = new Vector(HorizontalOffset + x, VerticalOffset + y);
     60 
     61             offset.Y = range(offset.Y, 0, _totalSize.Height - _screenSize.Height);
     62             offset.X = range(offset.X, 0, _totalSize.Width - _screenSize.Width);
     63 
     64             HorizontalOffset = offset.X;
     65             VerticalOffset = offset.Y;
     66 
     67             InvalidateArrange();
     68         }
     69 
     70         double range(double value, double value1, double value2)
     71         {
     72             var min = Math.Min(value1, value2);
     73             var max = Math.Max(value1, value2);
     74 
     75             value = Math.Max(value, min);
     76             value = Math.Min(value, max);
     77 
     78             return value;
     79         }
     80 
     81 
     82         const double _lineOffset = 30;
     83         const double _wheelOffset = 90;
     84 
     85         public void LineDown()
     86         {
     87             appendOffset(0, _lineOffset);
     88         }
     89 
     90         public void LineUp()
     91         {
     92             appendOffset(0, -_lineOffset);
     93         }
     94 
     95         public void LineLeft()
     96         {
     97             appendOffset(-_lineOffset, 0);
     98         }
     99 
    100         public void LineRight()
    101         {
    102             appendOffset(_lineOffset, 0);
    103         }
    104 
    105         public Rect MakeVisible(Visual visual, Rect rectangle)
    106         {
    107             throw new NotSupportedException();
    108         }
    109 
    110         public void MouseWheelDown()
    111         {
    112             appendOffset(0, _wheelOffset);
    113         }
    114 
    115         public void MouseWheelUp()
    116         {
    117             appendOffset(0, -_wheelOffset);
    118         }
    119 
    120         public void MouseWheelLeft()
    121         {
    122             appendOffset(0, _wheelOffset);
    123         }
    124 
    125         public void MouseWheelRight()
    126         {
    127             appendOffset(_wheelOffset, 0);
    128         }
    129 
    130         public void PageDown()
    131         {
    132             appendOffset(0, _screenSize.Height);
    133         }
    134 
    135         public void PageUp()
    136         {
    137             appendOffset(0, -_screenSize.Height);
    138         }
    139 
    140         public void PageLeft()
    141         {
    142             appendOffset(-_screenSize.Width, 0);
    143         }
    144 
    145         public void PageRight()
    146         {
    147             appendOffset(_screenSize.Width, 0);
    148         }
    149 
    150         public void SetVerticalOffset(double offset)
    151         {
    152             this.appendOffset(HorizontalOffset, offset - VerticalOffset);
    153         }
    154 
    155         public void SetHorizontalOffset(double offset)
    156         {
    157             this.appendOffset(offset - HorizontalOffset, VerticalOffset);
    158         }
    159         #endregion
    160     }
    View Code

    基本上从代码中也能看出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>

    需要注意的是,这儿需要设置<ScrollViewer CanContentScroll="True">否则使用的不是逻辑滚动。

    数据源代码如下:

        var brushes = from property in typeof(Brushes).GetProperties()
                        let value = property.GetValue(null)
                        select value;
    this.DataContext = brushes.Take(100).ToArray();

    由于使用了IscrollInfo接口,所有的滚动操作是自己实现的,这里我是通过设置Panel的RenderTransFrom的X,Y偏移来实现滚动操作的。运行后看上去上和WrapPanel没有什么区别,但是由于是自己控制的滚动,加上动画效果也只是分分钟的事情了,把上面代码的RenderTransFrom的X,Y硬切换改成动画切换即可:

        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; }

    对于其它的Panel,如Grid,DockPanel等,基本上也可以按照这种方式实现,IScrollInfo接口处基本上可以保持不变,只需要重写MeasureOverride和ArrangeOverride两个函数即可。一个特殊的控件是StackPanel,由于它本身已经实现了IScrollInfo接口,也就是说它本身就有自身的自绘制滚动的方案,并且没有提供接口在覆盖自身的自绘制滚动,因此我们需要自己写一个StackPanel,好在实现StackPanel并不难,由于篇幅有限,这里我懒得继续写了,读者朋友自己实现吧。至于那些非Panel的控件,实现就更简单了,也留着读者朋友自己实现吧。

  • 相关阅读:
    MySQL 有关权限的表都有哪几个?
    MyISAM 表格将在哪里存储,并且还提供其存储格式?
    你是怎么看Spring框架的?
    elasticsearch 的倒排索引是什么 ?
    主键和候选键有什么区别?
    MySQL 支持事务吗?
    可以使用多少列创建索引?
    LIKE 声明中的%和_是什么意思?
    什么是通用 SQL 函数?
    MySQL 中有哪些不同的表格?
  • 原文地址:https://www.cnblogs.com/TianFang/p/4198731.html
Copyright © 2011-2022 走看看