[uwp]自定義圖形裁切控件


 

  開始之前,先上一張美圖。圖中的花叫什么,我已經忘了,或者說從來就不知道,總之謂之曰“野花”。只記得花很美,很香,春夏時節,漫山遍野全是她。這大概是七八年前的記憶了,不過她依舊會很准時的在山上沐浴春光,燦爛盛開,只是我看不到罷了。

 

  文藝過后,就要看到重點了。上圖是Windows10自帶的圖片裁切工具,應該是作為插件集成在“照片”應用中。當然不止於此,幾乎所有涉及照片上傳類的APP,都會提供裁切圖片這個基本功能。實現方式有很多種,我這兒給出自己的一種解決方案。

  先上效果圖:

  大致分析如下:

    圖片本身不作為裁切工具的一部分,只是把裁切控件放在圖片上層,然后調整四個按鈕,選出想要裁切的區域,計算出裁切區域的坐標和長寬信息,然后根據比例應用到圖片上面,從而實現裁              切。這篇博文主要描述怎么實現裁切控件本身,而實際裁切圖片等不進行討論。

 

  知道了要干什么,接着就要想想怎么辦。

 

  由於最終要計算出一個裁切區域,所以控件實現一個自定義附加屬性,用來對外提供裁切區域信息,為了簡單,直接選用Windows.Foundation.Rect這個結構體來描述。

  就該控件自身結構來講由Canvas+Path+Button * 4這六個主要的控件來實現。

 

    Canvas:作為容器,用來承載Path,Button等,關鍵是方便操作子元素的位置等。

    Button:很明顯的四個拖拽點,這兒用Button.Template重寫了Button的外觀,將其改為一個圓(Ellipse)

        改變中間矩形區域大小就是通過拖拽Button來實現,顯然Button支持拖拽,這兒我用自定義Behavior實現它的拖拽功能,關於該Behavior的實現,可以參看上一篇博文《[uwp]自定義Behavior之隨意拖動》

    Path:一個填充路徑。看到上圖中黑色半透明部分,就是該對象的可視部分。具體是通過兩個矩形減去重疊區域實現,第一個矩形就是和Canvas等大的一個矩形,第二個矩形就是中間透明區域的矩形,兩個矩形進行減去重疊區域的運算后,就可以得到Path的區域。具體的減去操作也很簡單,通過GeometryGroup實現,設置其填充規則為FillRule.EvenOdd即可。事實上,經過分解這個Path后,最終就回歸到怎么計算中間透明區域大小的問題上,而這個問題,可以通過四個Button的位置來計算。

 

  通過上面的分析,只需要計算四個Button的位置信息即可,那么這個時候,就可以利用XAML強大的依賴屬性系統(DependencyProperty),通過數據綁定等技巧來實際操作。

  針對四個Button的位置信息,分析如下:

  1.四個Button,為了在拖動的任意時刻,保持一個矩形區域,當一個按鈕移動時,和他同行或者同列的按鈕會跟着動,變化量相同。(此處用左上,右上,左下,右下來標識四個Button)

   所以可以選左上和右下兩個Button為主動點,他們的位置定了,另外兩個也就定了。值得注意的是,Button的位置是通過附加屬性Canvas.Left和Canvas.Top來確定的。所以讓左下的Canvas.Left和左上的Canvas.Left綁定,左下的Canvas.Top和右下的Canvas.Top綁定;讓右上的Canvas.Left和右下的Canvas.Left綁定,右上Canvas.Top和左上的Canvas.Top綁定。經過綁定之后,左上和右下的位置變化,就能引起左下和右上的位置變化,如果將以上綁定全部設置為雙向綁定,那么左下和右上的變化也就同樣能引起其他連個主動點的變化。

  2.確定了兩個主動點后,便可以自定義一個類來表示這兩個主動點的一些信息了(設置坐標X1,Y1,X2,Y2)。在接下來的實現中,用PointModel這個類來表示。  

  

  最終,只需要關注PointModel中兩個主動點坐標的變化即可。

  為了檢測這種變化,PointModel中定義了四個屬性X1,Y1,X2,Y2,在他們的Set方法中,包含了控制矩形大小和主動點自身位置(邊界檢測和兩個Button靠近檢測)的一些邏輯。

  接着貼出PointModel的代碼:

        public class PointModel : INotifyPropertyChanged
        {
            private double _x1;//代表左上Button的Canvas.Left
            public double X1
            {
                get { return _x1; }
                set
                {
                    double abspos = 0 - _buttonWidth / 2.0;//button最左可以到達的位置
                    if (value < abspos)//如果實際位置還小於該最小位置,
                    {
                        _x1 = abspos;//則強制修改Button的位置到最邊界處
                        _call?.Invoke("X1", _x1);//通知修改Button位置
                        _rectcall?.Invoke();//修改矩形區域位置
                        return;
                    }

                    if ((_x2 - value) >= _minRectWidth)//如果Button和同行的button間距大於_minRectWidth,屬正常情況
                    {
                        _x1 = value;
                        OnPropertyChanged();
                    }
                    else//如果小於該最小間距
                    {
                        _x1 = _x2 - _minRectWidth;//根據最小間距,強制修改Button位置。
                        _call?.Invoke("X1", _x1);//通知修改Button位置
                    }
                    _rectcall?.Invoke();//修改矩形區域位置
                }
            }

            private double _y1;
            public double Y1
            {
                get { return _y1; }
                set
                {
                    double abspos = 0 - _buttonWidth / 2.0;
                    if (value < abspos)
                    {
                        _y1 = abspos;
                        _call?.Invoke("Y1", _y1);
                        _rectcall?.Invoke();
                        return;
                    }
                    if ((_y2 - value) >= _minRectWidth)
                    {
                        _y1 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _y1 = _y2 - _minRectWidth;
                        _call?.Invoke("Y1", _y1);

                    }
                    _rectcall?.Invoke();
                }
            }

            private double _x2;
            public double X2
            {
                get { return _x2; }
                set
                {

                    double abspos = CanvasRect.Width - _buttonWidth / 2.0;
                    if (value > abspos)
                    {
                        _x2 = abspos;
                        _call?.Invoke("X2", _x2);
                        _rectcall?.Invoke();
                        return;
                    }

                    if ((value - _x1) >= _minRectWidth)
                    {
                        _x2 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _x2 = _minRectWidth + _x1;
                        _call?.Invoke("X2", _x2);
                    }
                    _rectcall?.Invoke();
                }
            }

            private double _y2;
            public double Y2
            {
                get { return _y2; }
                set
                {
                    double abspos = CanvasRect.Height - _buttonWidth / 2.0;
                    if (value > abspos)
                    {
                        _y2 = abspos;
                        _call?.Invoke("Y2", _y2);
                        _rectcall?.Invoke();
                        return;
                    }

                    if ((value - _y1) >= _minRectWidth)
                    {
                        _y2 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _y2 = _y1 + _minRectWidth;
                        _call?.Invoke("Y2", _y2);
                    }
                    _rectcall?.Invoke();
                }
            }


            public event PropertyChangedEventHandler PropertyChanged;
            public void OnPropertyChanged([CallerMemberName] string propertyName = "")
            {
                var handler = PropertyChanged;
                handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }

            /// <summary>
            /// 用於限制Button靠近邊界和互相靠近的回調方法
            /// </summary>
            private Action<String, double> _call;
            /// <summary>
            /// 用於改變矩形區域大小的回調方法
            /// </summary>
            private Action _rectcall;

            private Rect _canvasRect;//代表中間透明矩形區域
            public Rect CanvasRect
            {
                get { return _canvasRect; }
                set
                {
                    _canvasRect = value;
                    OnPropertyChanged();
                }
            }

            private double _buttonWidth; //Button的寬度
            private double _minRectWidth;//中間透明矩形區域的最小寬度,不能讓四個點重合,這兒最小寬度和最小高度都用這個來表示
            public PointModel(Action<string, double> pointAction, Action rectAction, double btnWidth, double minRectWidth)
            {
                _call = pointAction;
                _rectcall = rectAction;
                _buttonWidth = btnWidth;
                _minRectWidth = minRectWidth;
            }
        }
        public class RectModel : INotifyPropertyChanged
        {
            private GeometryGroup _group;
            public GeometryGroup Group
            {
                get { return _group; }
                set
                {
                    _group = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Group"));
                }
            }
            public event PropertyChangedEventHandler PropertyChanged;
        }    
