OSによってコントロールを描画してもらうのではなく、
コードを書いて、独自に描画する方法になります。
ListViewで独自に描画しようとしたところ、
Microsoft公式APIドキュメントに記載されているコードが
正常に動作しなかったため、備忘録として記載いたします。
Microsoft 公式APIドキュメント ListView.OwnerDraw
正しい表示
◯負の値の文字が赤色
◯負の値の背景が黒色(DrawSubItemで標準の背景を描画)
◯選択行の背景が栗色 (DrawItemで描画)
◯上記以外の背景はオレンジ色ー栗色のグラデーション (DrawItemで描画)

正しくない表示
◯負の値の文字が赤色
✕負の値の背景が黒(DrawSubItemで標準の背景を描画)
◯選択行の背景が栗色 (DrawItemで描画)
✕上記以外の背景はオレンジ色ー栗色のグラデーション (DrawItemで描画)
全ての行が選択状態のようです。

確認した環境は、下記になります。
・Windows 11
・Visual Studio 2022
・C#
・Windows Desktop 6 (.NET 6.0 + Windows フォーム)
修正したソースコードは下記になります。
気づいたことをインラインで記載していますので、
参考にしていただければと思います。
ListViewOwnerDraw.cs
コードを書いて、独自に描画する方法になります。
ListViewで独自に描画しようとしたところ、
Microsoft公式APIドキュメントに記載されているコードが
正常に動作しなかったため、備忘録として記載いたします。
Microsoft 公式APIドキュメント ListView.OwnerDraw
正しい表示
◯負の値の文字が赤色
◯負の値の背景が黒色(DrawSubItemで標準の背景を描画)
◯選択行の背景が栗色 (DrawItemで描画)
◯上記以外の背景はオレンジ色ー栗色のグラデーション (DrawItemで描画)

正しくない表示
◯負の値の文字が赤色
✕負の値の背景が黒(DrawSubItemで標準の背景を描画)
◯選択行の背景が栗色 (DrawItemで描画)
✕上記以外の背景はオレンジ色ー栗色のグラデーション (DrawItemで描画)
全ての行が選択状態のようです。

