1
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でOwnerDrawのすゝめ

Last updated at Posted at 2024-02-04

OwnerDrawを使いこなす為の入り口になるかもしれない話。

目次

1. 下準備
2. 注意事項
3. 描画順序と範囲
4. 背景描画
5. 文字描画

下準備

至極まっとうな5行4列のListView、OwnerDrawを行うのでもちろんOwnerDraw = trueが必要

public partial class Form1 : Form
{
    ListView listView;
    public Form1()
    {
        InitializeComponent();
        listView = new ListView() { View = View.Details, OwnerDraw = true, GridLines = true, 
                                    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.DrawColumnHeader += ListView_DrawColumnHeader;
        listView.DrawItem += ListView_DrawItem;
        listView.DrawSubItem += ListView_DrawSubItem;
        Controls.Add(listView);
    }
    private void ListView_DrawColumnHeader(object? sender, DrawListViewColumnHeaderEventArgs e)
    => e.DrawDefault = true;
    private void ListView_DrawItem(object? sender, DrawListViewItemEventArgs e)
    => e.DrawDefault = true;
    private void ListView_DrawSubItem(object? sender, DrawListViewSubItemEventArgs e)
    => e.DrawDefault = true;
}

生まれたての状態、描画はシステム任せのe.DrawDefault = true
listview05.png
実行すると何の変哲もないListViewの完成

注意事項

まったく知識のない状態でいきなり注意点を書くのもどうかと思うが、頭の片隅に置いておくと助かることもあるので一応書いておく、読み飛ばしても問題はない

ListViewのFullRowSelectプロパティがTrueの時、かつDrawItem部分で背景描画の処理を行っている場合、初回のマウスホバー時にSubItemが背景色で塗りつぶされる現象が起こる。

マウスポインターが行上に移動すると、詳細ビューで行ごとに1回、DrawSubItemイベントを伴わずにDrawItemイベントが発生し、DrawSubItemイベントハンドラーで描画されたものがカスタム背景によって塗りつぶされるという既知のバグの為である

詳細は以下

通常DrawItemで背景を、DrawSubItemで文字などを描画するのでFullRowSelectを使用する場合は対策が必要となる。

いろいろ試してみた結果、ListView.HideSelection プロパティがTrueの時、
マウスポインターが行上に移動するだけでDrawItemが呼ばれるが、DrawSubItemは呼ばれない。
一方、HideSelectionがFalseの時、DrawListViewSubItemEventArgsのe.Stateの内容は正しくない値になるが、描画が必要な時はe.Stateに0以外の値が入るので、e.Stateが0の時を無視すれば思った通りの動作を行う。気がする
HideSelectionの規定値はNET Framework 4.8以降はFalse。

listview06.png
起動後、Item0から順番にカーソルを通過させている途中の図、GridLineが塗りつぶされているのが分かる。このような現象に陥った時に、上記のバグのことが頭の片隅にあると多少解決が早まるかもしれない。

順序と範囲

DrawColumnHeaderはカラムの数だけ呼ばれる。
DrawItemDrawSubItemは、DrawItemが呼ばれ、その後、属するSubItem数分DrawSubItemが呼ばれる。
今回利用する5行4列のListViewの場合

DrawColumnHeader index : 0
DrawColumnHeader index : 1
DrawColumnHeader index : 2
DrawColumnHeader index : 3
DrawItem index : 0
DrawSubItem ItemIndex : 0 ColumnIndex : 0
DrawSubItem ItemIndex : 0 ColumnIndex : 1
DrawSubItem ItemIndex : 0 ColumnIndex : 2
DrawSubItem ItemIndex : 0 ColumnIndex : 3
DrawItem index : 1
DrawSubItem ItemIndex : 1 ColumnIndex : 0
DrawSubItem ItemIndex : 1 ColumnIndex : 1
DrawSubItem ItemIndex : 1 ColumnIndex : 2
DrawSubItem ItemIndex : 1 ColumnIndex : 3
# 以下略

上記の順序で描画が為される、GridLineはDrawSubItemの後に引かれる。
起動時のログなのでDrawColumnHeaderが呼ばれているが、必ずしもDrawItemの前に呼ばれるわけではない。が、DrawItemの後にはDrawSubItemが必ず呼ばれる。注意事項のバグを除いてだが。

今回はDrawColumnHeaderには触れず、花形のDrawItemDrawSubItemについて書く。

描画するListViewItemの情報はDrawListViewItemEventArgsDrawListViewSubItemEventArgsにより提供される

DrawItemのDrawListViewItemEventArgs.Boundsには描画するListViewItemのサイズと位置をが記されている。
例として2行目のBoundsは、{X=0,Y=43,Width=240,Height=19}このような値。
それに従い2行目だけに枠線を引いてみると下図のようなことになる
listview07.png
一部GridLineと被る部分がある。枠線が1ピクセル幅なので、1ピクセルずつずれたような矩形となる。

DrawSubItemのDrawListViewSubItemEventArgs.Boundsには描画するListViewSubItemのサイズと位置をが記されている。
例として2行1列目のBoundsは、{X=0,Y=43,Width=60,Height=19}このような値である。
枠線を引くと下図のようになる
listview08.png
2行2列目のBoundsは、{X=60,Y=43,Width=60,Height=19}
listview09.png
なんと両端がGridLineの餌食に。

Boundsは上記のような感じ、当然サイズと位置が入っているだけなのでそれに従う必要は全くない。DrawItemですべてを描画してもいいし、DrawSubItemの好きな位置ですべてを描いてもいい。が、通常はDrawItemで背景、DrawSubItemで文字を描くと収まりがいい、自分もそうしている。

背景描画

ここからが本題、まずはDrawItem内で背景を描いてみる。
DrawListViewItemEventArgs.Graphicsに対して好きに描ける。

矩形、多角形、楕円、円弧、画像もテキストもなんでもOK。
コントロールを描画する場合はRendererを利用する。フォーカス枠が描きたい場合はControlPaint.DrawFocusRectangleなんてのもある。もちろんフォーカス枠をDrawRectangleで描いても良い。

DrawItem と DrawSubItem 変更
private void ListView_DrawItem(object? sender, DrawListViewItemEventArgs e)
{
    var location = new Point(e.Bounds.X + 4, e.Bounds.Y + 2);
    // チェックボックスを描画
    CheckBoxRenderer.DrawCheckBox(e.Graphics, location, CheckBoxState.CheckedNormal);
    e.Graphics.FillRectangle(Brushes.GreenYellow, 20, e.Bounds.Y + 4, 10, 10);
    e.Graphics.DrawRectangle(Pens.Red, 34, e.Bounds.Y + 5, 8, 8);
    // 背景なので横に長くてもいいし、何ならBoundsをはみ出しても問題はない
    e.Graphics.FillEllipse(Brushes.SkyBlue, 46, e.Bounds.Y + 2, 200, 15);
}
private void ListView_DrawSubItem(object? sender, DrawListViewSubItemEventArgs e)
{ /* DrawSubItemでは今は何も行わない */ }

適当に図形を配置した上記コードの実行結果が下図
listview10.png
e.Graphicsをキャンバスに好きに描けることが確認できる。
ちなみに、上図のチェックボックスはあくまでも画像なのでクリックしても変化は起こらない。

文字描画

基本的には背景と同様、e.Graphicsに好きに描くといい。

文字を描く方法は何通りかある。
e.DrawTextを使用すると、位置や色が指定できない。
e.Graphics.DrawStringを使用すると、それなりに描ける。
TextRenderer.DrawTextを使用すると、ListViewと同様の描画ができる。

DrawSubItemで文字を描く
private void ListView_DrawSubItem(object? sender, DrawListViewSubItemEventArgs e)
{
    if (e.ItemIndex == 0)
        e.DrawText();
    else if (e.ItemIndex == 1)
        e.Graphics.DrawString(e.SubItem.Text, Font, Brushes.DarkRed, e.Bounds.Location);
    else if (e.ItemIndex == 2)
        TextRenderer.DrawText(e.Graphics, e.SubItem.Text, Font, e.Bounds.Location, Color.Crimson);
}

listview11.png
上から順に、e.DrawTexte.Graphics.DrawStringTextRenderer.DrawText
e.Graphics.DrawStringTextRenderer.DrawTextの違いは数字を描くと表れる。

さらに、背景の上に掛かれるので描画位置は背景を考慮する必要がある。上図は全く考慮せずに描画したので重なりあい大変なことになっている。
チェックボックスが表示されているならその分ずらす必要があるし、カラム幅が文字の全長よりも狭い場合はその処理も必要。こだわりだすときりがない。

ここで唐突に、知っておくと何かの助けになる事柄を一つ
画像でもテキストでも、描画してみて何か違うなと思ったら、別の方法をとると解決することがある。例えばSmallImageListの画像を表示する場合、e.Graphics.DrawImageで描くと上手くいかないことでも、ListView.SmallImageList.Drawを使うとListViewと同様の画像が描画できる。

あてもなく綴ってきたが、以上がOwnerDrawの基礎の基礎であり全てだと思う。
アイデア次第で無限の可能性が広がるOwnerDrawの世界。
ここでの話がその入り口にたどり着く手掛かりにでもなれば幸いに思う。

1
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
1
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?