1. 程式人生 > >在WPF中的Canvas上實現控制元件的拖動、縮放

在WPF中的Canvas上實現控制元件的拖動、縮放

   如題,專案中需要實現使用滑鼠拖動、縮放一個矩形框,WPF中沒有現成的,那就自己造一個輪子:)

   造輪子前先看看Windows自帶的畫圖工具中是怎樣做的,如下圖:

 

   在被拖動的矩形框四周有9個小框,可以從不同方向拖動來放大縮小矩形框,另外需要注意的是,還有一個框,就是圖中虛線的矩形框,這個框,是用來拖動目標控制元件的;我們要做的,就是模仿畫圖中的做法,在自定義控制元件中顯示10個框,然後根據滑鼠所在的框來處理滑鼠輸入,實現拖動與放大。

    參考這篇博文繼續聊WPF——Thumb控制元件得知,WPF中有現成的拖動控制元件,可以提供對應的事件(DragDelta & DragCompleted), 就用它了。

還有一個需要考慮的是,我們的這個自定義控制元件中有10個不同作用的Thumb控制元件,如何區分事件從哪個Thumb發出來的呢?這樣我們才能知道使用者希望的操作是拖動,還是縮放,而且縮放也要知道朝哪個方向縮放。可以使用Tag屬性,但是它是Object型別的,會涉及到拆箱,所以還是自定義一個CustomThumb。

    首先,定義說明拖動方向的列舉:

    public enum DragDirection
    {
        TopLeft = 1,
        TopCenter = 2,
        TopRight = 4,
        MiddleLeft = 16,
        MiddleCenter = 32,
        MiddleRight = 64,
        BottomLeft = 256,
        BottomCenter = 512,
        BottomRight = 1024,
    }

好了,有了這個列舉,就可以知道使用者操作的意圖了,現在自定義一個CustomThumb。

    public class CustomThumb : Thumb
    {
        public DragDirection DragDirection { get; set; }
    }
這些都弄好了,現在來寫自定義控制元件的模板:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                    xmlns:local="clr-namespace:UICommon.Controls"
                    xmlns:Core="clr-namespace:System;assembly=mscorlib"
                    mc:Ignorable="d">


    
    <ControlTemplate TargetType="{x:Type local:DragHelperBase}" x:Key="DrapControlHelperTemplate">
        <ControlTemplate.Resources>
            <Style TargetType="{x:Type Thumb}" x:Key="CornerThumbStyle">
                <Setter Property="Width" Value="{Binding CornerWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/>
                <Setter Property="Height" Value="{Binding CornerWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/>
                <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/>
                <Setter Property="BorderThickness" Value="3"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type Thumb}">
                            <Border SnapsToDevicePixels="True"
                                    Width="{TemplateBinding Width}" 
						            Height="{TemplateBinding Height}"
						            Background="{TemplateBinding Background}" 
						            BorderBrush="{TemplateBinding BorderBrush}"
						            BorderThickness="{TemplateBinding BorderThickness}"/>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
            
            <Style TargetType="{x:Type Thumb}" x:Key="AreaThumbStyle">
                <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Padding" Value="0"/>
                <Setter Property="Margin" Value="0"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type Thumb}">
                            <Rectangle Margin="0" Fill="{TemplateBinding Background}" SnapsToDevicePixels="True"
                                       Stroke="{TemplateBinding BorderBrush}" StrokeDashArray="2.0 2.0" Stretch="Fill"
                                       StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top, Mode=OneWay}"/>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ControlTemplate.Resources>
        
        <Grid x:Name="PART_MainGrid">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>


            <local:CustomThumb DragDirection="MiddleCenter" Grid.RowSpan="3" Grid.ColumnSpan="3" Cursor="SizeAll" Style="{StaticResource AreaThumbStyle}"/>
            
            <local:CustomThumb DragDirection="TopLeft"      Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left"   VerticalAlignment="Top"    Cursor="SizeNWSE"/>
            <local:CustomThumb DragDirection="TopCenter"    Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Top"    Cursor="SizeNS"/>
            <local:CustomThumb DragDirection="TopRight"     Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right"  VerticalAlignment="Top"    Cursor="SizeNESW"/>
            
            <local:CustomThumb DragDirection="MiddleLeft"   Style="{StaticResource CornerThumbStyle}" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Left"   VerticalAlignment="Center" Cursor="SizeWE"/>
            <local:CustomThumb DragDirection="MiddleRight"  Style="{StaticResource CornerThumbStyle}" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Right"  VerticalAlignment="Center" Cursor="SizeWE"/>
            
            <local:CustomThumb DragDirection="BottomLeft"   Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="0" HorizontalAlignment="Left"   VerticalAlignment="Bottom" Cursor="SizeNESW"/>
            <local:CustomThumb DragDirection="BottomCenter" Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Cursor="SizeNS"/>
            <local:CustomThumb DragDirection="BottomRight"  Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="2" HorizontalAlignment="Right"  VerticalAlignment="Bottom" Cursor="SizeNWSE"/>
            
        </Grid>
    </ControlTemplate>

    <Style TargetType="{x:Type local:DragHelperBase}"  BasedOn="{StaticResource {x:Type ContentControl}}">
        <Setter Property="BorderBrush" Value="Green"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Padding" Value="0"/>
        <Setter Property="Margin" Value="0"/>
        <Setter Property="MinHeight" Value="5"/>
        <Setter Property="MinWidth" Value="5"/>
        <Setter Property="Template" Value="{StaticResource DrapControlHelperTemplate}"/>
    </Style>

