0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【C#】 ListViewのカラム(列)を非表示にする方法

Last updated at Posted at 2024-02-03

ListViewを使用したことがある人なら一度は考えるこのテーマ
簡単な実装から少々面倒な方法まで、備忘録を兼ねて簡潔に書いていこうというお話。

目次

下準備
方法1. カラム幅を0にする
方法2. カラムを削除/復元する
方法3. カラム幅0+α
時間のない人はこちら

下準備

表記は.Net8用ですが、やっていること自体は.Net Frameworkでも問題なく実装できるはず。
まずは、今回実験に使うListViewの設定をさくっと。

4列5行のListViiew作成
ListView listView;
CheckBox checkBox;
public Form1()
{
    InitializeComponent();
    listView = new ListView() { View = View.Details, Location = new(12, 12), Size = new(350, 150) };
    listView.Columns.AddRange(Enumerable.Range(0, 4).Select(i => new ColumnHeader() { Text = $"C {i}" }).ToArray());
    listView.Items.AddRange(Enumerable.Range(0,5).Select(i => new ListViewItem($"Item {i}")).ToArray());
    foreach (ListViewItem item in listView.Items)
        item.SubItems.AddRange(Enumerable.Range(0, 3).Select(i => new ListViewItem.ListViewSubItem(item, $"sub {i}")).ToArray());
    listView.ColumnWidthChanging += ListView_ColumnWidthChanging;
    checkBox = new CheckBox() { Text = "表示/非表示", Location = new(12, 162),Checked = true };
    checkBox.CheckedChanged += CheckBox_CheckedChanged;
    Controls.Add(listView);
    Controls.Add(checkBox);
}
private void ListView_ColumnWidthChanging(object? sender, ColumnWidthChangingEventArgs e)
=> throw new NotImplementedException();
private void CheckBox_CheckedChanged(object? sender, EventArgs e)
=> throw new NotImplementedException();

実行するとこうなる
listview01.png
ColumnWidthChangingはカラム幅が変更される場合に発生するイベント

CheckedChangedはチェック状態が変更される場合に発生するイベント、カラムの表示/非表示を切り替える際に使う
とりあえず今は作っただけ、触れると危険

準備完了、続いて実装方法を以下に記す

方法1. カラム幅を0にする

よく見かける方法、実装もお手軽

CheckBox_CheckedChanged 変更
private void CheckBox_CheckedChanged(object? sender, EventArgs e)
    => listView.Columns[2].Width = checkBox.Checked ? 60 : 0;

チェックボックスにチェックがある場合は3列目(C2)を幅60で表示
チェックがない場合は幅を0にする処理、簡素化のため列や幅は決め打ちで記述中

幅を0にするだけだと幅の変更が可能なので、幅が変更されたときの処理も必要

ListView_ColumnWidthChanging 変更
private void ListView_ColumnWidthChanging(object? sender, ColumnWidthChangingEventArgs e)
{
    if (e.ColumnIndex == 2)
    {
        e.NewWidth = 0;
        e.Cancel = true;
    }
}

3列目(ColumnIndexが2)の幅が変更されそうなとき、幅を0にして処理を終了する
これで最低限の目的は達成できるが、カーソルが下図のように変わるという問題が出る
listview02.png
これの解決方法は「3.カラム幅0+α」に記述

方法2. カラムを削除/復元する

非表示にするのならいっそのこと削除してしまおうという発想
削除した部分を復元するために元の情報を保存しておく必要がある

色付きが追加箇所
ListView listView;
CheckBox checkBox;
+List<ColumnHeader> columns = [];
+List<ListViewItem> items = [];
public Form1()
{
    InitializeComponent();
    listView = new ListView() { View = View.Details, Location = new(12, 12), Size = new(350, 150) };
    listView.Columns.AddRange(Enumerable.Range(0, 4).Select(i => new ColumnHeader() { Text = $"C {i}" }).ToArray());
    listView.Items.AddRange(Enumerable.Range(0,5).Select(i => new ListViewItem($"Item {i}")).ToArray());
    foreach (ListViewItem item in listView.Items)
        item.SubItems.AddRange(Enumerable.Range(0, 3).Select(i => new ListViewItem.ListViewSubItem(item, $"sub {i}")).ToArray());
    listView.ColumnWidthChanging += ListView_ColumnWidthChanging;
    checkBox = new CheckBox() { Text = "表示/非表示", Location = new(12, 162),Checked = true };
    checkBox.CheckedChanged += CheckBox_CheckedChanged;
    Controls.Add(listView);
    Controls.Add(checkBox);
    // コピーの作成、Itemsはディープコピーが必要
+   columns.AddRange(listView.Columns.Cast<ColumnHeader>().ToArray());
+   listView.Items.Cast<ListViewItem>().ToList().ForEach(item => items.Add((ListViewItem)item.Clone()));
}
CheckBox_CheckedChanged 変更
private void CheckBox_CheckedChanged(object? sender, EventArgs e)
{
    if (checkBox.Checked)
    {
        listView.Columns.Clear();
        listView.Items.Clear();
        listView.Columns.AddRange(columns.ToArray());
        listView.Items.AddRange(items.Select(item => (ListViewItem)item.Clone()).ToArray());
    }
    else
    {
        listView.Columns.RemoveAt(2);
        listView.Items.Cast<ListViewItem>().ToList().ForEach(item => item.SubItems.RemoveAt(2));
    }
}

