FC2ブログ

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ドラッグでListBoxItemを移動させる

 コレクションをListBoxにバインドし、データテンプレートを使用してデータを図で表示したと思うことがあります。しかも、ユーザーが見やすいように図の位置を微調整したい(図を動かして位置調整したい)こともあります。
 データテンプレートで設定した内容はListBoxItemというコンテナに入れられて表示されます。そのため、表示されている図をドラッグで移動させるにはListBoxItemを移動させる必要があります。
 ListBoxItemにビヘイビアを設定することはできません。そのため、Blend SDKのMouseDragElementBehaviorや前回作成したビヘイビアは使えません。
 そんなわけで、ListBoxItemをドラッグで移動させたいのならば別のビヘイビアを作成する必要があります。
 今回作成するビヘイビアの要点を上げると以下の様になります。
・ListBox用のビヘイビアを作成し、そのListBoxに含まれるListBoxItemをドラッグで移動できるようにする。
・せっかくなのでListBoxのパネルをビヘイビア側でCanvasに変更する。
・前回と同じようにX軸方向、Y軸方向の移動制限を行えるようにする。

 以下は、ListBoxItemを移動させるためのビヘイビアのコードです。(Blend SDKを使用しています)
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace WpfExtensions.Behaviors.Actions
{
    /// <summary>
    /// ListBoxItemをドラッグで移動させる為のビヘイビア
    /// </summary>
    public class MouseDragListBoxItemBehavior : Behavior<ListBox>
    {
        private bool IsDrag = false;
        private ListBoxItem TargetListBoxItem;
        private Point MouseStartPosition;
        private Point ListBoxItemStartPosition;
        private Canvas MoveArea;


        #region LockX
        /// <summary>
        /// X軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>
        public bool LockX
        {
            get { return (bool)GetValue(LockXProperty); }
            set { SetValue(LockXProperty, value); }
        }

        public static readonly DependencyProperty LockXProperty =
            DependencyProperty.Register("LockX", typeof(bool), typeof(MouseDragListBoxItemBehavior), new PropertyMetadata(false));
        #endregion

        #region LockY
        /// <summary>
        /// Y軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>

        public bool LockY
        {
            get { return (bool)GetValue(LockYProperty); }
            set { SetValue(LockYProperty, value); }
        }

        public static readonly DependencyProperty LockYProperty =
            DependencyProperty.Register("LockY", typeof(bool), typeof(MouseDragListBoxItemBehavior), new PropertyMetadata(false));
        #endregion

        protected override void OnAttached()
        {
            // ListBoxのItemsPanelにCanvasを指定する
            SetCanvas();

            AssociatedObject.Loaded += AssociatedObject_Loaded;
            AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObject_PreviewMouseLeftButtonDown;
            base.OnAttached();
        }        

        protected override void OnDetaching()
        {
            AssociatedObject.Loaded -= AssociatedObject_Loaded;
            AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObject_PreviewMouseLeftButtonDown;
            base.OnDetaching();
        }

        // ListBoxのItemsPanelTemplateにCanvasを設定する。
        private void SetCanvas()
        {
            var canvasFactory = new FrameworkElementFactory(typeof(Canvas));
            var itemsPanelTemplate = new ItemsPanelTemplate();
            itemsPanelTemplate.VisualTree = canvasFactory;
            itemsPanelTemplate.Seal();
            AssociatedObject.ItemsPanel = itemsPanelTemplate;
        }

        // Canvasインスタンスを取得する。
        // OnAttachedではインスタンスの取得ができなかったのでLoadedイベントで取得
        void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            MoveArea = (Canvas)FindControl(AssociatedObject, typeof(Canvas));
        }

        // ドラッグ開始
        void AssociatedObject_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (MoveArea == null) return;

            // マウスの位置を取得
            MouseStartPosition = e.GetPosition(AssociatedObject);

            // ドラッグ対象のListBoxItemを取得
            TargetListBoxItem = GetListBoxItem(MouseStartPosition);
            if (TargetListBoxItem == null) return;

            // ListBoxItemの位置を取得する
            ListBoxItemStartPosition = new Point(Canvas.GetLeft(TargetListBoxItem), Canvas.GetTop(TargetListBoxItem));
            if (double.IsNaN(ListBoxItemStartPosition.X)) ListBoxItemStartPosition.X = 0.0;
            if (double.IsNaN(ListBoxItemStartPosition.Y)) ListBoxItemStartPosition.Y = 0.0;

            // ListBoxItemのMouseMove, MouseLeftButtonUpイベントを登録
            TargetListBoxItem.MouseMove += TargetListBoxItem_MouseMove;
            TargetListBoxItem.MouseLeftButtonUp += TargetListBoxItem_MouseLeftButtonUp;

            // ドラッグ開始
            IsDrag = true;
            TargetListBoxItem.CaptureMouse();
        }

        // ListBoxItemのドラッグ移動処理
        void TargetListBoxItem_MouseMove(object sender, MouseEventArgs e)
        {
            if (MoveArea == null || TargetListBoxItem == null || !IsDrag) return;

            // 座標計算
            var currentMousePosition = e.GetPosition(AssociatedObject);
            var currentListBoxItemPosition = ListBoxItemStartPosition + (currentMousePosition - MouseStartPosition);

            if(!LockX)
            {
                var maxX = MoveArea.ActualWidth - TargetListBoxItem.ActualWidth;
                if(currentListBoxItemPosition.X < 0)
                {
                    currentListBoxItemPosition.X = 0.0;
                }
                else if(maxX < currentListBoxItemPosition.X)
                {
                    currentListBoxItemPosition.X = maxX;
                }

                Canvas.SetLeft(TargetListBoxItem, currentListBoxItemPosition.X);
            }

            if(!LockY)
            {
                var maxY = MoveArea.ActualHeight - TargetListBoxItem.ActualHeight;
                if(currentListBoxItemPosition.Y < 0)
                {
                    currentListBoxItemPosition.Y = 0.0;
                }
                else if(maxY < currentListBoxItemPosition.Y)
                {
                    currentListBoxItemPosition.Y = maxY;
                }

                Canvas.SetTop(TargetListBoxItem, currentListBoxItemPosition.Y);
            }

            e.Handled = true;
        }

        // ドラッグ終了(後始末)
        void TargetListBoxItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (TargetListBoxItem == null) return;

            IsDrag = false;
            TargetListBoxItem.ReleaseMouseCapture();
            TargetListBoxItem.MouseMove -= TargetListBoxItem_MouseMove;
            TargetListBoxItem.MouseLeftButtonUp -= TargetListBoxItem_MouseLeftButtonUp;
            TargetListBoxItem = null;

            e.Handled = true;
        }

        // 最初に見つかった目的のコントロールを返す
        private DependencyObject FindControl(DependencyObject obj, Type controlType)
        {
            if (obj == null) return null;
            if (obj.GetType() == controlType) return obj;

            var childrenCount = VisualTreeHelper.GetChildrenCount(obj);
            for(var i = 0; i < childrenCount; i++)
            {
                var child = VisualTreeHelper.GetChild(obj, i);
                var descendant = FindControl(child, controlType);
                if (descendant != null && descendant.GetType() == controlType) return descendant;
            }

            return null;
        }

        // マウスカーソル位置にあるListBoxItemを取得する。
        // ビヘイビアがアタッチされたListBoxのItemのみ返す。
        private ListBoxItem GetListBoxItem(Point position)
        {
            var result = VisualTreeHelper.HitTest(AssociatedObject, position);
            if (result == null) return null;

            // 目的のListBoxItemまで辿っていく
            var item = result.VisualHit;
            while(item != null)
            {
                if(item is ListBoxItem)
                {
                    // ビヘイビアがアタッチされたListBoxのListBoxItemなら終了
                    var index = AssociatedObject.ItemContainerGenerator.IndexFromContainer(item, true);
                    if (index >= 0) break;
                }

                // 次の調査項目を取得する
                item = VisualTreeHelper.GetParent(item);
            }

            return item != null ? (ListBoxItem)item : null;
        }
    }
}
 LockX, LockYについては前回と同じなので解説は省略します。
 SetCanvasメソッドの処理でListBoxのパネルをCanvasに置き換えています。
 C#でテンプレートをいじるなんて今回が初めてだったので、あまり詳しいことは解りませんが、テンプレートを操作するときにはFrameworkElementFactoryを使用して行うようです。
 FrameworkElementFactoryのインスタンスで作成したいコントロールの型を指定します。このfactoryインスタンスをItemPanelTemplateインスタンスのVisualTreeプロパティにセットします。
 あとはListBoxのItemsPanelにItemPanelTemplateインスタンスをセットするだけです。
 SetCanvasメソッドはOnAttachedメソッドで呼んでいますが、この時点ではまだCanvasは作成されていません。そのため、ここでCanvasを取得しようとすると失敗します。
 Canvasインスタンスの取得はLoadedイベントで行っています。
 コントロールを取得するにはVisualTreeHelperを使用して目的のコントロールの探索を行います。
 VisualTreeHelperで子要素を取得し、目的のコントロールと同じかどうかを調べています。

 ListBoxItemの移動ですが、座標計算等の処理は前回と同じなのですが、このビヘイビアはListBoxにアタッチされているので、まずは対象のListBoxItemを取得するところから始める必要があります。
 ListBoxのPreviewMouseLeftButtonDownイベントがドラッグ開始処理のトリガーとなります。
 ここでマウスの位置を元にドラッグの対象となるListBoxItemを取得します。
 ListBoxItem取得処理はGetListBoxItemメソッドで行っています。
 GetListBoxItemメソッドでは、ビヘイビアをアタッチしたListBoxのListBoxItemのみをターゲットにしています。
 これは、子要素にListBoxがある場合、子要素のListBoxのアイテムだけが移動してしまい、意図したとおりに移動できなくなることを避けるためです。
 そのため、ListBoxItemかどうかとAssociatedObject(アタッチしたListBox)に含まれるかどうかをチェックしています。

 ドラッグするのはListBoxItemなので、取得したListBoxItemのMouseMoveとMouseLeftButtonUpにイベントハンドラを追加します。
 MouseMoveの処理は前回とほぼ同じです。
 最後にe.Handledをtrueにしていますが、これを忘れると処理が重たくなり、移動がスムーズに行えなくなります。
 MouseLeftButtonUpでドラッグ終了時の処理を行っています。

 このビヘイビアをListBoxにアタッチさせれば、ListBoxItemをドラッグで移動できるようになります。
