まめ - たんたんめん

備忘録 C# / WPF 多め

WPF コントロールにグリップでサイズを変更する機能を付与するBehaviorを作る

TextBoxやButton等のWPFの標準コントロールに簡単にサイズを変える機能を添付する方法を紹介します

一番シンプルなのはコードビハインドのMouseDownやMouseMoveをごにょごにょして実装する方法ですが
あまりスマートではないし、コントロール事に実装しないといけなくて大変です。そこで今回はビヘイビアとして追加してみることにしました。

今回は個人で作っているNodeEditorに拡張機能として実装する目的で作成しました。
f:id:at12k313:20190828153113g:plain

もちろんTextBoxやButton等にも適用できます。
f:id:at12k313:20190828153518g:plain

実装方針

直感的なxamlでGrip機能を付けれるようにします。
ゴールは以下のxamlです。

        <Button Content="ボタンです。">
            <i:Interaction.Behaviors>
                <local:GripperBehavior/>
            </i:Interaction.Behaviors>
        </Button>

        <TextBox Text="TextBoxです。">
            <i:Interaction.Behaviors>
                <local:GripperBehavior/>
            </i:Interaction.Behaviors>
        </TextBox>

※ System.Windows.Intaractive.dllが必要です。
BlendSDKから取得してください、もしくは再配布されているOSS等を利用します。

グリップ部分の実装

サイズを変更する部分を実装します。
ここはVIewを持つので動的にVIewを生成する仕組みを考えます。
今回は実装がシンプルになるのでAdonerを利用します。以下にコードを示します。

    public class Gripper : Adorner
    {
        // ドラッグイベントが用意されていて便利なのでThumbコントロールを利用します。
        private readonly Thumb _resizeGrip;
        private readonly VisualCollection _visualChildren;

        public Gripper(UIElement adornedElement , ControlTemplate controlTemplate) : base(adornedElement)
        {
            _resizeGrip = new Thumb
            {
                Cursor = Cursors.SizeNWSE
            };
            _resizeGrip.SetValue(WidthProperty,18d);
            _resizeGrip.SetValue(HeightProperty, 18d);
            _resizeGrip.DragDelta += OnGripDelta;

            _resizeGrip.Template = controlTemplate ?? MakeDefaultGripTemplate();

            _visualChildren = new VisualCollection(this)
            {
                _resizeGrip
            };
        }

        private ControlTemplate MakeDefaultGripTemplate()
        {
            //! 指定なしの場合の見た目を作成
            var visualTree = new FrameworkElementFactory(typeof(Border));
            visualTree.SetValue(VerticalAlignmentProperty, VerticalAlignment.Center);
            visualTree.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
            visualTree.SetValue(WidthProperty, 12d);
            visualTree.SetValue(HeightProperty, 12d);
            visualTree.SetValue(Border.BackgroundProperty, new SolidColorBrush(Colors.RoyalBlue));
            visualTree.SetValue(Border.CornerRadiusProperty, new CornerRadius(6));

            return new ControlTemplate(typeof(Thumb))
            {
                VisualTree = visualTree
            };
        }

        // Thumb のドラッグイベントです。
        private void OnGripDelta(object sender, DragDeltaEventArgs e)
        {
            if (AdornedElement is FrameworkElement frameworkElement)
            {
                var w = frameworkElement.Width;
                var h = frameworkElement.Height;
                if (w.Equals(double.NaN))
                    w = frameworkElement.DesiredSize.Width;
                if (h.Equals(double.NaN))
                    h = frameworkElement.DesiredSize.Height;

                w += e.HorizontalChange;
                h += e.VerticalChange;

                // clamp
                w = Math.Max(_resizeGrip.Width, w);
                h = Math.Max(_resizeGrip.Height, h);
                w = Math.Max(frameworkElement.MinWidth, w);
                h = Math.Max(frameworkElement.MinHeight, h);
                w = Math.Min(frameworkElement.MaxWidth, w);
                h = Math.Min(frameworkElement.MaxHeight, h);

                // ※ = で入れるとBindingが外れるので注意
                frameworkElement.SetValue(WidthProperty,w);
                frameworkElement.SetValue(HeightProperty,h);
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (AdornedElement is FrameworkElement frameworkElement)
            {
                var w = _resizeGrip.Width;
                var h = _resizeGrip.Height;
                var x = frameworkElement.ActualWidth - w;
                var y = frameworkElement.ActualHeight - h;

                _resizeGrip.Arrange(new Rect(x, y, w, h));
            }
            return finalSize;
        }

        protected override int VisualChildrenCount => _visualChildren.Count;

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

工夫した点としてはコンストラクタで見た目を指定できるようにしたことです。
nullを指定した場合は決め打ちで青い丸の様なグリップを追加します。

AdonerをBehaviorでラップしてxaml上から指定できるようにする

Adoner単体だとコントロールと組み合わせることができません。
そこで今回はBehaviorを利用してみることにします。

    public class GripperBehavior : Behavior<FrameworkElement>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            if (AssociatedObject.IsLoaded)
            {
                AdornerLayer.GetAdornerLayer(AssociatedObject)?.Add(new Gripper(AssociatedObject, null));
            }
            else
            {
                AssociatedObject.Loaded += (s, e) =>
                {
                    AdornerLayer.GetAdornerLayer(AssociatedObject)?.Add(new Gripper(AssociatedObject, null));
                };
            }
        }
    }

以上です。
Loadedが完了する前にAdornerLayer.GetAdornerLayer()がコールされるとnullが返ってくるので実装分岐をいれています。