</ResourceDictionary>

下面編寫控制元件的建構函式,設定DefaultStyleKeyProperty,否則控制元件載入時將會找不到控制元件模板

        static DragHelperBase()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DragHelperBase),
                new FrameworkPropertyMetadata(typeof(DragHelperBase)));
        }
        public DragHelperBase()
        {
            SetResourceReference(StyleProperty, typeof(DragHelperBase));
        }

        從控制元件模板可以看出,10個CustomThumb都在自定義控制元件的視覺樹中,所以我們可以使用Thumb的路由事件,接收滑鼠操作的事件並進行處理:

        在重寫的方法OnApplyTemplate中新增路由事件訂閱:

        public sealed override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            MainGrid = GetPartFormTemplate<Grid>("PART_MainGrid");
            
            AddLogicalChild(MainGrid);

            AddHandler(Thumb.DragDeltaEvent, new DragDeltaEventHandler(OnDragDelta));
            AddHandler(Thumb.DragCompletedEvent, new RoutedEventHandler(OnDragCompleted));

            Visibility = Visibility.Collapsed;
        }
  可以看到在最後一句的程式碼中將自定義控制元件的Visibility屬性設為Collapsed,有2個原因,1.使用者沒選中目標控制元件前,我們的拖動控制元件是不應該顯示出來的,第二,如果在建構函式中設定這個屬性,OnApplyTemplate將不會被呼叫。

  由於這是個抽象類,所以我們需要派生類提供幾個必須的方法:

        protected abstract bool GetTargetIsEditable();
        protected abstract Rect GetTargetActualBound();
        protected abstract void SetTargetActualBound(Rect NewBound);
        protected abstract void RaisenDragChangingEvent(Rect NewBound);
        protected abstract void RaisenDragCompletedEvent(Rect NewBound);
另外,還要註冊2個路由事件,方便其他物件獲取目標控制元件的ActualBound:
        #region Drag Event

        public static readonly RoutedEvent DragChangingEvent
            = EventManager.RegisterRoutedEvent("DragChangingEvent", RoutingStrategy.Bubble, typeof(DragChangedEventHandler), typeof(DragHelperBase));

        public event DragChangedEventHandler DragChanging
        {
            add
            {
                AddHandler(DragChangingEvent, value);
            }
            remove
            {
                RemoveHandler(DragChangingEvent, value);
            }
        }

        public static readonly RoutedEvent DragCompletedEvent
                    = EventManager.RegisterRoutedEvent("DragCompletedEvent", RoutingStrategy.Bubble, typeof(DragChangedEventHandler), typeof(DragHelperBase));

        public event DragChangedEventHandler DragCompleted
        {
            add
            {
                AddHandler(DragCompletedEvent, value);
            }
            remove
            {
                RemoveHandler(DragCompletedEvent, value);
            }
        }
        #endregion
    public class DragChangedEventArgs : RoutedEventArgs
    {
        public DragChangedEventArgs(RoutedEvent Event, Rect NewBound, object Target = null) : base(Event)
        {
            this.NewBound = NewBound;
            DragTargetElement = Target;
        }
        public Rect NewBound { get; private set; }

        public object DragTargetElement { get; private set; }
    }
    public delegate void DragChangedEventHandler(object Sender, DragChangedEventArgs e);
當用戶點選目標控制元件時,我們的拖動控制元件應該顯示出來,而且,拖動控制元件的大小、位置應該跟目標控制元件一致:
        #region SetupVisualPropertes
        protected void SetupVisualPropertes(double TargetThickness, bool IsEditable)
        {
            Visibility IsCornerVisibe = IsEditable ? Visibility.Visible : Visibility.Collapsed;

            double ActualMargin = (CornerWidth - TargetThickness) / 2.0;
            //讓9個小框排布在目標邊框的中線上
            MainGrid.Margin = new Thickness(0 - ActualMargin);

            foreach (CustomThumb item in MainGrid.Children)
            {
                if (item != null)
                {
                    item.BorderThickness = new Thickness(TargetThickness);

                    if (item.DragDirection == DragDirection.MiddleCenter)
                    {
                        item.Margin = new Thickness(ActualMargin);
                    }
                    else
                    {
                        item.Visibility = IsCornerVisibe;
                    }
                }
            }
        }
        #endregion
 如果目標控制元件當前不允許編輯,則不要顯示四周的9個小框,只顯本體區域(虛線框),指示目標控制元件已經選中但不可以編輯。

