C#のWPFでサイズ変更できるTextBoxを作る

WindowクラスにはResizeModeプロパティがあってサイズ変更可能なグリップを表示させる事が出来るが、同じような事をTextBoxでやってみる。

↓こんな感じ。

WPFにはAdornerというUIElementを装飾する為の抽象クラスがある。

Adornerを使ってサイズ変更するグリップ部分をTextBoxコントロールの上に重ねて表示する仕組み。


Adornerを継承したクラスを作成

    public class ResizingAdorner : Adorner
    {
        private Thumb            resizeGrip;
        private VisualCollection visualChildren;


        public ResizingAdorner(UIElement adornedElement) : base(adornedElement)
        {
            resizeGrip = new Thumb();
            resizeGrip.Cursor = Cursors.SizeNWSE;
            resizeGrip.Width = 18;
            resizeGrip.Height = 18;
            resizeGrip.DragDelta += new DragDeltaEventHandler(ResizeGripDragDelta);

            var p1 = new FrameworkElementFactory(typeof(Path));
            p1.SetValue(Path.FillProperty, new SolidColorBrush(Colors.White));
            p1.SetValue(Path.DataProperty, Geometry.Parse("M0,14L14,0L14,14z"));
            var p2 = new FrameworkElementFactory(typeof(Path));
            p2.SetValue(Path.StrokeProperty, new SolidColorBrush(Colors.LightGray));
            p2.SetValue(Path.DataProperty, Geometry.Parse("M0,14L14,0"));
            var p3 = new FrameworkElementFactory(typeof(Path));
            p3.SetValue(Path.StrokeProperty, new SolidColorBrush(Colors.LightGray));
            p3.SetValue(Path.DataProperty, Geometry.Parse("M4,14L14,4"));
            var p4 = new FrameworkElementFactory(typeof(Path));
            p4.SetValue(Path.StrokeProperty, new SolidColorBrush(Colors.LightGray));
            p4.SetValue(Path.DataProperty, Geometry.Parse("M8,14L14,8"));
            var p5 = new FrameworkElementFactory(typeof(Path));
            p5.SetValue(Path.StrokeProperty, new SolidColorBrush(Colors.LightGray));
            p5.SetValue(Path.DataProperty, Geometry.Parse("M12,14L14,12"));

            var grid = new FrameworkElementFactory(typeof(Grid));
            grid.SetValue(Grid.MarginProperty, new Thickness(2));
            grid.AppendChild(p1);
            grid.AppendChild(p2);
            grid.AppendChild(p3);
            grid.AppendChild(p4);
            grid.AppendChild(p5);

            var template = new ControlTemplate(typeof(Thumb));
            template.VisualTree = grid;
            resizeGrip.Template = template;

            visualChildren = new VisualCollection(this);
            visualChildren.Add(resizeGrip);
        }

        private void ResizeGripDragDelta(object sender, DragDeltaEventArgs e)
        {
            var element = this.AdornedElement as FrameworkElement;

            var w = element.Width;
            var h = element.Height;
            if (w.Equals(Double.NaN))
                w = element.DesiredSize.Width;
            if (h.Equals(Double.NaN))
                h = element.DesiredSize.Height;

            w += e.HorizontalChange;
            h += e.VerticalChange;
            w = Math.Max(resizeGrip.Width, w);
            h = Math.Max(resizeGrip.Height, h);
            w = Math.Max(element.MinWidth, w);
            h = Math.Max(element.MinHeight, h);
            w = Math.Min(element.MaxWidth, w);
            h = Math.Min(element.MaxHeight, h);

            element.Width = w;
            element.Height = h;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            var element = this.AdornedElement as FrameworkElement;

            var w = resizeGrip.Width;
            var h = resizeGrip.Height;
            var x = element.ActualWidth - w;
            var y = element.ActualHeight - h;

            resizeGrip.Arrange(new Rect(x, y, w, h));

            return finalSize;
        }

        protected override int VisualChildrenCount
        {
            get { return visualChildren.Count; }
        }

        protected override Visual GetVisualChild(int index)
        {
            return visualChildren[index];
        }
    }

サイズ変更用コントロールを作成 (9~13行目)

ドラッグ操作をしたいのでThumbコントロールを使用する。

カーソルをサイズ変更っぽいやつになるよう設定。

ドラッグ時の処理を追加する為、DragDeltaイベントへハンドラーを追加する。

 

 

 

Thumbコントロールの見た目を変える (15~41行目)

ThumbコントロールのTemplateを使ってデザインを変更する。

Template内のコントロールはFrameworkElementFactoryクラスを使って作成していく。

 

Pathクラスを使って白い三角形と斜線を作成。(15~29行目)

それらをGridクラスへ追加。(31~37行目)

それをControlTemplateクラスのVisualTreeプロパティへセット。(40行目)

更にそれをThumbコントロールのTemplateへセットする。(41行目)

 

 

作成したコントロールの管理 (43~44行目)

作成したThumbコントロールはVisualCollectionクラスを使って管理する。

Adornerが子要素を管理する場合、VisualChildrenCountプロパティとGetVisualChildメソッドをオーバライドして適切な値を返すよう作り込む必要がある。

※既存のコントロールを使わずDrawingContextを使って独自の描画を行うだけの場合は、VisualCollectionで管理する必要は無く

 OnRenderをオーバーライドして描画を実装する

 

VisualCollectionクラスを作成して、そこに作成したThumbコントロールを追加する。

 

 

ドラッグされたらTextBoxのサイズを変える (47~69行目)

 

装飾される側のコントロール(TextBox)はAdornedElementプロパティに入っている。

パラメータDragDeltaEventArgsに入っているドラッグの移動量を元にTextBoxの幅・高さを計算して、AdornedElementのWidth・Heightプロパティへセットする。

 

 

サイズが変更されたらThumbコントロールの位置を変える (71~83行目)

ArrangeOverrideメソッドをオーバーライドして、ThumbコントロールがTextBoxの右下の位置になるように座標を計算して、ThumbコントロールのArrangeメソッド呼び出して位置を変える。

 

 

Adornerで既存コントロールを管理する為のお約束 (85~93行目)

VisualChildrenCountプロパティをオーバーライドしてVisualCorrectionの件数を返す。

GetVisualChildメソッドをオーバライドしてVisualCorrectionの指定番目の要素を返す。

 

 

 

 

 


Adornerを追加したTextBoxを作成

    public class ResizableTextBox : TextBox
    {
        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            this.Loaded += new RoutedEventHandler(InitializeAdorner);
        }

        private void InitializeAdorner(object sender, RoutedEventArgs e)
        {
            var layer = AdornerLayer.GetAdornerLayer(this);
            var adorner = new ResizingAdorner(this);
            layer.Add(adorner);
        }
    }

OnInitializedメソッドをオーバーライド (3~7行目)

ロード時にAdornerをセットする為に、Loadedイベントにハンドラーを追加。

 

 

ロード時にAdornerをセットする (9~14行目)

AdornerLayerクラスのGetAdornerLayerメソッドを使ってAdonerを追加するレイヤーを取得。

そのレイヤーに作ったAdornerを追加する。