for WPF developers
Home Profile Tips 全記事一覧

ドラッグ操作でゴーストを表示する

(2017/06/27 7:32:15 created.)

(2017/06/27 7:42:01 modified.)

ドラッグ操作を実装したとき、ドラッグ対象のコントロールが半透明になってマウスポインタに追従するようになると、操作感が格段に良くなります。ここでは Adorner クラスを使用して実現する方法を紹介します。

ここでは最終的に次のような動作が実現できるようなサンプルを紹介します。


Adorner クラス

Adorner クラスは、UIElement クラスを装飾するための FrameworkElement クラスの派生クラスです。また、Adorner クラスを表示するには、装飾層と呼ばれるレイヤーが必要になり、これは AdornerDecorator クラスによって提供されます。AdornerDecorator クラスは Grid コントロールなどの標準的なパネルコントロールに含まれているため、意図的に AdornerDecorator を XAML に記述することはあまりありません。

ゴーストを表示するための Adorner 派生クラス

まずゴーストを表示するために次のようなクラスを定義します。

GhostAdorner.cs
  1. namespace Tips_Adorner.Views.Behaviors
  2. {
  3.     using System.Windows;
  4.     using System.Windows.Documents;
  5.     using System.Windows.Media;
  6.  
  7.     /// <summary>
  8.     /// ゴーストを表示する装飾用コントロールを表します。
  9.     /// </summary>
  10.     internal class GhostAdorner : Adorner
  11.     {
  12.         /// <summary>
  13.         /// 新しいインスタンスを生成します。
  14.         /// </summary>
  15.         /// <param name="visual">装飾する要素を指定します。</param>
  16.         /// <param name="adornedElement">装飾に用いる要素を指定します。</param>
  17.         /// <param name="point">装飾を表示する位置を、装飾する要素に対する相対位置として指定します。</param>
  18.         /// <param name="offset">装飾を表示する位置に対するオフセットを指定します。</param>
  19.         public GhostAdorner(Visual visual, UIElement adornedElement, Point point, Point offset)
  20.             : base(adornedElement)
  21.         {
  22.             this._layer = AdornerLayer.GetAdornerLayer(visual);
  23.             this.CurrentPoint = point;
  24.             this.Offset = offset;
  25.  
  26.             Attach();
  27.         }
  28.  
  29.         #region 依存関係プロパティ
  30.  
  31.         /// <summary>
  32.         /// CurrentPoint 依存関係プロパティの定義
  33.         /// </summary>
  34.         public static readonly DependencyProperty CurrentPointProperty = DependencyProperty.Register("CurrentPoint", typeof(Point), typeof(GhostAdorner), new FrameworkPropertyMetadata(default(Point), FrameworkPropertyMetadataOptions.AffectsRender));
  35.  
  36.         /// <summary>
  37.         /// ゴーストの表示位置を取得または設定します。
  38.         /// </summary>
  39.         public Point CurrentPoint
  40.         {
  41.             get { return (Point)GetValue(CurrentPointProperty); }
  42.             set { SetValue(CurrentPointProperty, value); }
  43.         }
  44.  
  45.         /// <summary>
  46.         /// Offset 依存関係プロパティの定義
  47.         /// </summary>
  48.         public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register("Offset", typeof(Point), typeof(GhostAdorner), new FrameworkPropertyMetadata(default(Point), FrameworkPropertyMetadataOptions.AffectsRender));
  49.  
  50.         /// <summary>
  51.         /// ゴーストの表示位置のオフセットを取得または設定します。
  52.         /// </summary>
  53.         public Point Offset
  54.         {
  55.             get { return (Point)GetValue(OffsetProperty); }
  56.             set { SetValue(OffsetProperty, value); }
  57.         }
  58.  
  59.         #endregion 依存関係プロパティ
  60.  
  61.         #region 公開メソッド
  62.  
  63.         /// <summary>
  64.         /// アタッチします。
  65.         /// </summary>
  66.         public void Attach()
  67.         {
  68.             if (this._layer != null)
  69.             {
  70.                 if (!this._isAttached)
  71.                 {
  72.                     this._layer.Add(this);
  73.                     this._isAttached = true;
  74.                 }
  75.             }
  76.         }
  77.  
  78.         /// <summary>
  79.         /// デタッチします。
  80.         /// </summary>
  81.         public void Detach()
  82.         {
  83.             if (this._layer != null)
  84.             {
  85.                 if (this._isAttached)
  86.                 {
  87.                     this._layer.Remove(this);
  88.                     this._isAttached = false;
  89.                 }
  90.             }
  91.         }
  92.  
  93.         #endregion 公開メソッド
  94.  
  95.         #region 描画オーバーライド
  96.  
  97.         /// <summary>
  98.         /// 描画処理のオーバーライド
  99.         /// </summary>
  100.         /// <param name="drawingContext">描画先のコンテキストを指定します。</param>
  101.         protected override void OnRender(DrawingContext drawingContext)
  102.         {
  103.             var pt = new Point(this.CurrentPoint.X + this.Offset.X, this.CurrentPoint.Y + this.Offset.Y);
  104.             var rect = new Rect(pt, this.AdornedElement.RenderSize);
  105.             var brush = new VisualBrush(this.AdornedElement);
  106.             brush.Opacity = 0.3;
  107.  
  108.             drawingContext.DrawRectangle(brush, null, rect);
  109.         }
  110.  
  111.         #endregion 描画オーバーライド
  112.  
  113.         #region private フィールド
  114.  
  115.         /// <summary>
  116.         /// 装飾層
  117.         /// </summary>
  118.         private AdornerLayer _layer;
  119.  
  120.         /// <summary>
  121.         /// アタッチされているかどうか
  122.         /// </summary>
  123.         private bool _isAttached;
  124.  
  125.         #endregion private フィールド
  126.     }
  127. }

