プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

WPFのListBoxでマウスドラッグ中の要素選択を無効にする方法

確認時の.NET Frameworkのバージョン: 4.6.00081
参考元: Disable ListBox Dragging Selection in Single SelectionMode?

2015/11/03追記: ListBox Class の派生である ListView Class の場合でも、同様の方法で対策可能です。

現象

ListBox.SelectionMode Property がSingleまたはExtendedである場合、マウスドラッグ中にマウスカーソル位置を動かすとSelectedItemプロパティ値が変化する機能があります。
.NET4.6の現状では、このドラッグ中選択機能の有効無効を制御するプロパティは存在しません。
ListBoxItemの独自ドラッグアンドドロップ処理(要素表示順の変更など、以下独自D&D)を行う場合は、ドラッグ中選択機能が邪魔になる場合があります:

  1. 要素の端の方をMouseLeftButtonDownして選択
  2. そのままMouseMoveして、独自D&Dを行おうとする
  3. マウスカーソルの移動距離が足りず独自D&DのDoDragDropが呼び出される前に、マウスカーソルが別の要素上に移動する
  4. ドラッグ中選択機能により選択要素が変化する、これにより要素の表示が変化する(標準ControlTemplateではListBoxItemの背景がハイライトされます)
  5. その後マウスカーソルが十分な距離を移動した後に、独自D&D処理が開始される(以降はD&Dが終了するまではDrag系イベントが発生し、Mouse系イベントは発生しなくなります)
  6. Q. この時DragDropの対象になる要素はMouseLeftButtonDownした要素か、現在選択されている要素か? A. どちらにしろユーザーは混乱するでしょう

解決策

ReferenceSourceを追ってみると、 ListBoxItem.OnMouseEnter から呼ばれる、internalな ListBox.NotifyListItemMouseDragged が元凶のようです。
そのあたりの条件式を見ると、ListBoxのマウスフォーカスを解除すればドラッグ中選択機能を無効にできそうです。
というわけで、対策するコードは次の内容になります:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

public class MyListBox : ListBox
{
    protected override DependencyObject GetContainerForItemOverride()
        => new MyListBoxItem();
    protected override bool IsItemItsOwnContainerOverride(object item)
        => item is MyListBoxItem;
}
public class MyListBoxItem : ListBoxItem
{
    protected override void OnMouseEnter(MouseEventArgs e)
    {
        var parent = ItemsControl.ItemsControlFromItemContainer(this);
        if (parent.IsMouseCaptured)
        {
            parent.ReleaseMouseCapture();
        }

        base.OnMouseEnter(e);
    }
}

この自作ListBoxを使用すると、MouseLeftButtonDown時には選択要素が変わりますが、その後のMouseMoveでは選択要素が変化しないことを確認できます。

なお参考元では、上のコードで行っているOnMouseEnter中ではなく、OnMouseMove中にてマウスキャプチャを解放しています。
しかしそれでは、WPFのイベント発生順序が MouseEnter → MouseMove であるらしいので、MouseLeftButtonDown後の最初の移動時に選択要素が切り替わってしまいます。
(WPFでのマウスイベントの発生順序の説明がMSDNに見つかりませんでした。WinFormsでは Control.MouseMove Event などに記述が見つかったのですが。)