リサイズハンドルをAdornerで実装する
今回はパワーポイントのようなオブジェクトのサイズ変更可能なリサイズハンドルを作ってみます。
Adonerの利用方法と実装のサンプル
まず、サイズ変更対象のオブジェクトの四隅にドラッグ可能なコントロール(リサイズ・ハンドル)を作ります。WPFではこのような用途にAdornerという仕組みが用意されています。ただしAdornerの実装サンプルを調べてみてもあまりヒットしません。またXAMLではなくコードベースのものしか見当たりません。
ResizingAdorner のサンプル | Microsoft Docs
http://denisvuyka.wordpress.com/2007/10/15/wpf-simple-adorner-usage-with-drag-and-resize-operations/
そもそもAdorner自体がXAMLで使うようになっていないので仕方がないと言えるのですが、装飾部分までもコードで書くのではカスタマイズが大変です。まず装飾部分をXAML上でテンプレートで定義できるようにAdornerを拡張するところから始めます。
AdornedByクラスの準備
まずTemplate添付プロパティを作ります。添付プロパティの説明は割愛しますが既に馴染みがあると思います。とか
今回の実装ではAdornerdBy.Template=〜を付けた要素を装飾対象になります。またTemplateにはControlTemplateを渡しますがこれが装飾部分の表示に使われます。
namespace WpfApplication1 { public class AdornedBy : Adorner { //添付プロパティの実装 public static readonly DependencyProperty TemplateProperty = DependencyProperty.RegisterAttached("Template", typeof(ControlTemplate), typeof(AdornedBy), new FrameworkPropertyMetadata(null, OnTemplateChanged)); public static ControlTemplate GetTemplate(DependencyObject obj) { return (ControlTemplate)obj.GetValue(TemplateProperty); } public static void SetTemplate(DependencyObject obj, ControlTemplate value) { obj.SetValue(TemplateProperty, value); } //テンプレート描画用のControlオブジェクトへの参照 private FrameworkElement _Content; //コンストラクタ public AdornedBy(UIElement adornedElement) : base(adornedElement) { } //添付プロパティTemplateの設定時に初期化処理を行う private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var adorned = d as FrameworkElement; var me = new AdornedBy(adorned); //装飾層に登録する if (adorned.IsInitialized) { me.AddToAdornerLayer(); }else{ //初期化中の場合は登録処理を遅延させる adorned.Loaded += (_, __) => me.AddToAdornerLayer(); } //子Controlオブジェクトを生成して設定されたテンプレートを設定する var t = e.NewValue as ControlTemplate; var ctrl = new Control { Template = t }; me._Content = ctrl; me.AddVisualChild(ctrl); me.AddLogicalChild(ctrl); me.InvalidateVisual(); } //装飾層に登録する private void AddToAdornerLayer(){ AdornerLayer layer = AdornerLayer.GetAdornerLayer(AdornedElement); if (layer == null) throw new InvalidOperationException("XAML tree must have at lest one AdornerDecorator."); //既存の装飾を除去 Adorner[] registed = layer.GetAdorners(AdornedElement); if (registed != null) { foreach (var ad in registed) { if (ad is AdornedBy) layer.Remove(ad); } } //装飾を登録 layer.Add(this); } //テンプレート中の要素から装飾対象を取得するヘルパーメソッド public static UIElement GetAdornedElementFromTemplateChild(FrameworkElement contained) { var tp = contained.TemplatedParent as FrameworkElement; if (tp == null || tp.GetType() != typeof(Control)) return null; var me = tp.Parent as AdornedBy; if (me == null) return null; return me.AdornedElement; } //サイズ計測処理の実装 (テンプレートの大きさを装飾対象に一致させる) protected override Size MeasureOverride(Size constraint) { return AdornedElement.DesiredSize; } //配置処理の実装 (テンプレートの位置を装飾対象に一致させる) protected override Size ArrangeOverride(Size finalSize) { _Content.Arrange(new Rect(AdornedElement.DesiredSize)); return AdornedElement.DesiredSize; } //描画されるために不可欠なので実装をしておく protected override int VisualChildrenCount { get { return 1; } } protected override Visual GetVisualChild(int index) { return _Content; } } }
添付プロパティが設定された時にAdornerの初期化処理をします。具体的にはAdonerを継承したAdornedByクラスのインスタンスを生成し、装飾表示用のレイヤーを探して登録します。またAdornedByの直下にControlクラスのインスタンスを作りTemplateの値を設定します。構造は次のような感じになります。
Window ├ 装飾層(AdornerDecorator) │ └ AdornerdBy (Adorner) │ └ Control │ └ ControlTemplate (Teplateの中身) └ 装飾対象
適切に装飾を表示させるために、いくつかのメソッドをオーバーライドしています。
MeasureOverride / ArrangeOverride
レイアウト機構に関係するもので装飾表示のサイズや位置合わせに関係します。
MeasureOverrideは装飾が要求するサイズを回答して、サイズを装飾対象と一致させます。ArrangeOverrideは表示位置の調整に関係しますが今回は特別なことはしていません。
GetVisulaChild / VisualChildrenCount
AdornerクラスがFrameworkElementクラスを継承していないので実装しないと子要素が適切に描画されないようです。
またAddLogicalChildを呼び出していますが、これはControl.Parentで値を取得するために必要です。なお今回は踏み込みませんがWPFにはビジュアルツリーと論理ツリーがあります。ビジュアルツリーは描画処理に、論理ツリーはParentやTemplatedParentあたりに関係します。
AdornedByの使い方
AdornedByを使って今回のリサイズハンドルの表示をXAMLで実装してみます。
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <!--リサイズハンドル用のテンプレート定義--> <ControlTemplate TargetType="Thumb" x:Key="ResizeHandleTemplate"> <Ellipse Width="10" Height="10" Margin="-3" Stroke="DimGray" Fill="LightSteelBlue"/> </ControlTemplate> <!--装飾用のテンプレート定義--> <ControlTemplate x:Key="AdornerTemplate"> <Grid> <Thumb Name="ResizeThumb_LT" HorizontalAlignment="Left" VerticalAlignment="Top" Template="{StaticResource ResizeHandleTemplate}" DragDelta="ResizeThumb_DragDelta"/> <Thumb Name="ResizeThumb_RT" HorizontalAlignment="Right" VerticalAlignment="Top" Template="{StaticResource ResizeHandleTemplate}" DragDelta="ResizeThumb_DragDelta"/> <Thumb Name="ResizeThumb_LB" HorizontalAlignment="Left" VerticalAlignment="Bottom" Template="{StaticResource ResizeHandleTemplate}" DragDelta="ResizeThumb_DragDelta"/> <Thumb Name="ResizeThumb_RB" HorizontalAlignment="Right" VerticalAlignment="Bottom" Template="{StaticResource ResizeHandleTemplate}" DragDelta="ResizeThumb_DragDelta"/> </Grid> </ControlTemplate> </Window.Resources> <Canvas> <!--単純な利用:装飾対象には添付プロパティを設定する--> <Button Name="Target" Canvas.Left="100" Canvas.Top="20" Width="100" Height="40" Content="サイズ変更可" my:AdornedBy.Template="{StaticResource AdornerTemplate}"/> <!--フォーカスで装飾を有効にする場合--> <Button Name="Target2" Canvas.Left="100" Canvas.Top="80" Width="100" Height="40" Content="フォーカスで"> <Button.Style> <Style TargetType="Button"> <Style.Triggers> <Trigger Property="IsFocused" Value="True"> <Setter Property="my:AdornedBy.Template" Value="{StaticResource AdornerTemplate}"/> </Trigger> </Style.Triggers> </Style> </Button.Style> </Button> </Canvas > </Window>
ThumbコントロールはMarginを負値にすることで少し外側に配置しています。また二つ目のボタンではフォーカス時のみリサイズハンドルを表示しています。
リサイズハンドルのイベント処理です。
//リサイズハンドルのイベント処理 private void ResizeThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { var thumb = sender as Thumb; if (thumb == null) return; //サイズ変更の対象要素を取得する var adored = AdornedBy.GetAdornedElementFromTemplateChild(thumb) as FrameworkElement; if (adored == null) return; //サイズ変更処理(横) if (thumb.Name == "ResizeThumb_LT" || thumb.Name == "ResizeThumb_LB") { Canvas.SetLeft(adored, Canvas.GetLeft(adored) + e.HorizontalChange); adored.Width = Math.Max(20, adored.Width - e.HorizontalChange); } else { adored.Width = Math.Max(20, adored.Width + e.HorizontalChange); } //サイズ変更処理(たて) if (thumb.Name == "ResizeThumb_LT" || thumb.Name == "ResizeThumb_RT") { Canvas.SetTop(adored, Canvas.GetTop(adored) + e.VerticalChange); adored.Height = Math.Max(20, adored.Height - e.VerticalChange); } else { adored.Height = Math.Max(20, adored.Height + e.VerticalChange); } e.Handled = true; }