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-05

ListViewの一行の高さを変更する方法は色々あるので、備忘録を兼ねて纏めてみるという話。
ViewがDetailsの場合を想定して話してます。

実行環境は.Net8ですが、やっていること自体は.Net Frameworkでも問題なく実装できるはず。多分。

目次

方法1. SmallImageListを使用する
方法2. StateImageListを利用する
方法3. Fontを利用する
方法4. WM_MEASUREITEMメッセージを利用する
方法5. WM_MEASUREITEMメッセージを利用する改
時間のない人はこちら

方法1. SmallImageListを使用する

行の高さを変更する方法を検索すると、大抵この方法がヒットする。
最も簡潔に書ける実装方法

listView.SmallImageList = new ImageList() { ImageSize = new Size(1, 30) };

のように書くだけで、行の高さが30ピクセルに変更できる。
当然のことながらSmallImageListは本来の目的では使用できなくなる。

方法2. StateImageListを利用する

方法1とほぼ同じことをStateImageListを利用して行うことができる。

listView.StateImageList = new ImageList() { ImageSize = new Size(1, 30) };

コードもほぼ同じで、結果も同じ。
ただしこちらの方法の場合、チェックボックスが利用できなくなる。
listView.CheckBoxes=trueの場合でも、チェックボックスが表示されない)
しかし、チェックボックスを使用しない場合は気兼ねなく使え、さらにSmallImageListと併用できる。

併用した場合
listView.StateImageList = new ImageList() { ImageSize = new Size(1, 50) };
listView.SmallImageList = new ImageList() { ImageSize = new Size(79, 30) };
listView.SmallImageList.Images.Add(new Bitmap("logo.png"));
listView.Items.Add(new ListViewItem("test"));                  // イメージ無し
listView.Items.Add(new ListViewItem("test") { ImageIndex = 0});// イメージ有り

listview12.png
ロゴが大きかったので行の高さは50にしている。
SmallImageListの表示位置を調節する場合は、OwnerDrawを行う必要がある(と思う)。

方法3. Fontを利用する

フォントサイズの変更でも行の高さが変わる。

listView.Font = new Font(listView.Font.FontFamily, 30);

listview13.png
ついでにカラムの高さも変わる。色がついている部分がカラム。
サイズを30と指定しても行の高さが30ピクセルとなるわけではない。文字のサイズが30emになる。

カラムの高さは変わってほしくないので小細工を行う

// カラムのハンドラを取得して
var handle = SendMessage(listView.Handle, 0x101F/*LVM_GETHEADER*/, IntPtr.Zero, IntPtr.Zero);
// カラムのフォントはサイズが元のままの9em
var font = new Font(listView.Font.FontFamily, listView.Font.Size);
// ListViewはフォントサイズ20em
listView.Font = new Font(listView.Font.FontFamily, 20);
// カラムのフォントを再設定
SendMessage(handle, 0x0030/*WM_SETFONT*/, font, 0);

listview14.png
カラムの高さは元に戻ったが、なぜかフォントがおかしなことに。

問題点

以上の方法だと、どれも高さを広げることはできても、既定のサイズ(環境にもよるが大体18ピクセル、.Net Frameworkの場合は15ピクセル)以下に縮めることができない。
さらにImageListを使用する場合、ImageSizeの最大値は256ピクセルという制限もある。

方法4. WM_MEASUREITEMメッセージを利用する

規定のサイズよりも高さを小さくしたい、逆に256ピクセルよりも大きな行を作りたい衝動に駆られた場合どうするのか。
それが今回のお話の肝となるWM_MEASUREITEMメッセージを利用する方法である。
WM_MEASUREITEMメッセージについては下記参照。

このメッセージで送られるMEASUREITEMSTRUCT構造体のitemHeightの値を変更すれば目的の結果が得られる。
MEASUREITEMSTRUCT構造体については下記参照

問題はどうすればWM_MEASUREITEMメッセージが発生するかという点。

WM_MEASUREITEMメッセージの説明文には

