【C#】 ListView に RowHeight プロパティを追加する

今回は、継承したListViewに行の高さを取得や設定するための、RowHeightプロパティを持たせようというお話。

行の高さの設定方法は、下記事に書いてあるので今回は行の高さの取得方法を考える。

https://qiita.com/HALXene/items/2d2f20bb63501c210c3e

今回もテストは.Net8で行っているが、多少表記が違うだけで.Net Frameworkでも実装できると思う。

# 目次
[ListViewItem.Boundsを利用する](#itemsboundsを利用する)
[ListViewItemがない場合](#listviewitemがない場合)
[時間のない人はこちら](#ソースコード)

## ListViewItem.Boundsを利用する

[前述の記事](https://qiita.com/HALXene/items/2d2f20bb63501c210c3e) の追記にあるように、`ListViewItem.Bounds`を利用すると、項目に外接する四角形を取得できる。そこから`Height`の値を取得すれば行の高さが取得できそうだ。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.listviewitem.bounds?view=windowsdesktop-8.0

上記説明にもある通り、これは`listView.GetItemRect(Index)`と同値であり、さらに言えば`SendMessage(LVM_GETITEMRECT, index, ref rect)`を実行しているに他ならない。

https://learn.microsoft.com/ja-jp/windows/win32/controls/lvm-getitemrect

つまり、`ListViewItem.Bounds`を利用したい場合、`項目のIndex`が必要となる。
では、項目数が0の場合、つまり`ListView.Items.count`が`0`の時どうすればいいのか。

## ListViewItemがない場合
恐らく最も簡単な方法は、適当な項目を追加して、測って、削除する、だと思う。

注意点は、`VirtualMode`を使用している場合、安易に追加すると`InvalidOperationException`が発生することだろうか。ただ、`VirtualMode`の場合は項目を追加する必要は無く、`VirtualListSize`を弄るだけで取得することができる。

以上を踏まえて、行の高さを取得するメソッドを作る
```C#
int getRowHeight()
{
    var height = 0;
    // 項目があるなら普通に取得
    if (listView.Items.Count > 0)
        height = listView.GetItemRect(0).Height;
    // 仮想モードの場合、VirtualListSizeを使い計測
    else if(listView.VirtualMode)
    {
        listView.VirtualListSize = 1;
        height = listView.GetItemRect(0).Height;
        listView.VirtualListSize = 0;
    }
    // 項目無しかつ仮想モードでもない場合、項目追加で計測
    else
    {
        listView.Items.Add(new ListViewItem());
        height = listView.GetItemRect(0).Height;
        listView.Items.Clear();
    }
    // 高さは外接する四角形よりも1ピクセル小さい
    return height - 1;
}
```
細かいことを書くと、`listView.Columns`が設定されていない場合、上記メソッドは`-1`を返す。**カラムが未設定の場合でも高さを取得したい場合は、項目と同様一度カラムを追加して計測、その後削除を行う必要がある。**

今回の内容と、[前述の記事](https://qiita.com/HALXene/items/2d2f20bb63501c210c3e)の内容を合わせて、RowHeight プロパティを持った継承ListViewのコードを下記に記して、今回の話は終了、お粗末様。

## ソースコード
```C#
partial class ListViewEx : ListView
{
    int rowHeight;
    public int RowHeight
    {
        get
        {
            if (Items.Count > 0)
                rowHeight = GetItemRect(0).Height;
            else if (VirtualMode)
            {
                VirtualListSize = 1;
                rowHeight = GetItemRect(0).Height;
                VirtualListSize = 0;
            }
            else
            {
                Items.Add(new ListViewItem());
                rowHeight = GetItemRect(0).Height;
                Items.Clear();
            }
            return rowHeight - 1;
        }
        set
        {
            // 0以下には設定しない
            if(value < 1) return;
            
            // WM_MEASUREITEMで使われるrowHeightはBounds.Heightと同値にする
            rowHeight = value + 1;

            var size = Size;
            var style = GetWindowLong(Handle, GWL_STYLE);
            style |= LVS_OWNERDRAWFIXED;
            SetWindowLong(Handle, GWL_STYLE, style);
            Size = new Size(size.Width, size.Height + 1);
            style ^= LVS_OWNERDRAWFIXED;
            SetWindowLong(Handle, GWL_STYLE, style);
            Size = size;
        }
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (WM_MEASUREITEM == m.Msg)
        {
            Marshal.WriteInt32(m.LParam + (sizeof(uint) * 4), rowHeight);
            m.Result = 1;
        }
    }
    const int
        GWL_STYLE = -16,
        LVS_OWNERDRAWFIXED = 0x0400,
        WM_MEASUREITEM = 0x002C + 0x2000;

    [DllImport("user32.dll")]
    static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
}
```
以下使用方法
```C#
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        var listView = new ListViewEx()
        {
            View = View.Details,
            GridLines = true,
            Location = new(12, 12),
            Size = new(350, 150)
        };
        // カラムだけ用意
        listView.Columns.Add(new ColumnHeader() { Text = "test01" });
        Controls.Add(listView);

        // サイズを取得する場合、これはデモなので値は廃棄('_')している
        _ = listView.RowHeight;
        // サイズ変更の場合、RowHeightに代入すると行の高さが変わる
        listView.RowHeight = 10;
    }
}
```


[目次に戻る](#目次)