確認した環境は、下記になります。
・Windows 11
・Visual Studio 2022
・C#
・Windows Desktop 6 (.NET 6.0 + Windows フォーム)
修正したソースコードは下記になります。
気づいたことをインラインで記載していますので、
参考にしていただければと思います。
ListViewOwnerDraw.cs
using System.Drawing.Drawing2D;
using System.Globalization;
namespace ListViewOwnerDraw {
public partial class ListViewOwnerDraw: Form {
private ListView listView1 = new ListView();
// ▲▲▲▲ .NET 6.0用に変更 ▲▲▲▲
// .NET 6.0では、
// ContextMenu -> ContextMenuStrip に変更
// private ContextMenu contextMenu1 = new ContextMenu();
private ContextMenuStrip contextMenu1 = new ContextMenuStrip();
// ▼▼▼▼ .NET 6.0用に変更 ▼▼▼▼
public ListViewOwnerDraw() {
// ListViewコントロールを初期化
listView1.BackColor = Color.Black;
listView1.ForeColor = Color.White;
listView1.Dock = DockStyle.Fill;
listView1.View = View.Details;
listView1.FullRowSelect = true;
// ListViewコントロールにカラムを追加
listView1.Columns.Add("Name", 100, HorizontalAlignment.Center);
listView1.Columns.Add("First", 100, HorizontalAlignment.Center);
listView1.Columns.Add("Second", 100, HorizontalAlignment.Center);
listView1.Columns.Add("Third", 100, HorizontalAlignment.Center);
// アイテムを作成し、ListViewコントロールに追加
ListViewItem listViewItem1 = new ListViewItem(
new string[] { "One", "20", "30", "-40" }, -1);
ListViewItem listViewItem2 = new ListViewItem(
new string[] { "Two", "-250", "145", "37" }, -1);
ListViewItem listViewItem3 = new ListViewItem(
new string[] { "Three", "200", "800", "-1,001" }, -1);
ListViewItem listViewItem4 = new ListViewItem(
new string[] { "Four", "not available", "-2", "100" }, -1);
listView1.Items.AddRange(new ListViewItem[] {
listViewItem1, listViewItem2, listViewItem3, listViewItem4 });
// ▲▲▲▲ .NET 6.0用に変更 ▲▲▲▲
// .NET 6.0では、
// ContextMenu -> ContextMenuStrip に変更
// ショートカットメニューを初期化し、
// ListViewコントロールに割り当て
// contextMenu1.MenuItems.Add("List",
contextMenu1.Items.Add("List", null,
new EventHandler(menuItemList_Click));
// contextMenu1.MenuItems.Add("Details",
contextMenu1.Items.Add("Details", null,
new EventHandler(menuItemDetails_Click));
// listView1.ContextMenu = contextMenu1;
listView1.ContextMenuStrip = contextMenu1;
// ▼▼▼▼ .NET 6.0用に変更 ▼▼▼▼
// ListViewコントロールをOwnerDrawに設定し、
// OwnerDrawのハンドラーを追加
listView1.OwnerDraw = true;
listView1.DrawItem +=
new DrawListViewItemEventHandler(listView1_DrawItem);
listView1.DrawSubItem +=
new DrawListViewSubItemEventHandler(listView1_DrawSubItem);
listView1.DrawColumnHeader +=
new DrawListViewColumnHeaderEventHandler(listView1_DrawColumnHeader);
// MouseUpイベントを追加し、
// 項目の幅のどこをクリックしても選択できるようにする
listView1.MouseUp += new MouseEventHandler(listView1_MouseUp);
// ▲▲▲▲ 参考 ▲▲▲▲
// ListViewは、Win32コントロールをラップして作られており、
// そのため、多くのバグが潜んでいます。
// 下記の、余分なDrawItemイベントが送信される現象も、その一つだと思われます。
// ▼▼▼▼ 参考 ▼▼▼▼
// マウスが各行に初めて移動したときに発生する
// 余分なDrawItemイベントを補完する為に、様々なイベントを追加
listView1.MouseMove += new MouseEventHandler(listView1_MouseMove);
listView1.ColumnWidthChanged +=
new ColumnWidthChangedEventHandler(listView1_ColumnWidthChanged);
listView1.Invalidated += new InvalidateEventHandler(listView1_Validated);
// フォームを初期化し、ListViewコントロールを追加
this.ClientSize = new Size(450, 150);
this.FormBorderStyle = FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Text = "ListView OwnerDraw Example";
this.Controls.Add(listView1);
}
// 使用中のリソースをクリーンアップ
protected override void Dispose(bool disposing) {
if (disposing) {
contextMenu1.Dispose();
}
base.Dispose(disposing);
}
[STAThread]
static void Main() {
// ▲▲▲▲ .NET 6.0用に変更 ▲▲▲▲
// .NET 6.0では、
// 初期化時に実行するメソッドが変更されております。
// Application.EnableVisualStyles();
ApplicationConfiguration.Initialize();
// ApplicationConfiguration.Initialize()内では、
// Application.EnableVisualStyles()
// Application.SetCompatibleTextRenderingDefault(false)
// Application.SetHighDpiMode(HighDpiMode.SystemAware)
// が呼ばれております。
// ▼▼▼▼ .NET 6.0用に変更 ▼▼▼▼
Application.Run(new ListViewOwnerDraw());
}
// ListViewコントロールをList表示に設定
private void menuItemList_Click(Object sender, EventArgs e) {
listView1.View = View.List;
listView1.Invalidate();
}
// ListViewコントロールを詳細表示に設定
private void menuItemDetails_Click(Object sender, EventArgs e) {
listView1.View = View.Details;
// 各アイテムのTagをリセットして、
// MouseMoveイベントハンドラーで、回避策を再度有効にする
foreach (ListViewItem item in listView1.Items) {
item.Tag = null;
}
}
// 項目のどこかがクリックされると、
// 項目を選択し、フォーカスする
//
// クリックは通常、親アイテムのテキスト錠で無ければならない
private void listView1_MouseUp(Object sender, MouseEventArgs e) {
// ▲▲▲▲ コメント ▲▲▲▲
// 親アイテム外でも選択できるように、
// X軸は考慮せず(X=5)、Y座標だけでアイテムを特定
// ▼▼▼▼ コメント ▼▼▼▼
ListViewItem clickedItem = listView1.GetItemAt(5, e.Y);
if (clickedItem != null) {
clickedItem.Selected = true;
clickedItem.Focused = true;
}
}
// ListViewのアイテム全体の背景を描画
private void listView1_DrawItem(Object sender, DrawListViewItemEventArgs e) {
// ▲▲▲▲ 不具合のため、修正 ▲▲▲▲
// .NET 6.0では、
// e.Stateが常に選択状態になってしまうため、e.Item.Selectedで判断
// もしかしたら、.NET Frameworkの場合では正常に動作するのかもしれない
// if ((e.State & ListViewItemStates.Selected) != 0) {
if (e.Item.Selected) {
// ▼▼▼▼ 不具合のため、修正 ▼▼▼▼
// 選択されたアイテムの、背景とフォーカスの矩形を描画
e.Graphics.FillRectangle(Brushes.Maroon, e.Bounds);
e.DrawFocusRectangle();
} else {
// 選択されていないアイテムの、背景を描画
using (LinearGradientBrush brush = new LinearGradientBrush(
e.Bounds, Color.Orange, Color.Maroon,
LinearGradientMode.Horizontal)) {
e.Graphics.FillRectangle(brush, e.Bounds);
}
}
// 詳細表示以外の表示の場合、アイテムテキストを描画
if (listView1.View != View.Details) {
e.DrawText();
}
}
// コンテンツベースの書式を適用し、サブアイテムのテキストを描画
private void listView1_DrawSubItem(Object sender, DrawListViewSubItemEventArgs e) {
TextFormatFlags flags = TextFormatFlags.Left;
using (StringFormat sf = new StringFormat()) {
// 列のテキスト配置を保持
// Centerまたは、Rightに設定されていない場合、
// デフォルトでLeftに設定
switch (e.Header.TextAlign) {
case HorizontalAlignment.Center:
sf.Alignment = StringAlignment.Center;
flags = TextFormatFlags.HorizontalCenter;
break;
case HorizontalAlignment.Right:
sf.Alignment = StringAlignment.Far;
flags = TextFormatFlags.Right;
break;
}
// 負の値を持つサブアイテムの場合、背景とテキストを描画
double subItemValue;
if (e.ColumnIndex > 0 &&
Double.TryParse(e.SubItem.Text,
NumberStyles.Currency,
NumberFormatInfo.CurrentInfo,
out subItemValue) &&
subItemValue < 0) {
// ▲▲▲▲ 不具合のため、修正 ▲▲▲▲
// .NET 6.0では、
// e.Stateが常に選択状態になってしまうため、e.Item.Selectedで判断
// もしかしたら、.NET Frameworkの場合では正常に動作するのかもしれない
// アイテムが選択されていない場合、標準の背景を描画
// グラデーションより目立たせるため
// if ((e.ItemState & ListViewItemStates.Selected) == 0) {
if (e.Item.Selected == false) {
// ▼▼▼▼ 不具合のため、修正 ▼▼▼▼
// ▲▲▲▲ コメント ▲▲▲▲
// 下記を実行することで、標準の背景を描画 (黒色)
// ▼▼▼▼ コメント ▼▼▼▼
e.DrawBackground();
}
// 赤で強調して、サブアイテムのテキストを描画
e.Graphics.DrawString(e.SubItem.Text,
listView1.Font, Brushes.Red, e.Bounds, sf);
return;
}
// 負でない値または、数値でない値を持つサブアイテムの場合、
// テキストを通常描画
e.DrawText(flags);
}
}
// 列ヘッダーを描画
private void listView1_DrawColumnHeader(
Object sender, DrawListViewColumnHeaderEventArgs e) {
using (StringFormat sf = new StringFormat()) {
// 列のテキスト配置を保持
// Centerまたは、Rightに設定されていない場合、
// デフォルトでLeftに設定
switch (e.Header.TextAlign) {
case HorizontalAlignment.Center:
sf.Alignment = StringAlignment.Center;
break;
case HorizontalAlignment.Right:
sf.Alignment = StringAlignment.Far;
break;
}
// ヘッダーの背景を、通常描画
e.DrawBackground();
// ヘッダーのテキストを描画
using (Font headerFont =
new Font("Helvetica", 10, FontStyle.Bold)) {
e.Graphics.DrawString(e.Header.Text, headerFont,
Brushes.Black, e.Bounds, sf);
}
}
return;
}
// マウスが初めて移動したとき、各行がそれ事態を再描画するように強制
// ラップされたWin32コントロールによって送信される
// 余分なDrawItemイベントを補正
// この問題は、ListViewが無効化されるたびに発生
private void listView1_MouseMove(Object sender, MouseEventArgs e) {
ListViewItem item = listView1.GetItemAt(e.X, e.Y);
if (item != null && item.Tag == null) {
listView1.Invalidate(item.Bounds);
item.Tag = "tagged";
// ▲▲▲▲ 参考 ▲▲▲▲
// Tagを設定することにより、Invalidate呼び出しによる再描画が、
// 何度も発生しないようにしている
// ▼▼▼▼ 参考 ▼▼▼▼
}
}
// アイテムのTagをリセット
private void listView1_Validated(Object sender, EventArgs e) {
foreach (ListViewItem item in listView1.Items) {
if (item == null)
return;
item.Tag = null;
// ▲▲▲▲ 参考 ▲▲▲▲
// 再描画が終わったら、Tagをリセットしている
// ▼▼▼▼ 参考 ▼▼▼▼
}
}
// 列の幅が変更された場合、
// コントロール全体を強制的に再描画
private void listView1_ColumnWidthChanged(
Object sender, ColumnWidthChangedEventArgs e) {
listView1.Invalidate();
}
}
}
コメント