My form has a TreeView control named tv1
which uses TreeViewDrawMode.OwnerDrawText
and implements a custom handler. It works as expected with the exception that the nodes which are passed to the system by using e.DrawDefault = true
will only allow me to change the node ForeColor but not the node BackColor. If I disable the handler by changing the DrawMode to Normal then the BackColor of the nodes will change as expected. Example code:
using System.Drawing;
namespace TreeViewEx
{
public partial class Form1 : Form
{
private bool DoColor = false;
private List<TreeNode> ColoredNodes = new();
private Color myColor = SystemColors.HighlightText;
private Color myBackColor = Color.Black;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// tv is the TreeView control on Form1
tv.HideSelection = false;
tv.DrawMode = TreeViewDrawMode.OwnerDrawText;
tv.DrawNode += tv_DrawNode!;
}
private static Color GetContrastColor(Color color)
{
return (color.R * 0.299M) + (color.G * 0.587M) + (color.B * 0.114M) > 149 ?
SystemColors.WindowText :
SystemColors.Window;
}
private void ColorAncestors()
{
foreach (var anode in ColoredNodes)
{
anode.ForeColor = SystemColors.WindowText;
anode.BackColor = SystemColors.Window;
}
ColoredNodes.Clear();
if (!DoColor) { return; }
var node = tv.SelectedNode;
while (node.Parent != null)
{
node = node.Parent;
node.ForeColor = myColor;
node.BackColor = myBackColor;
ColoredNodes.Add(node);
}
}
private void tv_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Node == null) return;
var selected = (e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected;
var unfocused = !e.Node.TreeView.Focused;
if (selected && unfocused)
{
using var font = e.Node.NodeFont ?? e.Node.TreeView.Font;
e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
TextRenderer.DrawText(e.Graphics, e.Node.Text, font, e.Bounds,
SystemColors.HighlightText, TextFormatFlags.GlyphOverhangPadding);
}
else
{
e.DrawDefault = true;
}
}
// cbOwnerDraw is a checkbox on Form1
private void cbOwnerDraw_CheckedChanged(object sender, EventArgs e)
{
tv.DrawMode = cbOwnerDraw.Checked ?
TreeViewDrawMode.OwnerDrawText : TreeViewDrawMode.Normal;
}
}
}
For brevity some code segments have been left out here, such as setting DoColor = true
etc.
I have looked all over for any explanation for this but am coming up empty. My expectation is that any nodes not explicitly drawn in the event handler (by passing to the system via e.DrawDefault = true
) would be drawn with whatever attributes I assign to them. What happens is the BackColor never changes. I expect there is a simple answer but I am stumped.
The base renderer doesn't use the TreeNode.BackColor
when you request a default draw through the e.DrawDefault
property. Unlike the normal-state foreground color (unselected node) and font, the TreeNode.BackColor
is ignored and theTreeView.BackColor
is used instead to fill the label.
You can see that in this relevant source code snippet from the TreeView.cs
.
DrawTreeNodeEventArgs e = new DrawTreeNodeEventArgs(g, node, bounds, (TreeNodeStates)(nmtvcd->nmcd.uItemState));
OnDrawNode(e);
if (e.DrawDefault)
{
//Simulate default text drawing here
TreeNodeStates curState = e.State;
Font font = node.NodeFont ?? node.TreeView.Font;
Color color = (((curState & TreeNodeStates.Selected) == TreeNodeStates.Selected) && node.TreeView.Focused) ? SystemColors.HighlightText : (node.ForeColor != Color.Empty) ? node.ForeColor : node.TreeView.ForeColor;
// Draw the actual node.
if ((curState & TreeNodeStates.Selected) == TreeNodeStates.Selected)
{
g.FillRectangle(SystemBrushes.Highlight, bounds);
ControlPaint.DrawFocusRectangle(g, bounds, color, SystemColors.Highlight);
TextRenderer.DrawText(g, node.Text, font, bounds, color, TextFormatFlags.Default);
}
else
{
using var brush = BackColor.GetCachedSolidBrushScope(); // <- Notice
g.FillRectangle(brush, bounds);
TextRenderer.DrawText(g, node.Text, font, bounds, color, TextFormatFlags.Default);
}
}
So, in your case, there's no use for e.DrawDefault
and you need to draw all the labels.
private void tv_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Node is null) return;
if (e.Node.IsEditing) return;
TreeNode node = e.Node;
bool selected = (e.State & TreeNodeStates.Selected) != 0;
bool focused = (e.State & TreeNodeStates.Focused) != 0;
bool highlight = selected && (focused || !node.TreeView.HideSelection);
Font font = node.NodeFont ?? node.TreeView.Font;
Color foreColor = highlight
? SystemColors.HighlightText
: node.ForeColor.IsEmpty
? node.TreeView.ForeColor
: node.ForeColor;
Color backColor = highlight
? SystemColors.Highlight
: node.BackColor.IsEmpty
? node.TreeView.BackColor
: node.BackColor;
Rectangle bounds = e.Bounds;
if (e.Node.TreeView.CheckBoxes)
{
bounds.Inflate(-1, 0);
}
using var brBack = new SolidBrush(backColor);
e.Graphics.FillRectangle(brBack, bounds);
TextRenderer.DrawText(
e.Graphics,
e.Node.Text,
font,
bounds,
foreColor,
TextFormatFlags.Left |
TextFormatFlags.VerticalCenter |
TextFormatFlags.SingleLine);
if (highlight && node.TreeView.Focused)
{
bounds.Width--;
bounds.Height--;
ControlPaint.DrawFocusRectangle(e.Graphics, bounds);
}
}
Set the TreeView.HideSelection
property to false
to fill the selected label with the SystemColors.Highlight
color even when the control is not focused.
Side note, you shouldn't do this using var font = e.Node.NodeFont ?? e.Node.TreeView.Font;
. This will destroy at the end of the scope a font you didn't create. Remove the using
keyword and let the source clean up in time.