<Window x:Class="MouseDragDemo2.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:MouseDragDemo2.Views"
        xmlns:vm="clr-namespace:MouseDragDemo2.ViewModels"
        xmlns:ex="http://wpf-extensions.net/2014/wpf"
        Title="MainWindow" Height="300" Width="225">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
        
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <ListBox Grid.Row="0" ItemsSource="{Binding Items}">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Canvas.Left" Value="{Binding X, Mode=TwoWay}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Y, Mode=TwoWay}"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type vm:ItemViewModel}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" Text="{Binding Name}" Background="Gray" Foreground="White"/>
                        <ListBox Grid.Row="1" ItemsSource="{Binding Colors}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <Ellipse Stroke="Black" Fill="{Binding}" Width="20" Height="20"/>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <i:Interaction.Behaviors>
                <ex:MouseDragListBoxItemBehavior/>
            </i:Interaction.Behaviors>
        </ListBox>
        
        <ListView Grid.Row="1" ItemsSource="{Binding Items}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="名前" DisplayMemberBinding="{Binding Name}"/>
                    <GridViewColumn Header="X" DisplayMemberBinding="{Binding X}"/>
                    <GridViewColumn Header="Y" DisplayMemberBinding="{Binding Y}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
 ViewModelではバインドするデータを用意しているだけなので、コードは省略します。
 ListBoxにMouseDragListBoxItemBehaviorをアタッチすれば、ListBoxItemの移動ができるようになります。
 アイテムの見た目はデータテンプレートで変更しています。見ての通り、内部にListBoxを持っています。
 ListBoxItemの座標とバインディングしたい場合は、ListBox.ItemContainerStyleでバインディングを行います。
 これを実行すると以下の様になります。
ListBoxItemDragMove.png
 アタッチしたListBoxのアイテムのみが対象になっているので、ListBoxItemの中にある子ListBoxのアイテム(色つきの○)をドラッグしても、問題なく移動できます。
スポンサーサイト

テーマ : プログラミング
ジャンル : コンピュータ

コメントの投稿

非公開コメント

カレンダー
10 | 2018/11 | 12
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 -
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
11  09  08  07  06  05  04  03  02  01  12  11  10  09  08  07  06  04  03  02  01  12  11  10  09  08  07  06  05  04  03  02  01  12  11  10  09 
最新記事
リンク
最新コメント
検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。