このクラスは OnRender() メソッドをオーバーライドしており、AdornedElement という UIElement クラスを VisualBrush にし、Opacity を指定して DrawRectangle() メソッドで描画しています。つまり、AdornedElement に指定されたコントロールを半透明にして表示している、まさにゴーストを表示していることになります。

描画処理の中で、ゴーストを表示させる位置を CurrentPoint プロパティと Offset プロパティから決定しています。CurrentPoint プロパティはゴーストの現在位置、Offset プロパティは CurrentPoint プロパティからのオフセットを表しています。

コンストラクタでは、この Adorner クラスを描画するための装飾層を取得しています。AdornerLayer.GetAdornerLayer() メソッドで、指定したコントロールに対する装飾層を取得できます。ここで取得した装飾層をプライベートフィールドである _layer 変数で保持し、各メソッド内で使い回しています。

Attach() メソッドで、このクラスを装飾層に登録することで実際に描画させています。また、この装飾の描画を取りやめるための Detach() メソッドも用意しています。

ゴーストを表示するための添付ビヘイビア

前節で定義したクラスを実際に使ってみます。ここでは添付ビヘイビアに組み込んでみます。

AdornerBehavior.cs
  1. namespace Tips_Adorner.Views.Behaviors
  2. {
  3.     using System.Windows;
  4.     using System.Windows.Controls;
  5.     using System.Windows.Documents;
  6.     using System.Windows.Input;
  7.     using System.Windows.Media;
  8.  
  9.     public class AdornerBehavior
  10.     {
  11.         /// <summary>
  12.         /// IsEnabled 添付プロパティの定義
  13.         /// </summary>
  14.         public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(AdornerBehavior), new PropertyMetadata(false, OnIsEnabledPropertyChanged));
  15.  
  16.         /// <summary>
  17.         /// IsEnabled 添付プロパティを取得します。
  18.         /// </summary>
  19.         /// <param name="target">対象とする DependencyObject を指定します。</param>
  20.         /// <returns>取得した値を返します。</returns>
  21.         public static bool GetIsEnabled(DependencyObject target)
  22.         {
  23.             return (bool)target.GetValue(IsEnabledProperty);
  24.         }
  25.  
  26.         /// <summary>
  27.         /// IsEnabled 添付プロパティを設定します。
  28.         /// </summary>
  29.         /// <param name="target">対象とする DependencyObject を指定します。</param>
  30.         /// <param name="value">設定する値を指定します。</param>
  31.         public static void SetIsEnabled(DependencyObject target, bool value)
  32.         {
  33.             target.SetValue(IsEnabledProperty, value);
  34.         }
  35.  
  36.         /// <summary>
  37.         /// IsEnabled 添付プロパティ変更イベントハンドラ
  38.         /// </summary>
  39.         /// <param name="sender">イベント発行元</param>
  40.         /// <param name="e">イベント引数</param>
  41.         private static void OnIsEnabledPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
  42.         {
  43.             var element = sender as UIElement;
  44.             if (element == null)
  45.                 return;
  46.             var isEnabled = GetIsEnabled(element);
  47.             if (isEnabled)
  48.             {
  49.                 element.PreviewMouseLeftButtonDown += element_PreviewMouseLeftButtonDown;
  50.                 element.PreviewMouseMove += element_PreviewMouseMove;
  51.                 element.PreviewMouseLeftButtonUp += element_PreviewMouseLeftButtonUp;
  52.             }
  53.             else
  54.             {
  55.             }
  56.         }
  57.  
  58.         /// <summary>
  59.         /// 装飾用コントロール
  60.         /// </summary>
  61.         private static GhostAdorner _adorner;
  62.  
  63.         /// <summary>
  64.         /// PreviewMouseLeftButtonDown イベントハンドラ
  65.         /// </summary>
  66.         /// <param name="sender">イベント発行元</param>
  67.         /// <param name="e">イベント引数</param>
  68.         static void element_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
  69.         {
  70.             var originalElement = e.OriginalSource as FrameworkElement;
  71.             var parent = originalElement != null ? FindAncestor<Panel>(originalElement) : null;
  72.             var adornedElement = sender as FrameworkElement;
  73.             if ((parent == null) || (adornedElement == null))
  74.                 return;
  75.  
  76.             var pt = e.GetPosition(adornedElement);
  77.             var offset = new Point(-pt.X, -pt.Y);
  78.             _adorner = new GhostAdorner(parent, adornedElement, pt, offset);
  79.  
  80.             adornedElement.CaptureMouse();
  81.         }
  82.  
  83.         /// <summary>
  84.         /// PreviewMouseLeftButtonUp イベントハンドラ
  85.         /// </summary>
  86.         /// <param name="sender">イベント発行元</param>
  87.         /// <param name="e">イベント引数</param>
  88.         static void element_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  89.         {
  90.             if (_adorner != null)
  91.             {
  92.                 _adorner.AdornedElement.ReleaseMouseCapture();
  93.                 _adorner.Detach();
  94.                 _adorner = null;
  95.             }
  96.         }
  97.  
  98.         /// <summary>
  99.         /// PreviewMouseMove イベントハンドラ
  100.         /// </summary>
  101.         /// <param name="sender">イベント発行元</param>
  102.         /// <param name="e">イベント引数</param>
  103.         static void element_PreviewMouseMove(object sender, MouseEventArgs e)
  104.         {
  105.             if (_adorner != null)
  106.             {
  107.                 if (_adorner.AdornedElement.IsMouseCaptured && (e.LeftButton == MouseButtonState.Pressed))
  108.                 {
  109.                     var pt = e.GetPosition(_adorner.AdornedElement);
  110.                     _adorner.CurrentPoint = pt;
  111.                 }
  112.             }
  113.         }
  114.  
  115.         /// <summary>
  116.         /// 指定された型の親要素を探します。
  117.         /// </summary>
  118.         /// <typeparam name="T">親要素の型を指定します。</typeparam>
  119.         /// <param name="element">探索を開始する要素を指定します。</param>
  120.         /// <returns>親要素を返します。</returns>
  121.         private static T FindAncestor<T>(FrameworkElement element)
  122.             where T : FrameworkElement
  123.         {
  124.             do
  125.             {
  126.                 element = VisualTreeHelper.GetParent(element) as FrameworkElement;
  127.                 if (element is T)
  128.                     return element as T;
  129.             } while (element != null);
  130.             return null;
  131.         }
  132.     }
  133.  }