當用戶拖動滑鼠時,處理拖動事件:
        private void OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            if(!GetTargetIsEditable())
            {
                e.Handled = true;
                return;
            }

            CustomThumb thumb = e.OriginalSource as CustomThumb;
            
            if (thumb == null)
            {
                return;
            }

            double VerticalChange = e.VerticalChange;
            double HorizontalChange = e.HorizontalChange;

            Rect NewBound = Rect.Empty;

            if (thumb.DragDirection == DragDirection.MiddleCenter)
            {
                NewBound = DragElement(HorizontalChange, VerticalChange);
            }
            else
            {
                NewBound = ResizeElement(thumb, HorizontalChange, VerticalChange);
            }

            RaisenDragChangingEvent(NewBound);
            SetTargetActualBound(NewBound);

            e.Handled = true;
        }

        private void OnDragCompleted(object sender, RoutedEventArgs e)
        {
            Rect NewBound = new Rect
            {
                Y = Canvas.GetTop(this),
                X = Canvas.GetLeft(this),
                Width = this.ActualWidth,
                Height = this.ActualHeight
            };

            RaisenDragCompletedEvent(NewBound);

            e.Handled = true;
        }
下面是處理目標控制元件的拖動:修改目標控制元件的XY座標即可
        private Rect DragElement(double HorizontalChange, double VerticalChange)
        {
            Rect TargetActualBound = GetTargetActualBound();

            double TopOld  = CorrectDoubleValue(TargetActualBound.Y);
            double LeftOld = CorrectDoubleValue(TargetActualBound.X);
            double TopNew  = CorrectDoubleValue(TopOld + VerticalChange);
            double LeftNew = CorrectDoubleValue(LeftOld + HorizontalChange);

            TopNew  = CorrectNewTop(DragHelperParent, TopNew, TargetActualBound.Height);
            LeftNew = CorrectNewLeft(DragHelperParent, LeftNew, TargetActualBound.Width);

            Canvas.SetTop(this, TopNew);
            Canvas.SetLeft(this, LeftNew);

            return new Rect
            {
                Y = TopNew,
                X = LeftNew,
                Width = TargetActualBound.Width,
                Height = TargetActualBound.Height
            };
        }

下面是處理縮放目標控制元件,思考一下,其實原理就是從不同的方向放大或縮小目標控制元件的 Width & Height 屬性,或者XY座標,並且加上限制,不讓目標控制元件超過父控制元件(在這裡是Canvas)的邊界:
        private Rect ResizeElement(CustomThumb HitedThumb, double HorizontalChange, double VerticalChange)
        {
            #region Get Old Value

            if (HitedThumb == null) return Rect.Empty;
            

            Rect TargetActualBound = GetTargetActualBound();

            double TopOld    = CorrectDoubleValue(TargetActualBound.Y);
            double LeftOld   = CorrectDoubleValue(TargetActualBound.X);
            double WidthOld  = CorrectDoubleValue(TargetActualBound.Width);
            double HeightOld = CorrectDoubleValue(TargetActualBound.Height);

            double TopNew    = TopOld;
            double LeftNew   = LeftOld;
            double WidthNew  = WidthOld;
            double HeightNew = HeightOld;

            #endregion

            if (HitedThumb.DragDirection == DragDirection.TopLeft
                || HitedThumb.DragDirection == DragDirection.MiddleLeft
                || HitedThumb.DragDirection == DragDirection.BottomLeft)
            {
                ResizeFromLeft(DragHelperParent, LeftOld, WidthOld, HorizontalChange, out LeftNew, out WidthNew);
            }

            if (HitedThumb.DragDirection == DragDirection.TopLeft
                || HitedThumb.DragDirection == DragDirection.TopCenter
                || HitedThumb.DragDirection == DragDirection.TopRight)
            {
                ResizeFromTop(DragHelperParent, TopOld, HeightOld, VerticalChange, out TopNew, out HeightNew);
            }

            if (HitedThumb.DragDirection == DragDirection.TopRight
                || HitedThumb.DragDirection == DragDirection.MiddleRight
                || HitedThumb.DragDirection == DragDirection.BottomRight)
            {
                ResizeFromRight(DragHelperParent, LeftOld, WidthOld, HorizontalChange, out WidthNew);
            }

            if (HitedThumb.DragDirection == DragDirection.BottomLeft
                || HitedThumb.DragDirection == DragDirection.BottomCenter
                || HitedThumb.DragDirection == DragDirection.BottomRight)
            {
                ResizeFromBottom(DragHelperParent, TopOld, HeightOld, VerticalChange, out HeightNew);
            }

            this.Width = WidthNew;
            this.Height = HeightNew;
            Canvas.SetTop(this, TopNew);
            Canvas.SetLeft(this, LeftNew);

            return new Rect
            {
                X = LeftNew,
                Y = TopNew,
                Width = WidthNew,
                Height = HeightNew
            };
        }

