.net System.Windows.Forms.ListViewのカラムヘッダー右クリックに対応する

タイトルの通りのことをしようとしたら嵌ったのでメモ。



ダメなこと

  • ColumnClickは左クリックのみ
  • MouseDown/MouseClickはデータ領域のみ

解決策

  • ContextMenuStripを追加してListViewに設定する(メニュー出す気がなくても)
  • contextMenuStripのOpeningイベントで右クリックを処理する
    この時メニュー出さないならCancelしたらよさげ
  • どこでクリックしたかは、HitTestさんが結構がんばってくれる(でも補助が必要)

実際のOpeningイベント処理

下記コードでは、ListViewはlistViewMain、ContextMenuStripはcontextMenuStripListViewになっとりやす。
Log関数は適当に作っといて。

        private void contextMenuStripListView_Opening(object sender, System.ComponentModel.CancelEventArgs e)
        {
            var pos = listViewMain.PointToClient(Cursor.Position);
            var top = listViewMain.TopItem.Bounds.Top;
            if (pos.Y < top) pos.Y = top - 1;
            var lvhti = listViewMain.HitTest(pos);
            if (lvhti.Item != null && lvhti.SubItem != null)
            {
                if (pos.Y < top)
                {
                    var si = listViewMain.Items[lvhti.Item.Index].SubItems.IndexOf(lvhti.SubItem);
                    Log(string.Format("contextMenuStripListView_Opening Header {0} {1} {2} {3}", pos.X, pos.Y, si, listViewMain.Columns[si].Text));
                }
                else
                {
                    Log(string.Format("contextMenuStripListView_Opening {0} {1} {2} {3}", pos.X, pos.Y, lvhti.Item.Index, lvhti.SubItem.Text));
                }
            }
            else
            {
                Log(string.Format("contextMenuStripListView_Opening Blank {0} {1}", pos.X, pos.Y));
            }
            e.Cancel = true;
        }
注意点

ListViewのHitTestには細かい注意点がある。

  • ヘッダーの下の方でクリックすると、(画面上)TopのアイテムにHitする
  • ヘッダーの上の方でクリックすると、何もHitしない
    データ行の高さ分より高いとダメそう
    なので、一件目より上(ようはヘッダー部)をクリックした場合は、一件目のすぐ上をクリックしたことにする
  • SubItemがないと、Hitできない。すなわち一件目のSubItemがないとヘッダーのどこかわからない

そんなわけで、未設定のSubItemがある場合、上のコードではダメ。


もうちょっとベタな調査が必要。

        private void contextMenuStripListView_Opening(object sender, System.ComponentModel.CancelEventArgs e)
        {
            var pos = listViewMain.PointToClient(Cursor.Position);
            var top = listViewMain.TopItem.Bounds.Top;
            if (pos.Y < top) pos.Y = top - 1;
            var lvhti = listViewMain.HitTest(pos);
            int sc = listViewMain.Columns.Count, si = sc;
            if (lvhti.Item != null && lvhti.SubItem != null)
            {
                si = listViewMain.Items[lvhti.Item.Index].SubItems.IndexOf(lvhti.SubItem);
            }
            else
            {
                var x = pos.X;
                for (si = 0; si < sc; si++)
                {
                    x -= listViewMain.Columns[si].Width;
                    if (x < 0)
                    {
                        break;
                    }
                }
            }
            if (lvhti.Item != null && si < sc)
            {
                if (pos.Y < top)
                {
                    Log(string.Format("contextMenuStripListView_Opening Header {0} {1} {2} {3}", pos.X, pos.Y, si, listViewMain.Columns[si].Text));
                }
                else
                {
                    Log(string.Format("contextMenuStripListView_Opening Data {0} {1} {2} {3}", pos.X, pos.Y, lvhti.Item.Index, si));
                }
            }
            else
            {
                Log(string.Format("contextMenuStripListView_Opening Blank {0} {1}", pos.X, pos.Y));
            }
            e.Cancel = true;
        }

ループするしか思いつかなかった、、、。
この方法だと、Data行にSubItemがない場合もBlankにならないで済みますです。
ループがダサいねぇ。
ヘッダーの幅が固定な場合は、もうちょっと上手くできそう。