Search code examples
c#winformstreeview

I cannot change Node.BackColor on TreeView nodes with TreeViewDrawMode.OwnerDrawText c#


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.


Solution

  • 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.