カラムを表示する場合は予め保存しておいたカラムとアイテムで復元
カラムを削除する際は、Itemsより先にColumnsから削除を行う

一応これでも目的のことができるが、非常に無駄が多い

方法3. カラム幅0+α

ここからが本題
「1.カラム幅を0にする」+カーソルアイコンを変化させない方法

アイコンが変わる原因となるWM_SETCURSORメッセージをフックする
手順は、ListViewのHeaderControlをサブクラス化してWM_SETCURSORをフック
カーソルアイコンが変化する場合には処理を止める、ただこれだけ。
HeaderControlについては下記参照、知らなくても問題はない

メッセージを処理するためにHeaderControlを操作するための準備を行う

class HeaderControl : NativeWindow
{
    ListView owner;
    public bool invisible = false;

    public HeaderControl(ListView owner)
    {
        this.owner = owner;
        var handle = SendMessage(owner.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
        this.AssignHandle(handle);
    }
    protected override void WndProc(ref Message m)
    {
        if (invisible && m.Msg == WM_SETCURSOR)
        {
            var info = new HDHITTESTINFO() { pt = owner.PointToClient(Cursor.Position) };
            var res = SendMessage(this.Handle, HDM_HITTEST, IntPtr.Zero, info);
            // info.iItemはヒットしたカラムのインデックス
            if (res >= 0 && info.iItem == 2)
            {
                // 区切り線アイコンの場合は無視
                if ((info.flags & HHT_ONDIVIDER) > 0 || (info.flags & HHT_ONDIVOPEN) > 0)
                {
                    Cursor.Current = Cursors.Default;
                    return;
                }
            }
        }
        base.WndProc(ref m);
    }
    const int
    HDM_HITTEST = 0x1206,
    WM_SETCURSOR = 0x0020,
    LVM_GETHEADER = 0x101F,
    HHT_ONDIVIDER = 0x0004,     // 項目間の区切り線を表すフラグ
    HHT_ONDIVOPEN = 0x0008;     // 幅0の項目の区切り線を表すフラグ
    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [In, Out] HDHITTESTINFO lParam);
    [StructLayout(LayoutKind.Sequential)]
    class HDHITTESTINFO { public Point pt; public int flags; public int iItem; }
}

準備は以上、あとはこのクラスを使用してアイコンを制御するだけ、お粗末様。
コード全体はページ最後に記載 ← 押してみな、飛ぶぞ

上記HeaderControlクラスの説明