コントロールまたはメニューの作成時に、リストビューコントロールの所有者ウィンドウに送信されます。

さらに、注釈には

システムは 、WM_INITDIALOG メッセージを送信する前に、OWNERDRAWFIXED スタイルで作成されたウィンドウに WM_MEASUREITEM メッセージを送信します

とある、つまり作成時にOWNERDRAWFIXEDスタイルを持ったListViewを作る必要があるというわけだが、どのみちWM_MEASUREITEMメッセージの処理のために、ListViewをサブクラス化するのでちょうどいい。以上を踏まえてListViewを継承したListViewExを作る。

partial class ListViewEx : ListView
{
    // 行の高さ
    int rowHeight = 10;
    
    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        
        if(WM_MEASUREITEM == m.Msg)
        {
            // 詳しくは後述
            Marshal.WriteInt32(m.LParam + (sizeof(uint) * 4), rowHeight);
            // 処理したのでTrueを設定
            m.Result = 1;
        }
        else if(WM_DRAWITEM == m.Msg)
        { /* 描画処理が必要 */ }
    }

    protected override CreateParams CreateParams
    {
        get
        {
            var cp = base.CreateParams;
            cp.Style |= LVS_OWNERDRAWFIXED;
            return cp;
        }
    }
    
    const int LVS_OWNERDRAWFIXED = 0x0400;
    const int WM_REFLECT = 0x2000;
    const int WM_MEASUREITEM = 0x002C + WM_REFLECT;
    const int WM_DRAWITEM = 0x002B + WM_REFLECT;
}

少し説明を加えると

Marshal.WriteInt32(m.LParam + (sizeof(uint) * 4), rowHeight);

この部分、WM_MEASUREITEMメッセージのLParamにはMEASUREITEMSTRUCT構造体へのポインタが入っている。rowHeightは行の高さ、今回は10ピクセルに挑戦。
MEASUREITEMSTRUCT構造体は下記サイト参照。

構造体の中身は、目的のitemHeightに至るまでuint型が4つ並んでいるので、itemHeightのアドレスに対してMarshal.WriteInt32で値を直接書き換えるというのが先のコード。
もちろん構造体を定義して書き換えてもいい、下記がその方法

構造体を使用する場合のコード、当該箇所のみ。
if(WM_MEASUREITEM == m.Msg)
{
    var measure = Marshal.PtrToStructure<MEASUREITEMSTRUCT>(m.LParam);
    measure.itemHeight = rowHeight;
    Marshal.StructureToPtr<MEASUREITEMSTRUCT>(measure, m.LParam, false);
    m.Result = 1;
}
// 中略
[StructLayout(LayoutKind.Sequential)]
struct MEASUREITEMSTRUCT
{
    public int CtlType;
    public int CtlID;
    public int itemID;
    public int itemWidth;
    public int itemHeight;
    public IntPtr itemData;
}

続いてCreateParamsプロパティについて
LVS_OWNERDRAWFIXEDCreateParamsは下記サイト参照、スタイルにLVS_OWNERDRAWFIXEDを追加している。

LVS_OWNERDRAWFIXEDの説明によると、もれなくWM_DRAWITEM メッセージを処理する必要があると書かれている。今回はとりあえず行の高さが変更できることを確認したいだけなので、何も処理を描いていない。何も処理していないので文字や諸々のものは表示されない。

listview15.png
実行すると見事に行の高さが10ピクセルのListViewが完成した。めでたしめでたし。

方法5. WM_MEASUREITEMメッセージを利用する改

ここからが本題。おそらく世界初の方法(適当調べ)

方法4の場合、行の高さの変更はできたが、WM_DRAWITEMを処理する必要が出てきた。
どうにかして行の高さだけを変更し、WM_DRAWITEMはシステムに任せる方法はないだろうか。もちろん方法はある。