下面是從不同的方向修改目標控制元件的XY座標或者Width & Height 屬性:
        #region Resize Base Methods

        #region ResizeFromTop
        private static void ResizeFromTop(FrameworkElement Parent, double TopOld, double HeightOld, double VerticalChange, out double TopNew, out double HeightNew)
        {
            double MiniHeight = 10;

            double top = TopOld + VerticalChange;
            TopNew = ((top + MiniHeight) > (HeightOld + TopOld)) ? HeightOld + TopOld - MiniHeight : top;
            TopNew = TopNew < 0 ? 0 : TopNew;

            HeightNew = HeightOld + TopOld - TopNew;

            HeightNew = CorrectNewHeight(Parent, TopNew, HeightNew);
        }
        #endregion

        #region ResizeFromLeft
        private static void ResizeFromLeft(FrameworkElement Parent, double LeftOld, double WidthOld, double HorizontalChange, out double LeftNew, out double WidthNew)
        {
            double MiniWidth = 10;
            double left = LeftOld + HorizontalChange;

            LeftNew = ((left + MiniWidth) > (WidthOld + LeftOld)) ? WidthOld + LeftOld - MiniWidth : left;

            LeftNew = LeftNew < 0 ? 0 : LeftNew;

            WidthNew = WidthOld + LeftOld - LeftNew;

            WidthNew = CorrectNewWidth(Parent, LeftNew, WidthNew);
        }
        #endregion

        #region ResizeFromRight
        private static void ResizeFromRight(FrameworkElement Parent, double LeftOld, double WidthOld, double HorizontalChange, out double WidthNew)
        {
            if (LeftOld + WidthOld + HorizontalChange < Parent.ActualWidth)
            {
                WidthNew = WidthOld + HorizontalChange;
            }
            else
            {
                WidthNew = Parent.ActualWidth - LeftOld;
            }

            WidthNew = WidthNew < 0 ? 0 : WidthNew;
        }
        #endregion

        #region ResizeFromBottom
        private static void ResizeFromBottom(FrameworkElement Parent, double TopOld, double HeightOld, double VerticalChange, out double HeightNew)
        {
            if (TopOld + HeightOld + VerticalChange < Parent.ActualWidth)
            {
                HeightNew = HeightOld + VerticalChange;
            }
            else
            {
                HeightNew = Parent.ActualWidth - TopOld;
            }

            HeightNew = HeightNew < 0 ? 0 : HeightNew;
        }
        #endregion

        #region CorrectNewTop
        private static double CorrectNewTop(FrameworkElement Parent, double Top, double Height)
        {
            double NewHeight = ((Top + Height) > Parent.ActualHeight) ? (Parent.ActualHeight - Height) : Top;
            return NewHeight < 0 ? 0 : NewHeight;
        }
        #endregion

        #region CorrectNewLeft
        private static double CorrectNewLeft(FrameworkElement Parent, double Left, double Width)
        {
            double NewLeft = ((Left + Width) > Parent.ActualWidth) ? (Parent.ActualWidth - Width) : Left;

            return NewLeft < 0 ? 0 : NewLeft;
        }
        #endregion

        #region CorrectNewWidth
        private static double CorrectNewWidth(FrameworkElement Parent, double Left, double WidthNewToCheck)
        {
            double Width = ((Left + WidthNewToCheck) > Parent.ActualWidth) ? (Parent.ActualWidth - Left) : WidthNewToCheck;

            return Width < 0 ? 0 : Width;
        }
        #endregion

        #region CorrectNewHeight
        private static double CorrectNewHeight(FrameworkElement Parent, double Top, double HeightNewToCheck)
        {
            double Height = ((Top + HeightNewToCheck) > Parent.ActualHeight) ? (Parent.ActualHeight - Top) : HeightNewToCheck;
            return Height < 0 ? 0 : Height;
        }
        #endregion

        #region CorrectDoubleValue
        protected static double CorrectDoubleValue(double Value)
        {
            return (double.IsNaN(Value) || (Value < 0.0)) ? 0 : Value;
        }
        #endregion

        #endregion

下面是測試效果:





完整的測試程式碼可以到我的github下載