示例:WPF应用Behavior设置带有拖动和缓冲效果的ScrollView

48 篇文章 11 订阅

一、目的:封装一个具有拖动效果和缓冲效果的ScrollView

二、实现

1、在ScrollView中通过鼠标拖动可以平移

2、在ScrollView中滚动时具有缓冲效果

3、封装在行为中直接附加

三、示例

四、实现过程

1、创建拖动行为,如下

    /// <summary> ScrollViewer带有鼠标拖动和触摸拖动效果 </summary>
    public class ScrollViewMouseDragBehavior : Behavior<ScrollViewer>
    {

        ScrollViewer scrollViewer;

        protected override void OnAttached()
        {

            scrollViewer = AssociatedObject as ScrollViewer;

            if (scrollViewer == null) return;


            scrollViewer.PreviewMouseDown += AssociatedObject_PreviewMouseDown;

            scrollViewer.PreviewMouseMove += AssociatedObject_PreviewMouseMove;

            scrollViewer.TouchDown += AssociatedObject_TouchDown;

            scrollViewer.PreviewTouchMove += AssociatedObject_PreviewTouchMove;
        }

        protected override void OnDetaching()
        {
            if (scrollViewer == null) return;

            scrollViewer.PreviewMouseDown -= AssociatedObject_PreviewMouseDown;

            scrollViewer.PreviewMouseMove -= AssociatedObject_PreviewMouseMove;


            scrollViewer.TouchDown -= AssociatedObject_TouchDown;

            scrollViewer.PreviewTouchMove -= AssociatedObject_PreviewTouchMove;
        }

  

        /// <summary> 是否到达顶部 </summary>
        public bool IsTopped
        {
            get { return (bool)GetValue(IsToppedProperty); }
            set { SetValue(IsToppedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsToppedProperty =
            DependencyProperty.Register("IsTopped", typeof(bool), typeof(ScrollViewMouseDragBehavior), new PropertyMetadata(default(bool), (d, e) =>
             {
                 ScrollViewMouseDragBehavior control = d as ScrollViewMouseDragBehavior;

                 if (control == null) return;

                 //bool config = e.NewValue as bool;

             }));

        /// <summary> 是否到达底部 </summary>
        public bool IsBottomed
        {
            get { return (bool)GetValue(IsBottomedProperty); }
            set { SetValue(IsBottomedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsBottomedProperty =
            DependencyProperty.Register("IsBottomed", typeof(bool), typeof(ScrollViewMouseDragBehavior), new PropertyMetadata(default(bool), (d, e) =>
             {
                 ScrollViewMouseDragBehavior control = d as ScrollViewMouseDragBehavior;

                 if (control == null) return;

                 //bool config = e.NewValue as bool;

             }));



        void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton != MouseButtonState.Pressed) return;

            this.ScrollMove(e.Source);
        }

        void AssociatedObject_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            this.GetLast(e.Source);
        }


        private void AssociatedObject_PreviewTouchMove(object sender, TouchEventArgs e)
        {
            this.ScrollMove(e.Source);
        }

        private void AssociatedObject_TouchDown(object sender, TouchEventArgs e)
        {
            this.GetLast(e.Source);
        }


        void GetLast(object source)
        {
            Point pp = Mouse.GetPosition(source as FrameworkElement);

            Point temp = (source as FrameworkElement).PointToScreen(pp);

            last = temp;
        }

        Point last;

        void ScrollMove(object source)
        {
            Point pp = Mouse.GetPosition(source as FrameworkElement);

            Point temp = (source as FrameworkElement).PointToScreen(pp);

            double y = temp.Y - last.Y;

            this.scrollViewer.ScrollToVerticalOffset(this.scrollViewer.VerticalOffset - y);

            last = temp;

            this.CheckPosition();


        }

        void CheckPosition()
        {
            if (this.scrollViewer.VerticalOffset == 0)
            {
                this.IsTopped = true;
            }

            else if (IsVerticalScrollBarAtBottom(this.scrollViewer))
            {  
                this.IsBottomed = true;
            }
            else
            {
                this.IsTopped = false;
                this.IsBottomed = false;
            }
        }

        public bool IsVerticalScrollBarAtBottom(ScrollViewer s)
        {
            bool isAtButtom = false;

            double dVer = s.VerticalOffset;

            double dViewport = s.ViewportHeight;

            double dExtent = s.ExtentHeight;

            if (dVer != 0)
            {
                if (dVer + dViewport == dExtent)
                {
                    isAtButtom = true;
                }
                else
                {
                    isAtButtom = false;
                }
            }
            else
            {
                isAtButtom = false;
            } 
            return isAtButtom;
        }
    }

 

2、创建缓冲行为如下

    /// <summary>
    /// Behavior that watches an element (or a set of elements) for layout changes, and moves the element smoothly to the new position when needed.
    /// This behavior does not animate the size or visibility of an element; it only animates the offset of that element within its parent container.
    /// </summary>
    public sealed class FluidMoveBehavior : FluidMoveBehaviorBase
    {
        /// <summary>
        /// The duration of the move.
        /// </summary>
        public Duration Duration
        {
            get { return (Duration)this.GetValue(DurationProperty); }
            set { this.SetValue(DurationProperty, value); }
        }
        /// <summary>
        /// Dependency property for the duration of the move.
        /// </summary>
        public static readonly DependencyProperty DurationProperty = DependencyProperty.Register("Duration", typeof(Duration), typeof(FluidMoveBehavior), new PropertyMetadata(new Duration(TimeSpan.FromSeconds(1.0))));

        /// <summary>
        /// Spawning point for this item.
        /// </summary>
        public TagType InitialTag
        {
            get { return (TagType)this.GetValue(InitialTagProperty); }
            set { this.SetValue(InitialTagProperty, value); }
        }
        /// <summary>
        /// Dependency property for the tag type to use just before the object is loaded.
        /// </summary>
        public static readonly DependencyProperty InitialTagProperty = DependencyProperty.Register("InitialTag", typeof(TagType), typeof(FluidMoveBehavior), new PropertyMetadata(TagType.Element));

        /// <summary>
        /// Extra path to add to the binding when TagType is specified.
        /// </summary>
        public string InitialTagPath
        {
            get { return (string)this.GetValue(InitialTagPathProperty); }
            set { this.SetValue(InitialTagPathProperty, value); }
        }
        /// <summary>
        /// Dependency property for the extra path to add to the binding when UsaBindingAsTag is true.
        /// </summary>
        public static readonly DependencyProperty InitialTagPathProperty = DependencyProperty.Register("InitialTagPath", typeof(string), typeof(FluidMoveBehavior), new PropertyMetadata(String.Empty));

        /// <summary>
        /// Identity tag used to detect element motion between containers.
        /// </summary>

        private static readonly DependencyProperty initialIdentityTagProperty = DependencyProperty.RegisterAttached("InitialIdentityTag", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
        private static object GetInitialIdentityTag(DependencyObject obj) { return obj.GetValue(initialIdentityTagProperty); }
        private static void SetInitialIdentityTag(DependencyObject obj, object value) { obj.SetValue(initialIdentityTagProperty, value); }

        /// <summary>
        /// Flag that says whether elements are allowed to float above their containers (in a Popup or Adorner) when changing containers.
        /// </summary>
        public bool FloatAbove
        {
            get { return (bool)this.GetValue(FloatAboveProperty); }
            set { this.SetValue(FloatAboveProperty, value); }
        }
        /// <summary>
        /// Dependency property for the FloatAbove flag.
        /// </summary>
        public static readonly DependencyProperty FloatAboveProperty = DependencyProperty.Register("FloatAbove", typeof(bool), typeof(FluidMoveBehavior), new PropertyMetadata(true));

        /// <summary>
        /// EasingFunction to use for the horizontal component of the move.
        /// </summary>
        public IEasingFunction EaseX
        {
            get { return (IEasingFunction)this.GetValue(EaseXProperty); }
            set { this.SetValue(EaseXProperty, value); }
        }
        /// <summary>
        /// Dependency property for the EasingFunction to use for the horizontal component of the move.
        /// </summary>
        public static readonly DependencyProperty EaseXProperty = DependencyProperty.Register("EaseX", typeof(IEasingFunction), typeof(FluidMoveBehavior), new PropertyMetadata(null));

        /// <summary>
        /// EasingFunction to use for the vertical component of the move.
        /// </summary>
        public IEasingFunction EaseY
        {
            get { return (IEasingFunction)this.GetValue(EaseYProperty); }
            set { this.SetValue(EaseYProperty, value); }
        }
        /// <summary>
        /// Dependency property for the EasingFunction to use for the vertical component of the move.
        /// </summary>
        public static readonly DependencyProperty EaseYProperty = DependencyProperty.Register("EaseY", typeof(IEasingFunction), typeof(FluidMoveBehavior), new PropertyMetadata(null));

        /// <summary>
        /// Remember the popup/adorner being used, in case of element motion between containers when FloatAbove is true.
        /// </summary>
        private static readonly DependencyProperty overlayProperty = DependencyProperty.RegisterAttached("Overlay", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
        private static object GetOverlay(DependencyObject obj) { return obj.GetValue(overlayProperty); }
        private static void SetOverlay(DependencyObject obj, object value) { obj.SetValue(overlayProperty, value); }

        /// <summary>
        /// Opacity cache used when floating a Popup.
        /// </summary>
        private static readonly DependencyProperty cacheDuringOverlayProperty = DependencyProperty.RegisterAttached("CacheDuringOverlay", typeof(object), typeof(FluidMoveBehavior), new PropertyMetadata(null));
        private static object GetCacheDuringOverlay(DependencyObject obj) { return obj.GetValue(cacheDuringOverlayProperty); }
        private static void SetCacheDuringOverlay(DependencyObject obj, object value) { obj.SetValue(cacheDuringOverlayProperty, value); }

        /// <summary>
        /// Marks the animation transform.
        /// </summary>
        private static readonly DependencyProperty hasTransformWrapperProperty = DependencyProperty.RegisterAttached("HasTransformWrapper", typeof(bool), typeof(FluidMoveBehavior), new PropertyMetadata(false));
        private static bool GetHasTransformWrapper(DependencyObject obj) { return (bool)obj.GetValue(hasTransformWrapperProperty); }
        private static void SetHasTransformWrapper(DependencyObject obj, bool value) { obj.SetValue(hasTransformWrapperProperty, value); }

        private static Dictionary<object, Storyboard> transitionStoryboardDictionary = new Dictionary<object, Storyboard>();

        protected override bool ShouldSkipInitialLayout
        {
            get
            {
                return base.ShouldSkipInitialLayout || (this.InitialTag == TagType.DataContext);
            }
        }

        protected override void EnsureTags(FrameworkElement child)
        {
            base.EnsureTags(child);

            // If we are going to use a binding for the tag, make sure we have one set up.
            if (this.InitialTag == TagType.DataContext)
            {
                object tagValue = child.ReadLocalValue(initialIdentityTagProperty);
                if (!(tagValue is BindingExpression))
                {
                    child.SetBinding(initialIdentityTagProperty, new Binding(this.InitialTagPath));
                }
            }
        }

        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Trying to keep the number of function parameters down to a minimum.")]
        internal override void UpdateLayoutTransitionCore(FrameworkElement child, FrameworkElement root, object tag, TagData newTagData)
        {
            TagData tagData;
            Rect previousRect;
            bool parentChange = false;
            bool usingBeforeLoaded = false;
            object initialTag = GetInitialIdentityTag(child);

            // Locate the previous tag, and the parent-relative previous rect. The previous rect is computed using the app-relative rect if switching parents.
            // Note that we do not use the app-relative rect all time time, because when the parent itself moves, it accounts for all the motion and we do not have to.
            bool gotData = TagDictionary.TryGetValue(tag, out tagData);

            // if spawn point has changed then throw away the old one
            if (gotData && tagData.InitialTag != initialTag)
            {
                gotData = false;
                TagDictionary.Remove(tag);
            }
            if (!gotData)
            {
                TagData spawnData;

                if (initialTag != null && TagDictionary.TryGetValue(initialTag, out spawnData))
                {
                    previousRect = TranslateRect(spawnData.AppRect, root, newTagData.Parent);
                    parentChange = true;
                    usingBeforeLoaded = true;
                }
                else
                {
                    previousRect = Rect.Empty;
                }

                tagData = new TagData() { ParentRect = Rect.Empty, AppRect = Rect.Empty, Parent = newTagData.Parent, Child = child, Timestamp = DateTime.Now, InitialTag = initialTag };
                TagDictionary.Add(tag, tagData);
            }
            else if (tagData.Parent != VisualTreeHelper.GetParent(child))
            {
                previousRect = TranslateRect(tagData.AppRect, root, newTagData.Parent);
                parentChange = true;
            }
            else
            {
                previousRect = tagData.ParentRect;
            }

            FrameworkElement originalChild = child;

            if ((!FluidMoveBehavior.IsEmptyRect(previousRect) && !FluidMoveBehavior.IsEmptyRect(newTagData.ParentRect)) && (!IsClose(previousRect.Left, newTagData.ParentRect.Left) || !IsClose(previousRect.Top, newTagData.ParentRect.Top)) ||
                (child != tagData.Child && transitionStoryboardDictionary.ContainsKey(tag)))
            {
                Rect currentRect = previousRect;
                bool forceFloatAbove = false;

                // If this element was animating before, append its current transform to the start position and kill the old animation.
                // Note that in an overlay scenario, the animation is on the image in the overlay.
                Storyboard oldTransitionStoryboard = null;
                if (transitionStoryboardDictionary.TryGetValue(tag, out oldTransitionStoryboard))
                {
                    object tagOverlay = GetOverlay(tagData.Child);
                    AdornerContainer adornerContainer = (AdornerContainer)tagOverlay;

                    forceFloatAbove = (tagOverlay != null); // if floating before, we need to keep floating
                    FrameworkElement elementWithTransform = tagData.Child;

                    if (tagOverlay != null)
                    {
                        Canvas overlayCanvas = adornerContainer.Child as Canvas;
                        if (overlayCanvas != null)
                        {
                            elementWithTransform = overlayCanvas.Children[0] as FrameworkElement;
                        }
                    }

                    // if we're picking a specific starting point, don't append this transform
                    if (!usingBeforeLoaded)
                    {
                        Transform transform = GetTransform(elementWithTransform);
                        currentRect = transform.TransformBounds(currentRect);
                    }

                    transitionStoryboardDictionary.Remove(tag);
                    oldTransitionStoryboard.Stop();
                    oldTransitionStoryboard = null;
                    RemoveTransform(elementWithTransform);

                    if (tagOverlay != null)
                    {
                        System.Windows.Documents.AdornerLayer.GetAdornerLayer(root).Remove(adornerContainer);
                        TransferLocalValue(tagData.Child, FluidMoveBehavior.cacheDuringOverlayProperty, FrameworkElement.RenderTransformProperty);
                        SetOverlay(tagData.Child, null);
                    }
                }

                object overlay = null;

                // If we need to float this element, then we have to:
                // 1. Take a picture of it
                // 2. Put that picture in an Image in a popup
                // 3. Hide the original element (opacity=0 so we do not disturb layout)
                // 4. Animate the image
                // 5. Keep track of all the info we need to unwind this later
                if (forceFloatAbove || (parentChange && this.FloatAbove))
                {
                    Canvas canvas = new Canvas() { Width = newTagData.ParentRect.Width, Height = newTagData.ParentRect.Height, IsHitTestVisible = false };

                    Rectangle rectangle = new Rectangle() { Width = newTagData.ParentRect.Width, Height = newTagData.ParentRect.Height, IsHitTestVisible = false };
                    rectangle.Fill = new VisualBrush(child);
                    canvas.Children.Add(rectangle);
                    AdornerContainer adornerContainer = new AdornerContainer(child) { Child = canvas };
                    overlay = adornerContainer;

                    // remember this overlay so we can get info from it
                    SetOverlay(originalChild, overlay);

                    System.Windows.Documents.AdornerLayer adorners = System.Windows.Documents.AdornerLayer.GetAdornerLayer(root);
                    adorners.Add(adornerContainer);

                    // Note: Not using this approach currently because the bitmap is not ready yet
                    // To remove use of VisualBrush, have to fill in bitmap after a render
                    //RenderTargetBitmap bitmap = new RenderTargetBitmap((int)child.ActualWidth, (int)child.ActualHeight, 96, 96, PixelFormats.Pbgra32);
                    //bitmap.Render(parent);
                    //image.Source = bitmap;

                    // can't animate this or it will flash, have to set the value outright
                    TransferLocalValue(child, FrameworkElement.RenderTransformProperty, FluidMoveBehavior.cacheDuringOverlayProperty);
                    child.RenderTransform = new TranslateTransform(-10000, -10000);
                    canvas.RenderTransform = new TranslateTransform(10000, 10000);

                    // change value here so that the animations will be applied to the image
                    child = rectangle;
                }

                // OK, now build the actual animation
                Rect parentRect = newTagData.ParentRect;
                Storyboard transitionStoryboard = CreateTransitionStoryboard(child, usingBeforeLoaded, ref parentRect, ref currentRect);

                // Put this storyboard in the running dictionary so we can detect reentrancy
                transitionStoryboardDictionary.Add(tag, transitionStoryboard);

                transitionStoryboard.Completed += delegate (object sender, EventArgs e)
                {
                    Storyboard currentlyRunningStoryboard;
                    if (transitionStoryboardDictionary.TryGetValue(tag, out currentlyRunningStoryboard) && currentlyRunningStoryboard == transitionStoryboard)
                    {
                        transitionStoryboardDictionary.Remove(tag);
                        transitionStoryboard.Stop();
                        RemoveTransform(child);
                        child.InvalidateMeasure();

                        if (overlay != null)
                        {
                            System.Windows.Documents.AdornerLayer.GetAdornerLayer(root).Remove((AdornerContainer)overlay);
                            TransferLocalValue(originalChild, FluidMoveBehavior.cacheDuringOverlayProperty, FrameworkElement.RenderTransformProperty);
                            SetOverlay(originalChild, null);
                        }
                    }
                };

                transitionStoryboard.Begin();
            }

            // Store current tag status
            tagData.ParentRect = newTagData.ParentRect;
            tagData.AppRect = newTagData.AppRect;
            tagData.Parent = newTagData.Parent;
            tagData.Child = newTagData.Child;
            tagData.Timestamp = newTagData.Timestamp;
        }

        private Storyboard CreateTransitionStoryboard(FrameworkElement child, bool usingBeforeLoaded, ref Rect layoutRect, ref Rect currentRect)
        {
            Duration duration = this.Duration;
            Storyboard transitionStoryboard = new Storyboard();
            transitionStoryboard.Duration = duration;

            double xScaleFrom = (!usingBeforeLoaded || layoutRect.Width == 0.0) ? 1.0 : (currentRect.Width / layoutRect.Width);
            double yScaleFrom = (!usingBeforeLoaded || layoutRect.Height == 0.0) ? 1.0 : (currentRect.Height / layoutRect.Height);
            double xFrom = currentRect.Left - layoutRect.Left;
            double yFrom = currentRect.Top - layoutRect.Top;

            TransformGroup transform = new TransformGroup();
            transform.Children.Add(new ScaleTransform() { ScaleX = xScaleFrom, ScaleY = yScaleFrom });
            transform.Children.Add(new TranslateTransform() { X = xFrom, Y = yFrom });
            AddTransform(child, transform);

            string prefix = "(FrameworkElement.RenderTransform).";

            TransformGroup transformGroup = child.RenderTransform as TransformGroup;
            if (transformGroup != null && GetHasTransformWrapper(child))
            {
                prefix += "(TransformGroup.Children)[" + (transformGroup.Children.Count - 1) + "].";
            }

            if (usingBeforeLoaded)
            {
                if (xScaleFrom != 1.0)
                {
                    DoubleAnimation xScaleAnimation = new DoubleAnimation() { Duration = duration, From = xScaleFrom, To = 1.0 };
                    Storyboard.SetTarget(xScaleAnimation, child);
                    Storyboard.SetTargetProperty(xScaleAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[0].(ScaleTransform.ScaleX)", new object[0]));
                    xScaleAnimation.EasingFunction = this.EaseX;
                    transitionStoryboard.Children.Add(xScaleAnimation);
                }

                if (yScaleFrom != 1.0)
                {
                    DoubleAnimation yScaleAnimation = new DoubleAnimation() { Duration = duration, From = yScaleFrom, To = 1.0 };
                    Storyboard.SetTarget(yScaleAnimation, child);
                    Storyboard.SetTargetProperty(yScaleAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[0].(ScaleTransform.ScaleY)", new object[0]));
                    yScaleAnimation.EasingFunction = this.EaseY;
                    transitionStoryboard.Children.Add(yScaleAnimation);
                }
            }

            if (xFrom != 0.0)
            {
                DoubleAnimation xAnimation = new DoubleAnimation() { Duration = duration, From = xFrom, To = 0.0 };
                Storyboard.SetTarget(xAnimation, child);
                Storyboard.SetTargetProperty(xAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[1].(TranslateTransform.X)", new object[0]));
                xAnimation.EasingFunction = this.EaseX;
                transitionStoryboard.Children.Add(xAnimation);
            }

            if (yFrom != 0.0)
            {
                DoubleAnimation yAnimation = new DoubleAnimation() { Duration = duration, From = yFrom, To = 0.0 };
                Storyboard.SetTarget(yAnimation, child);
                Storyboard.SetTargetProperty(yAnimation, new PropertyPath(prefix + "(TransformGroup.Children)[1].(TranslateTransform.Y)", new object[0]));
                yAnimation.EasingFunction = this.EaseY;
                transitionStoryboard.Children.Add(yAnimation);
            }

            return transitionStoryboard;
        }

        private static void AddTransform(FrameworkElement child, Transform transform)
        {
            TransformGroup transformGroup = child.RenderTransform as TransformGroup;

            if (transformGroup == null)
            {
                transformGroup = new TransformGroup();
                transformGroup.Children.Add(child.RenderTransform);
                child.RenderTransform = transformGroup;
                SetHasTransformWrapper(child, true);
            }

            transformGroup.Children.Add(transform);
        }

        private static Transform GetTransform(FrameworkElement child)
        {
            TransformGroup transformGroup = child.RenderTransform as TransformGroup;
            if (transformGroup != null && transformGroup.Children.Count > 0)
            {
                return transformGroup.Children[transformGroup.Children.Count - 1];
            }
            else
            {
                return new TranslateTransform();
            }
        }

        private static void RemoveTransform(FrameworkElement child)
        {
            TransformGroup transformGroup = child.RenderTransform as TransformGroup;

            if (transformGroup != null)
            {
                if (GetHasTransformWrapper(child))
                {
                    child.RenderTransform = transformGroup.Children[0];
                    SetHasTransformWrapper(child, false);
                }
                else
                {
                    transformGroup.Children.RemoveAt(transformGroup.Children.Count - 1);
                }
            }
        }

        private static void TransferLocalValue(FrameworkElement element, DependencyProperty source, DependencyProperty dest)
        {
            object value = element.ReadLocalValue(source);

            BindingExpressionBase bindingExpressionBase = value as BindingExpressionBase;
            if (bindingExpressionBase != null)
            {
                element.SetBinding(dest, bindingExpressionBase.ParentBindingBase);
            }
            else if (value == DependencyProperty.UnsetValue)
            {
                element.ClearValue(dest);
            }
            else
            {
                element.SetValue(dest, element.GetAnimationBaseValue(source));
            }

            element.ClearValue(source);
        }

        private static bool IsClose(double a, double b)
        {
            return (Math.Abs((double)(a - b)) < 1E-07);
        }

        private static bool IsEmptyRect(Rect rect)
        {
            return ((rect.IsEmpty || double.IsNaN(rect.Left)) || double.IsNaN(rect.Top));
        }
    }

 3、在XAML按如下加入行为

       <ScrollViewer>
            <h:Interaction.Behaviors>
                <h:ScrollViewMouseDragBehavior/>
            </h:Interaction.Behaviors>
            
            <StackPanel>
                <h:Interaction.Behaviors>
                    <h:FluidMoveBehavior AppliesTo="Self" Duration="00:00:01"  FloatAbove="True"/>
                </h:Interaction.Behaviors>
                
                    </StackPanel>
                </GroupBox>
            </StackPanel>
        </ScrollViewer>

由上面添加过程即可实现示例带有鼠标或触摸拖动效果的ScrollView控件

五、下载地址

GitHub下载地址:https://github.com/HeBianGu/WPF-ControlBase.git

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值