1. 程式人生 > >WPF 的Canvas畫圖區整體縮放與平移(二)

WPF 的Canvas畫圖區整體縮放與平移(二)

WPF物件都具有RenderTransform的屬性,可以通過設定RenderTransform來對WPF的元素進行變換,無論是控制元件還是形狀都可以變換。典型的變換包括縮小放大與平移。

(一)縮放

(二)平移

為了實現平移,這裡以按下滑鼠中間鍵並移動滑鼠作為事件觸發方式,來實現平移。即先下轄滑鼠中鍵(滾輪鍵),移動滑鼠,這樣WPF元素就會跟隨滑鼠平移。

WPF元素和形狀的變換是針對於自身的座標系的,因此不能直接獲取其自身座標系的點來進行變換,否則是不對了,有時也會不停閃爍跳動,本人一開始不小心用錯了,就用待平移的WPF元素自身座標點來作為平移引數,後來發現螢幕不停閃爍跳動,且平移距離也不正確。

如果採用Canvas作為畫板來繪製一些形狀,想要通過滑鼠或觸控操作來進行平移,那麼不能簡單地對canvas進行變換,否則Cancas平移的時候就會覆蓋周邊的其它控制元件,也就是Canvas畫布自身被移動了,而不僅僅是Canvas內部畫出來的形狀被移動了。

那如果需要實現移動怎麼辦呢?與前一篇文章一樣,我採取的方式是在Canvas外面包裝一個WPF元素,比如Border元素。這樣,Canvas就成為Border元素的子元素了,然後在Border元素上實現滑鼠控制操作來變換Canvas元素。

  <Border Name="outside" Grid.Column="1" Background="LightBlue" 
                PreviewMouseDown="outsidewrapper_PreviewMouseDown" 
                PreviewMouseMove="outsidewrapper_PreviewMouseMove" 
                PreviewMouseUp="outside_PreviewMouseUp"
                PreviewMouseWheel="outside_PreviewMouseWheel"
                ClipToBounds="True">

            <Canvas Name="inside" Width="{Binding Path=ActualWidth,RelativeSource={RelativeSource AncestorType=Border}}" 
                    Height="{Binding Path=ActualHeight,RelativeSource={RelativeSource AncestorType=Border}}">
                <Canvas.RenderTransform>
                    <TransformGroup/>
                </Canvas.RenderTransform>
                <Line Canvas.Left="50" Canvas.Top="50" X1="100" Y1="200"  X2="100" Y2="200" Stroke="Black" StrokeThickness="5"/>
                <Rectangle Canvas.Left="150" Canvas.Top="150" Width="380" Height="296" Fill="Red" />

            </Canvas>
        </Border>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        Point previousPoint;
        bool isTranslateStart = false;

        private void outsidewrapper_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
//說明:雖然WPF元素存在MouseLeftButtonDown和MouseRightButtonDown事件,
//但卻沒找到MouseMiddleButtonDown事件。
//然而,幸運的是,MouseDown事件是不論哪個滑鼠鍵按下的時候都會觸發的,因此可以利用該事件,
//並通過檢測哪個鍵被按下的狀態引數來判斷是否是中間鍵被按下了。
//也就是說,當出現MouseDown事件的時候,判斷三個鍵的狀態,如果中間鍵被按下,
//而其它兩個鍵沒有被按下,則認為是中間鍵按下的事件。在該事件中,記錄下當時滑鼠的位置
            
if(e.MiddleButton==MouseButtonState.Pressed&&e.LeftButton==MouseButtonState.Released&&e.RightButton==MouseButtonState.Released)
            {
                previousPoint = e.GetPosition(outside);
                isTranslateStart = true;
            }
            e.Handled = true;
        }

        private void outsidewrapper_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (e.MiddleButton == MouseButtonState.Pressed && e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released)
            {
                if (isTranslateStart)
                {
                    Point currentPoint = e.GetPosition(outside);  //不能用 inside,必須用outside
                    Vector v = currentPoint - previousPoint;
                    TransformGroup tg = inside.RenderTransform as TransformGroup;                   
                    tg.Children.Add(new TranslateTransform(v.X,v.Y));  //centerX和centerY用外部包裝元素的座標,不能用內部被變換的Canvas元素的座標
                //    inside.RenderTransform = tg;
                    previousPoint = currentPoint;
                }

            }
            e.Handled = true;
        }

        private void outside_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (e.MiddleButton == MouseButtonState.Pressed && e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released)
            {
                if (isTranslateStart)
                {
                    isTranslateStart = false;
                }
            }
            e.Handled = true;
        }

        private void outside_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {

            Point currentPoint = e.GetPosition(outside);  //不能用 inside,必須用outside
               
            TransformGroup tg = inside.RenderTransform as TransformGroup;

            double s = ((double)e.Delta) / 1000.0+1.0;

            //centerX和centerY用外部包裝元素的座標,不能用內部被變換的Canvas元素的座標
            tg.Children.Add(new ScaleTransform(s,s,currentPoint.X,currentPoint.Y));  
            e.Handled = true;
        }
    }

需要注意事項包括:

(1)包裝的元素需要新增ClipToBounds="True"屬性,這樣內部Canvas超出包裝元素的時候,超出部分就會被裁剪掉;

(2)把Canvas元素的初始大小設定為與包裝元素一樣大小,可以通過RelativeSource來設定:Width="{Binding Path=ActualWidth,RelativeSource={RelativeSource AncestorType=Border}}" ,Height="{Binding Path=ActualHeight,RelativeSource={RelativeSource AncestorType=Border}}"。

(3)將外包包裝元素的背景和Canvas的背景設定為一樣的背景,這樣當Canvas平移的時候,不會給然感覺Canvas畫布在移動,而是感覺內部元素(比如線、多邊形、圓形、矩形、文字等等)。或者Canvas不設定背景也是可行的(但可能Canvas會收不到滑鼠事件......)。

(4)用滑鼠事件控制平移的話,可以使用隧道事件,且事件應該關聯在外部包裝元素Border上。平移的大小用外包裝元素座標系統中的座標點進行計算。當然具體根據需要採用合適的事件,比如滾輪事件之類的,觸控事件之類的。

(5)雖然WPF元素存在MouseLeftButtonDown和MouseRightButtonDown事件,但卻沒找到MouseMiddleButtonDown事件。然而,幸運的是,MouseDown事件是不論哪個滑鼠鍵按下的時候都會觸發的,因此可以利用該事件,並通過檢測哪個鍵被按下的狀態引數來判斷是否是中間鍵被按下了。也就是說,當出現MouseDown事件的時候,判斷三個鍵的狀態,如果中間鍵被按下,而其它兩個鍵沒有被按下,則認為是中間鍵按下的事件。在該事件中,記錄下當時滑鼠的位置。滑鼠移動的時候,獲取新的滑鼠位置,並與記錄為物件成員變數的前一個滑鼠位置相減,即刻得知平移量,通過向Canvas的TransformGroup中新增平移變換,即可實現平移變換。

(6)理解:按理說所有元素的變換都是針對自身的座標體系的,而不是針對外部父元素的座標體系的,但是WPF有個特點,雖然時針對自身座標系的變化,但是變換前後自身的ActualWidth和ActualHeight都沒有發生任何變化,下次繼續變換的時候,還是用的ActualWidth和ActualHeigth作為自身座標系的參考用途,而不是按照變換後的實際尺寸在定位自身座標系尺寸的。所以,使用外部包裝元素的座標系來給定每次變換的(CenterX和CenterY)是可行的。這一點需要慢慢理解。