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});// イメージ有り
ロゴが大きかったので行の高さは50
にしている。
SmallImageListの表示位置を調節する場合は、OwnerDrawを行う必要がある(と思う)。
方法3. Fontを利用する
フォントサイズの変更でも行の高さが変わる。
listView.Font = new Font(listView.Font.FontFamily, 30);
ついでにカラムの高さも変わる。色がついている部分がカラム。
サイズを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);
カラムの高さは元に戻ったが、なぜかフォントがおかしなことに。
問題点
以上の方法だと、どれも高さを広げることはできても、既定のサイズ(環境にもよるが大体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_OWNERDRAWFIXED
とCreateParams
は下記サイト参照、スタイルにLVS_OWNERDRAWFIXED
を追加している。
LVS_OWNERDRAWFIXED
の説明によると、もれなくWM_DRAWITEM
メッセージを処理する必要があると書かれている。今回はとりあえず行の高さが変更できることを確認したいだけなので、何も処理を描いていない。何も処理していないので文字や諸々のものは表示されない。
実行すると見事に行の高さが10ピクセルのListViewが完成した。めでたしめでたし。
方法5. WM_MEASUREITEMメッセージを利用する改
ここからが本題。おそらく世界初の方法(適当調べ)
方法4の場合、行の高さの変更はできたが、WM_DRAWITEM
を処理する必要が出てきた。
どうにかして行の高さだけを変更し、WM_DRAWITEMはシステムに任せる方法はないだろうか。もちろん方法はある。
以下に、この美味しいとこ取りの概略を記す。
- ウィンドウスタイルにLVS_OWNERDRAWFIXEDを追加する
-
WM_MEASUREITEM
メッセージを送信させる - ウィンドウスタイルからLVS_OWNERDRAWFIXEDを削除する
問題は、WM_MEASUREITEM
メッセージをどうすれば送信してもらえるかという点だが、話は簡単。このメッセージ、システムがコントロールの寸法を知りたいときに送ってくるので、ListViewの寸法を変えてやればいい。つまりListViewのサイズを変更すれば送られてくるというわけだ。
加えて言うと、ListViewのOwnerDraw=true
とLVS_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
もすぐに削除しているので以降の描画はシステムが行ってくれる。
以上で任意のタイミングで行の高さを好きに変更することができるはず。
以下に方法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.Height
は31
となるが、
WM_MEASUREITEM
で30
と指定した場合は、ListViewItem.Bounds.Height
は30
となる。
数値のズレが気になる場合はrowHeightに+1した値を設定すればいい。
追記その2
プロパティでの実装の詳細は下記を参照してください。