以下に、この美味しいとこ取りの概略を記す。

  1. ウィンドウスタイルにLVS_OWNERDRAWFIXEDを追加する
  2. WM_MEASUREITEMメッセージを送信させる
  3. ウィンドウスタイルからLVS_OWNERDRAWFIXEDを削除する

問題は、WM_MEASUREITEMメッセージをどうすれば送信してもらえるかという点だが、話は簡単。このメッセージ、システムがコントロールの寸法を知りたいときに送ってくるので、ListViewの寸法を変えてやればいい。つまりListViewのサイズを変更すれば送られてくるというわけだ。

加えて言うと、ListViewのOwnerDraw=trueLVS_OWNERDRAWFIXEDは別物なので、OwnerDraw利用時でもウィンドウスタイルにLVS_OWNERDRAWFIXEDがない場合はWM_MEASUREITEMメッセージは発生しない。

上記を実現するためのコードは下記

// ListViewのサイズを保存しておく
var size = listView.Size;
// 現在のウィンドウスタイルを取得しLVS_OWNERDRAWFIXEDを追加
var style = GetWindowLong(listView.Handle, GWL_STYLE);
style |= LVS_OWNERDRAWFIXED;
// スタイルの適用
SetWindowLong(listView.Handle, GWL_STYLE, style);
// WM_MEASUREITEMを送信させるためにListViewのサイズを少し変更
listView.Size = new Size(size.Width, size.Height + 1);
// ウィンドウスタイルからLVS_OWNERDRAWFIXEDを削除
style ^= LVS_OWNERDRAWFIXED;
// スタイルの適用
SetWindowLong(listView.Handle, GWL_STYLE, style);
// 変更した分を元に戻す、ここではWM_MEASUREITEMが発生しない
listView.Size = size;

const int
    GWL_STYLE = -16,
    LVS_OWNERDRAWFIXED = 0x0400;

[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

コードの詳細はコメントにある通り。
WM_MEASUREITEMを発生させるためにListViewのサイズを変更するのがポイント。
変更した分はすぐに元に戻すので見た目の変化は何もない。
LVS_OWNERDRAWFIXEDもすぐに削除しているので以降の描画はシステムが行ってくれる。

以上で任意のタイミングで行の高さを好きに変更することができるはず。

実行すると
listview16.png
描画も行われており大満足。

以下に方法5のソース全体を記しておく

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

public partial class Form1 : Form
{
    ListViewEx listView;
    public Form1()
    {
        InitializeComponent();
        listView = new ListViewEx()
        {
            View = View.Details,
            GridLines = true,
            Location = new(12, 12),
            Size = new(450, 350)
        };
        listView.Columns.Add(new ColumnHeader() { Text = "test01" });
        listView.Items.Add(new ListViewItem("test"));
        listView.Items.Add(new ListViewItem("test"));
        Controls.Add(listView);

        // サイズ変更、実行場所はどこでもいい
        listView.RowHeightChange(10);
    }
    // ボタンで実行してもいい
    //private void button1_Click(object sender, EventArgs e)
    //=> listView.RowHeightChange(30);
}
partial class ListViewEx : ListView
{
    int rowHeight;

    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;
        }
    }
    public void RowHeightChange(int height)
    {
        rowHeight = height;

        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;
    }

    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);
}

行の高さ変更部分をメソッド化してあるので、好きな場所でRowHeightChange(目的の高さ)を実行すると、意図した結果が得られる。現在設定されている高さを返す予定なら、プロパティ化の方が良かった気もするが、項目(Items)が空の時の、高さの初期値の取得方法が思いつかないので、とりあえず今回の話はここまで、お粗末様。

追記

listView.SmallImageList = new ImageList() { ImageSize = new Size(1, 30) };

上記のようにSmallImageList等で30と指定した場合、ListViewItem.Bounds.Height31となるが、
WM_MEASUREITEM30と指定した場合は、ListViewItem.Bounds.Height30となる。
数値のズレが気になる場合はrowHeightに+1した値を設定すればいい。

追記その2

プロパティでの実装の詳細は下記を参照してください。

目次に戻る

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?