C#でListBoxの描画処理を変更したい場合、当然ながらOwnerDrawを使います。
DrawMode = DrawMode.OwnerDrawFixed;
これで
protected override void OnDrawItem(DrawItemPaintArgs e) {}
を定義すれば、項目は自由に描画できます。
背景描画も変更したい場合、ウィンドウプロシージャをオーバーライドして、
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x14) //0x14:WM_ERASEBKGND
{
PaintBackground();//任意の描画処理
return;
}
base.WndProc(ref m);
}
とすれば良いです。
ただしこれだと、項目が0個の時に正しく背景が描画されません。どんな処理を書いても、必ずBackColorの単色塗りつぶしになってしまいます。
メッセージを追っていくと、どうやら項目が0個の時にはWM_ERASEBKGND
が呼ばれないみたいです。
ListBoxは内部的に複数のコントロールから出来ているようで、項目が0個でも、ウィンドウを移動しているとたまにWM_ERASEBKGND
が呼ばれたり、よく分からない挙動をします。
WM_PAINT
は再描画時に必ず呼ばれるので、このときに背景描画処理をしてしまえば良さそうです。
しかし、単にWM_PAINT
時に描画しても、その後に上から単色塗りつぶしが行われて、期待した結果は得られません。
WM_PAINT
時に背景描画を行ってからreturn;
してしまうと、描画は上手くいくのですが、無限に再描画が発生してCPUを食い尽くしてしまいます。これは、無効領域が有効化されないためです。
解決
要するに、WinAPIで直接描画するのと同じように、BeginPaintをやって、EndPaintで無効領域を有効化してしまえば良いわけです。
[StructLayout(LayoutKind.Sequential)]
unsafe public struct PAINTSTRUCT
{
public IntPtr hdc;
public bool fErase;
public RECT rcPaint;
public bool fRestore;
public bool fIncUpdate;
public fixed byte rgbReserved[32];
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
public static extern IntPtr BeginPaint(IntPtr hWnd, out PAINTSTRUCT ps);
[DllImport("user32.dll")]
public static extern IntPtr EndPaint(IntPtr hWnd, ref PAINTSTRUCT ps);
protected override void WndProc(ref Message m)
{
if (m.Msg == 0xf && Items.Count == 0) //0xf:WM_PAINT
{
PAINTSTRUCT ps;
BeginPaint(Handle, out ps);
PaintBackground(); //任意の描画処理
EndPaint(Handle, ref ps);
return;
}
base.WndProc(ref m);
}
注意
注意点としては、PAINTSTRUCT構造体が固定長の配列を持つため、unsafeブロックで囲み、fixed
キーワードで配列を静的確保しています(この記述は古いC#ではだめみたいです)。
当然、ビルドオプションで「unsafeコードを許可する」にチェックを入れておいてください。
この手法を使う副作用として、VistualStudioのフォームデザイナが使えなくなります。unsafeコードを使うと、例外を吐いてだめになるみたいです。
しかたないので、これらのコードを全部#if
で囲み、条件付きコンパイルで普段は無効にしておき、リリース時に有効にすることで回避できます。これらのコードを無効にしても、背景が単色になるだけで、開発には支障がないはずです。
ここまでやるとC#で書く意味があるのか疑問になりますが、どうしてもリストボックスの背景を変更したいときの参考になれば幸いです。