やり方はそれぞれですが、ここで肝心なのは、PreviewMouseLeftButtonDown イベントハンドラで GhostAdorner クラスのインスタンスを生成し、PreviewMouseMove イベントハンドラで CurrentPoint プロパティを更新し、PreviewMouseLeftButtonUp イベントハンドラで GhostAdorner クラスをデタッチしているということです。

これを使用した XAML の例を以下に示します。

MainView.xaml
  1. <Window x:Class="Tips_Adorner.Views.MainView"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         xmlns:b="clr-namespace:Tips_Adorner.Views.Behaviors"
  5.         Title="MainView" Height="300" Width="300">
  6.     <Grid>
  7.         <Border b:AdornerBehavior.IsEnabled="True">
  8.             <Border.Style>
  9.                 <Style TargetType="Border">
  10.                     <Setter Property="BorderBrush" Value="Red" />
  11.                     <Setter Property="BorderThickness" Value="2" />
  12.                     <Setter Property="CornerRadius" Value="4" />
  13.                     <Setter Property="Padding" Value="4" />
  14.                     <Setter Property="Background" Value="Transparent" />
  15.                     <Setter Property="HorizontalAlignment" Value="Center" />
  16.                     <Setter Property="VerticalAlignment" Value="Center" />
  17.                 </Style>
  18.             </Border.Style>
  19.  
  20.             <TextBlock Text="Drag me." />
  21.         </Border>
  22.     </Grid>
  23. </Window>

定義した添付ビヘイビアを Border コントロールに添付しているため、ゴーストは Border コントロール以下のコントロールになります。