コンストラクタ部分
    public HeaderControl(ListView owner)
    {
        this.owner = owner;
        var handle = SendMessage(owner.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
        this.AssignHandle(handle);
    }

ListViewに対してLVM_GETHEADERメッセージを送り、ヘッダコントロールのハンドルを取得、AssignHandleでハンドルを割り当て

ウィンドウプロシージャ
    protected override void WndProc(ref Message m)
    {
        if (invisible && m.Msg == WM_SETCURSOR)
        {
            var info = new HDHITTESTINFO() { pt = owner.PointToClient(Cursor.Position) };
            var res = SendMessage(this.Handle, HDM_HITTEST, IntPtr.Zero, info);
            // info.iItemはヒットしたカラムのインデックス
            if (res >= 0 && info.iItem == 2)
            {
                // 区切り線アイコンの場合は無視
                if ((info.flags & HHT_ONDIVIDER) > 0 || (info.flags & HHT_ONDIVOPEN) > 0)
                {
                    Cursor.Current = Cursors.Default;
                    return;
                }
            }
        }
        base.WndProc(ref m);
    }

invisibleは、Trueの場合隠す、Falseなら表示のフラグ、表示するのなら処理は不要。
WM_SETCURSORメッセージの場合、HDM_HITTESTメッセージを使い、指定したポイントにあるヘッダー項目 (存在する場合) を判断する。

上サイト、戻り値の説明で失敗時に1が返るとあるがそれは誤りで、-1が返る

隠したいカラムの判定
if (res >= 0 && info.iItem == 2)

resはヒットテストの戻り値、ヒットした場合info.iItemと一致する
info.iItemはカーソルが乗っているカラムのインデックス。ヒットテストに成功している場合resと同じ値となる。
なので、if (res == 2)と書いても同じ動作になる。

カーソル判定
if ((info.flags & HHT_ONDIVIDER) > 0 || (info.flags & HHT_ONDIVOPEN) > 0)
{
    Cursor.Current = Cursors.Default;
    return;
}

info.flagsの詳細は下記サイト参照

HHT_ONDIVIDER、HHT_ONDIVOPENは下図カーソルアイコン
listview03.png 左がHHT_ONDIVIDER、右がHHT_ONDIVOPEN
Cursors.Defaultは通常の矢印カーソル
隠しているカラム上でカーソルが区切り線アイコンに代わる場合、デフォルトカーソルに変更して終了という処理

デフォルトに置き換えているのは、左右のカラムからカーソルが来た場合の対処も兼ねているため。置き換えていないと、選択しているカラムがないのに区切り線カーソルになるという下図の様なことが起こる。
listview04.png
C1からC2に侵入、アイコンは区切り線なのに色の変わったカラムが無い

クラスの使用方法は、カラムが作成されたListViewを引数にインスタンス化するだけ。

invisibleの値を変化させると、カーソルアイコンが変化するかどうか切り替えられる
HeaderControlクラスで行っているのはカーソルアイコンの制御だけなので別途、ListView_ColumnWidthChangingを処理して枠のサイズ変更を適宜無効にする必要がある
この処理は方法1と同様なので割愛

以下に埋め込んだマジックナンバーを定数として書き直したコード全体を記す
必要に応じて定数宣言を変数化すれば柔軟に非表示カラム処理が行えるはず。以上。

方法3 のソースコード全体

ListView listView;
CheckBox checkBox;
HeaderControl header;
// 非表示にするカラムインデックス
const int hiddenColumn = 2;
public Form1()
{
    InitializeComponent();
    listView = new ListView() { View = View.Details, Location = new(12, 12), Size = new(350, 150) };
    listView.Columns.AddRange(Enumerable.Range(0, 4).Select(i => new ColumnHeader() { Text = $"C {i}" }).ToArray());
    listView.Items.AddRange(Enumerable.Range(0, 5).Select(i => new ListViewItem($"Item {i}")).ToArray());
    foreach (ListViewItem item in listView.Items)
        item.SubItems.AddRange(Enumerable.Range(0, 3).Select(i => new ListViewItem.ListViewSubItem(item, $"sub {i}")).ToArray());
    listView.ColumnWidthChanging += ListView_ColumnWidthChanging;
    checkBox = new CheckBox() { Text = "表示/非表示", Location = new(12, 162), Checked = true };
    checkBox.CheckedChanged += CheckBox_CheckedChanged;

    Controls.Add(listView);
    Controls.Add(checkBox);

    // ヘッダコントロールのインスタンス化
    header = new HeaderControl(listView);
}

private void ListView_ColumnWidthChanging(object? sender, ColumnWidthChangingEventArgs e)
{
    if (e.ColumnIndex == hiddenColumn)
    {
        e.NewWidth = 0;
        e.Cancel = true;
    }
}

private void CheckBox_CheckedChanged(object? sender, EventArgs e)
{
    // 表示状態の切り替え
    header.invisible = !checkBox.Checked;
    listView.Columns[hiddenColumn].Width = checkBox.Checked ? 60 : 0;
}
class HeaderControl : NativeWindow
{
    ListView owner;
    public bool invisible = false;

    public HeaderControl(ListView owner)
    {
        this.owner = owner;
        var handle = SendMessage(owner.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
        this.AssignHandle(handle);
    }
    protected override void WndProc(ref Message m)
    {
        if (invisible && m.Msg == WM_SETCURSOR)
        {
            var info = new HDHITTESTINFO() { pt = owner.PointToClient(Cursor.Position) };
            var res = SendMessage(this.Handle, HDM_HITTEST, IntPtr.Zero, info);
            if (res >= 0 && info.iItem == hiddenColumn)
            {
                // 区切り線アイコンの場合は無視
                if ((info.flags & HHT_ONDIVIDER) > 0 || (info.flags & HHT_ONDIVOPEN) > 0)
                {
                    Cursor.Current = Cursors.Default;
                    return;
                }
            }
        }

        base.WndProc(ref m);
    }
    const int
    HDM_HITTEST = 0x1206,
    WM_SETCURSOR = 0x0020,
    LVM_GETHEADER = 0x101F,
    HHT_ONDIVIDER = 0x0004,
    HHT_ONDIVOPEN = 0x0008;
    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [In, Out] HDHITTESTINFO lParam);
    [StructLayout(LayoutKind.Sequential)]
    class HDHITTESTINFO { public Point pt; public int flags; public int iItem; }
}

目次に戻る

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?