【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

ListBoxのダブルクリック

n7shi2009-06-13


ListBoxのアイテム上でクリックしてもMouseLeftButtonDownイベントが起きません。これはListBoxItemの中でイベントが片付けられてしまうためです。WPFの場合はPreviewMouseLeftButtonDownで回避できますが、Silverlightにはありません。そのためListBoxItemの中から強制的に検出してみました。今まではXAMLを使った方法が紹介されていましたが、これはコードだけで実装する方法です。

これを応用すればダブルクリックが検出できるようになります。⇒ 動作確認とソース

クリック

ListBox.Items.Add()が呼ばれてListBoxItemが作成される際にMouseLeftButtonDownに委譲を追加します。

ListBoxItemの作成時にPrepareContainerForItemOverrideが呼ばれるためこれをoverrideします。しかしPrepareContainerForItemOverrideが呼ばれた時点ではListBoxItemに内部構造が構築されていないため、委譲を追加することができません。遅延処理されるイベントを発生させて(InvalidateArrange)、後で処理(ArrangeOverride)が呼ばれた際にキャッシュしておいたListBoxItemに委譲を追加します。

これによって途中で遮断されることなくPreviewMouseLeftButtonDownが呼ばれるようになります。

public class PreviewListBox : ListBox
{
    public event MouseButtonEventHandler PreviewMouseLeftButtonDown;

    protected virtual void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        if (PreviewMouseLeftButtonDown != null && !e.Handled)
            PreviewMouseLeftButtonDown(this, e);
    }

    private void previewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        OnPreviewMouseLeftButtonDown(e);
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        previewMouseLeftButtonDown(this, e);
        if (!e.Handled) base.OnMouseLeftButtonDown(e);
    }

    private List<DependencyObject> preps = new List<DependencyObject>();
    private bool invalidated = true;

    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
        preps.Add(element);
        if (!invalidated)
        {
            InvalidateArrange();
            invalidated = true;
        }
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (var lbi in preps)
        {
            var dobj = VisualTreeHelper.GetChild(lbi, 0) as UIElement;
            dobj.MouseLeftButtonDown += previewMouseLeftButtonDown;
        }
        preps.Clear();
        invalidated = false;
        return base.ArrangeOverride(finalSize);
    }
}

ダブルクリック

クリックが検出できれば、間隔を計測することでダブルクリックを検出できるようになります。Windowsのデフォルトが0.5秒のため500を指定しています。

lastを1秒ずらしているのはダブルクリックとして判断されないように間隔を空けているからです。

public class DoubleClickListBox : PreviewListBox
{
    public event MouseButtonEventHandler DoubleClick;

    protected virtual void OnDoubleClick(MouseButtonEventArgs e)
    {
        if (DoubleClick != null && !e.Handled)
            DoubleClick(this, e);
    }

    private DateTime last = DateTime.Now - TimeSpan.FromSeconds(1);

    protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnPreviewMouseLeftButtonDown(e);
        if (e.Handled) return;
        var prev = last;
        last = DateTime.Now;
        if ((last - prev).TotalMilliseconds < 500)
        {
            last -= TimeSpan.FromSeconds(1);
            OnDoubleClick(e);
        }
    }
}

ダブルクリックを検出するだけで半日も費やしてしまいました。標準でサポートされていないのはWebではダブルクリックが使われないためらしいですが、せっかくのRIAなのでローカルアプリケーションに操作方法を近付けたくなります。