読者です 読者をやめる 読者になる 読者になる

ドラッグアンドドロップで並べ替えできるListBox

カスタムコントロールにするのはちょっと…
じゃあどうするか?ビヘイビアでしょ!

public class ListBoxDragDropBehavior : Behavior<ListBox>
{
    private ListBoxItem dragItem { get; set; }
    private Point dragStartPosition { get; set; }
    private DragAdorner dragAdorner { get; set; }
    private int MoveItemIndex { get; set; }

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObject_PreviewMouseLeftButtonDown;
        AssociatedObject.PreviewMouseMove += AssociatedObject_PreviewMouseMove;
        AssociatedObject.QueryContinueDrag += AssociatedObject_QueryContinueDrag;
        AssociatedObject.Drop += AssociatedObject_Drop;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObject_PreviewMouseLeftButtonDown;
        AssociatedObject.PreviewMouseMove -= AssociatedObject_PreviewMouseMove;
        AssociatedObject.QueryContinueDrag -= AssociatedObject_QueryContinueDrag;
        AssociatedObject.Drop -= AssociatedObject_Drop;
    }

    void AssociatedObject_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var index = GetItemIndex(e.GetPosition(AssociatedObject));
        if (index < 0) return;

        dragItem = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
        dragStartPosition = e.GetPosition(dragItem);
    }

    void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        MoveItemIndex = GetItemIndex(e.GetPosition(AssociatedObject));
        if (MoveItemIndex < 0) return;

        var listboxitem = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(MoveItemIndex) as ListBoxItem;

        if (e.LeftButton == MouseButtonState.Pressed && dragAdorner == null && dragItem == listboxitem)
        {
            var nowPos = e.GetPosition(listboxitem);
            if (Math.Abs(nowPos.X - dragStartPosition.X) > SystemParameters.MinimumHorizontalDragDistance ||
                Math.Abs(nowPos.Y - dragStartPosition.Y) > SystemParameters.MinimumVerticalDragDistance)
            {
                var layer = AdornerLayer.GetAdornerLayer(AssociatedObject);
                dragAdorner = new DragAdorner(AssociatedObject, listboxitem, 0.5, dragStartPosition);
                layer.Add(dragAdorner);
                DragDrop.DoDragDrop(listboxitem, listboxitem, DragDropEffects.Move);
                layer.Remove(dragAdorner);
                dragAdorner = null;
                dragItem = null;
            }
        }
    }

    void AssociatedObject_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
    {
        if (dragAdorner != null)
        {
            var mousePosition = NativeMethods.GetMousePosition(AssociatedObject);
            var listboxPosition = (Window.GetWindow(AssociatedObject)).PointFromScreen(AssociatedObject.PointToScreen(new Point(0, 0)));
            dragAdorner.LeftOffset = mousePosition.X - listboxPosition.X;
            dragAdorner.TopOffset = mousePosition.Y - listboxPosition.Y;
        }
    }

    void AssociatedObject_Drop(object sender, DragEventArgs e)
    {
        var items = AssociatedObject.ItemsSource as IList<IDragDropItem>;

        var dropPosition = e.GetPosition(AssociatedObject);
        var moveItem = AssociatedObject.Items[MoveItemIndex] as IDragDropItem;

        for (int i = 0; i < AssociatedObject.Items.Count; i++)
        {
            var item = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
            var pos = AssociatedObject.PointFromScreen(item.PointToScreen(new Point(0, item.ActualHeight / 2)));
            if (dropPosition.Y < pos.Y)
            {
                items.Remove(moveItem);
                items.Insert((MoveItemIndex < i) ? i - 1 : i, moveItem);
                return;
            }
        }
        int last = AssociatedObject.Items.Count - 1;
        items.Remove(moveItem);
        items.Add(moveItem);
    }

    //位置からインデックスを取得する
    private int GetItemIndex(Point position)
    {
        var result = VisualTreeHelper.HitTest(AssociatedObject, position);
        if (result != null)
        {
            var visual = result.VisualHit;
            while (visual != null)
            {
                if (visual is ListBoxItem)
                {
                    return AssociatedObject.Items.IndexOf(((ListBoxItem)visual).Content);
                }
                visual = VisualTreeHelper.GetParent(visual);
            }
        }
        return -1;
    }
}

ドラッグ中に表示される半透明のアレはAdornerを追加して実装しました

public class DragAdorner : Adorner
{
    protected UIElement _child;
    private double DragPositionX;
    private double DragPositionY;

    public DragAdorner(UIElement adornedElement, UIElement dragItem, double opacity, Point dragPosition)
        : base(adornedElement)
    {
        var _brush = new VisualBrush(dragItem) { Opacity = opacity };
        var b = VisualTreeHelper.GetDescendantBounds(dragItem);
        var r = new Rectangle() { Width = b.Width, Height = b.Height };

        DragPositionX = dragPosition.X;
        DragPositionY = dragPosition.Y;

        r.Fill = _brush;
        _child = r;
    }


    private double _leftOffset;
    public double LeftOffset
    {
        get { return _leftOffset; }
        set
        {
            _leftOffset = value - DragPositionX;
            UpdatePosition();
        }
    }

    private double _topOffset;
    public double TopOffset
    {
        get { return _topOffset; }
        set
        {
            _topOffset = value - DragPositionY;
            UpdatePosition();
        }
    }

    private void UpdatePosition()
    {
        var adorner = this.Parent as AdornerLayer;
        if (adorner != null)
        {
            adorner.Update(this.AdornedElement);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return _child;
    }

    protected override int VisualChildrenCount
    {
        get { return 1; }
    }

    protected override Size MeasureOverride(Size finalSize)
    {
        _child.Measure(finalSize);
        return _child.DesiredSize;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        _child.Arrange(new Rect(_child.DesiredSize));
        return finalSize;
    }

    public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
    {
        var result = new GeneralTransformGroup();
        result.Children.Add(base.GetDesiredTransform(transform));
        result.Children.Add(new TranslateTransform(LeftOffset, TopOffset));
        return result;
    }
}
public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int X;
        public int Y;
    }

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetCursorPos(out POINT lpPoint);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);

    public static Point GetMousePosition(Visual v)
    {
        POINT p;
        GetCursorPos(out p);
        var source = HwndSource.FromVisual(v) as HwndSource;
        ScreenToClient(source.Handle, ref p);
        return new Point(p.X, p.Y);
    }
}

ItemsSourceにはIList<IDragDropItem>なリストを入れて使います

public class DDItem : IDragDropItem
{
    public DDItem(string text)
    {
        this.Text = text;
    }

    public string Text { get; set; }
}
private ObservableSynchronizedCollection<IDragDropItem> _ListItems;
public ObservableSynchronizedCollection<IDragDropItem> ListItems
{
    get
    { return _ListItems; }
    set
    {
        if (_ListItems == value)
            return;
        _ListItems = value;
        RaisePropertyChanged();
    }
}

public void Initialize()
{
    ListItems = new ObservableSynchronizedCollection<IDragDropItem>();
    ListItems.Add(new DDItem("hogehoge"));
    ListItems.Add(new DDItem("hugahuga"));
    ListItems.Add(new DDItem("foobar"));
}
<ListBox AllowDrop="True" ItemsSource="{Binding ListItems}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Label Content="{Binding Text}" FontSize="24" />
        </DataTemplate>
    </ListBox.ItemTemplate>

    <i:Interaction.Behaviors>
        <behaviors:ListBoxDragDropBehavior />
    </i:Interaction.Behaviors>
</ListBox>