View Code

  其中最繁雜的部分就是Set方法里面控制Button位置的代碼,主要有以下兩部分:

    1.保證Button不超出邊界

    2.保證Button和其他Button的最小間距。

 

  在PointModel的構造器中,加入了兩個Action,一個SetStaticPoint用來控制Button位置,另一個SetRect用來控制中間透明矩形的大小

    1.針對SetStaticPoint,不同的坐標執行不同的設置方法。

    2.針對SetRect,里面包含了構造Path的方法,如下

        private void SetRect()
        {
            if (group == null)
            {
                group = new GeometryGroup();
                group.FillRule = FillRule.EvenOdd;//設置規則為減去重疊部分。
            }
            group.Children.Clear();
            group.Children.Add(new RectangleGeometry() { Rect = new Rect { X = 0, Y = 0, Height = surface.ActualHeight, Width = surface.ActualWidth } });//大矩形區域,和Canvas同樣大小

            ClipRect = new Rect { X = Points.X1 + ButtonWidth / 2.0, Y = Points.Y1 + ButtonWidth / 2.0, Width = Points.X2 - Points.X1, Height = Points.Y2 - Points.Y1 };//中間透明區域大小
            group.Children.Add(new RectangleGeometry() { Rect = ClipRect });

            RectPath.Group = group;
        }    

 

  大致核心如上,其他部分都是細枝末節了。最后我把整個邏輯用UserContrl做了一個簡單的整合,弄了一個ClipRectangle控件。

  可以直接作為普通控件使用,只需要設置該控件的Width和Height即可,最后的裁切區域結果,通過一個自定義屬性ClipRectProperty來提供。

  

  

  在寫這個控件過程中,遇到幾個問題和心得如下:

    1.x:Bind:這種綁定方式好像不支持針對附加屬性的雙向綁定,單向沒問題

    2.起初一直嘗試直接用Rectangle來實現,但都不行,直到后來用Blend把兩個矩形進行相減合並時,發現XAML中把原來的Rectangle換成了Path,於是才有了想法。

    3.DependencyObject是可以說是XAML的核心,這東西一定要學好(現在只是大概會用而已)

  

  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割線

 

  點擊這兒下載源碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM