ListBoxの全項目を高速に選択する

ここでは、C#とWindows Forms(.NET Framework 4)でListBoxの全て選択をやってみようと思います。

未選択状態全選択状態

はじめる前に

自分で実際に作って使っているツールを例にしています。
そのため、ここではlbLogという名前を付けたリストボックスを使っています。

想定する項目数は「とにかくたくさん」です。
うっかり数えるの忘れたまま削除してましたが、例に使ったものではたぶん数百から数千件はあったような気がします。

当然ですが、リストボックスは複数選択ができるようにしておきます。

手っ取り早く結論だけ知りたいという方はこちらへどうぞ。

素朴な実装

最初に書いていたのは、教科書通りの素朴な実装。
SelectAllなんて便利なメソッドはないので、ひとつひとつ丁寧に。

for (int i = 0; i < lbLog.Items.Count; i++)
{
    lbLog.SetSelected(i, true);
}

なるほどこれなら間違いない。

メニューから全て選択を選ぶ

それではスタート!

始まったぞ!

ドゥルルルルルルル…(スクロールバーに注目しよう)

まだ続くぞ!

ドゥルルルル…(スクロールバーに注目だ)

まだまだ続くぞ!

ルルルル…(スクロール…)

全選択完了!

上手に全選択できましたー!

ヾ( `Д´)ノ おせーよ!

この方法は、項目が多くなると遅いのです。

ListBox.SelectedIndexCollection.Addを使ってみる

SetSelectedメソッドを使うから駄目なんじゃないかという仮説のもと、異なる方法でひとつひとつ選択してみることにします。

for (int i = 0; i < lbLog.Items.Count; i++)
{
    lbLog.SelectedIndices.Add(i);
}

これでどうだ!

始まったぞ!

ドゥルルルルルルル…

まだ続くぞ!

ドゥルルルル…

まだまだ続くぞ!

ルルルル…(使い回しです)

全選択完了!

上手に全選択できましたー!

ヾ( `Д´)ノ やっぱりおせーよ!

何も変わりませんでした。

自動レイアウトを切ってしまう

レイアウトなしで全選択してから最後に一気に表示でいいんじゃないか?

lbLog.SuspendLayout();
for (int i = 0; i < lbLog.Items.Count; i++)
{
    lbLog.SetSelected(i, true);
}
lbLog.ResumeLayout();
lbLog.PerformLayout();

さあどうなるか。

始まっ(略

ドゥルル(略

ヾ(ry

そうだよね。レイアウトと選択は別だよね。

いっそのこと非表示にしてしまう

途中過程を表示するからいけないのです。
少々不格好ですが処理中は不可視にしてしまいましょう。

lbPos.Hide();
for (int i = 0; i < lbLog.Items.Count; i++)
{
    lbLog.SetSelected(i, true);
}
lbPos.Show();

どきどき…

見えない

しーん。

見えない

しーん。

見えない

しーん。

全選択完了!

上手に全選択できましたー!

ヾ( `Д´)ノ 不格好なだけじゃねーか!

結局遅いことには変わりありませんでした。

手動操作を再現する

逆に考えるんだ。「手動でやっちゃえばいいさ」と考えるんだ。

  1. 一番上の項目をクリックします。
  2. Shiftを押しながら一番下の項目をクリックします。

実際やってみると、実に軽快な反応で、PCは高性能だったことを思い出させてくれます。
こいつを再現すればいいんじゃないか。

一番上をクリックはHomeキーで代用可能です。
一番下をクリックはEndキーで代用可能です。
.NETにはキーストロークを送信するSendKeysという便利なクラスが存在します。

この方針で書き上げたのが、以下の一文です。

SendKeys.SendWait("{HOME}+{END}");

では試してみます。

全選択完了!

上手に全選択できましたー!

ヾ(´ワ`)シ

ドゥルルルとか言っている暇もないくらい高速です。
途中過程なんかキャプチャしている暇もありません。
これでこそ全選択というべき速度です。

もちろん、SendKeysはフォーカスのあるコントロールに対して働くので、基本的には事前にFocusなどでフォーカスを移しておく必要があるでしょう。
別ウインドウが選択されていてあらぬところが全選択されたとなったら目も当てられません。

しかしここまでやるともうWindowsの機能にべったり依存しすぎているような…。
.NET Frameworkという仮想環境の中であえてここまでするなら、いっそのことWindows APIに頼っちゃえばいいんじゃないか。
ということで…

LB_SETSELメッセージを送る

ListBoxで項目を選択するにはSendMessage関数でLB_SETSELメッセージを送ります。

Windows APIを使うことになるので、まず宣言が必要になります。

[DllImport("User32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
private const int LB_SETSEL = 0x185;

そして実際の処理。

SendMessage(lbLog.Handle, LB_SETSEL, 1, -1);

一応解説しておきますと、LB_SETSELを使うときは、

と、いう意味になります。

ただ、この方法にはひとつ致命的な欠陥がありまして…。

ヾ( `Д´)ノ SelectedItemsが変わってねーよ!

ええ、Windows APIで、.NET Frameworkを介さず「直接」操作したため、.NET Framework側でその変化を把握できていないのです。

この変化に気付かせるため、.NET Frameworkを介してちょこっとだけいじってやります。

lbLog.SetSelected(0, true);

はい。最初の素朴な方法にちょっとだけ帰ってきました。
帰ってきでも別に感慨深くありませんね。これでちゃんと動きます。

余談ですが、SetSelectedメソッドのindexに-1を渡しても例外が出るだけで、全て選択にはなりません。

結論

宣言。

[DllImport("User32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
private const int LB_SETSEL = 0x185;

処理。

SendMessage(lbLog.Handle, LB_SETSEL, 1, -1);
lbLog.SetSelected(0, true);