winformsnotifyicon

Winforms ToolStripItem has a gray background behind its image unless I set its BackColor to anything


I am trying to add items to my NotifyIcon context menu like this:

notifyIcon = new()
{
    Icon = Resources.NotifyIcon,
    ContextMenuStrip = new()
    {
        Items = { { "Exit", Resources.Exit, Exit } }
    },
    Visible = true
};

For some reason, the image will always have a very subtle gray background. This happens even if I set the image to null.

However, if I set its BackColor to anything, even if to its original value, the gray background disappears:

notifyIcon = new()
{
    Icon = Resources.NotifyIcon,
    ContextMenuStrip = new()
    {
        Items = { { "Exit", Resources.Exit, Exit } }
    },
    Visible = true
};
Debug.WriteLine(notifyIcon.ContextMenuStrip.Items[0].BackColor.ToArgb());
notifyIcon.ContextMenuStrip.Items[0].BackColor = Color.FromArgb(255, 240, 240, 240);
Debug.WriteLine(notifyIcon.ContextMenuStrip.Items[0].BackColor.ToArgb());

The Debug.WriteLine calls are there to verify that it is still the same color before and after I set the color.

Although this fixes the problem, it feels very hacky. What is the root cause of this problem, and is there a better way to solve this?


Solution

  • The renderer does not fill the background of the ToolStripMenuItem if the value of its BackColor property equals its Owner.BackColor or the Control.DefaultBackColor value which is the SystemColors.Control color.

    You can see that in the BackColor property declaration in the ToolStripItem base class:

    /// <summary>
    ///  The BackColor of the item
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [SRDescription(nameof(SR.ToolStripItemBackColorDescr))]
    public virtual Color BackColor
    {
        get
        {
            Color c = RawBackColor;
            if (!c.IsEmpty)
            {
                return c;
            }
    
            Control? parent = ParentInternal;
            if (parent is not null)
            {
                return parent.BackColor;
            }
    
            return Control.DefaultBackColor;
        }
        set
        {
            Color c = BackColor;
            if (!value.IsEmpty || Properties.ContainsObject(s_backColorProperty))
            {
                Properties.SetColor(s_backColorProperty, value);
            }
    
            if (!c.Equals(BackColor))
            {
                OnBackColorChanged(EventArgs.Empty);
            }
        }
    }
    
    /// <summary>
    ///  Returns the value of the backColor field,
    ///  no asking the parent with its color is, etc.
    /// </summary>
    internal Color RawBackColor => Properties.GetColor(s_backColorProperty);
    

    The RawBackColor internal property in the getter returns Color.Empty from the properties store unless a different color is explicitly specified. Yes, the ARGB values of the SystemBrushes.Control and Color.FromArgb(255, 240, 240, 240) are equal. However, their Color structures are not based on the IEquatable<Color> interface implementation and == operator overload.

    public readonly struct Color : IEquatable<Color>
    {
        // ...
    
        public static bool operator ==(Color left, Color right) =>
        left.value == right.value
            && left.state == right.state
            && left.knownColor == right.knownColor
            && left.name == right.name;
    
        public static bool operator !=(Color left, Color right) => !(left == right);
    
        public override bool Equals([NotNullWhen(true)] object? obj) 
            => obj is Color other && Equals(other);
    
        public bool Equals(Color other) => this == other;
    
        public override int GetHashCode()
        {
            if (name != null && !IsKnownColor)
                return name.GetHashCode();
    
            return HashCode.Combine(
                value.GetHashCode(), 
                state.GetHashCode(), 
                knownColor.GetHashCode());
        }
    }
    

    All variables shown must be equal to get true otherwise false. One of the differences between the mentioned colors is the name. While the SystemBrushes.Control.Name returns Control, the Color.FromArgb(255, 240, 240, 240).Name returns the fff0f0f0 hex value.

    Back to the renderer. The renderer calls the OnRenderMenuItemBackground to fill the background of the top-level and drop-down menu items. You can see that part in the ToolStripProfessionalRenderer. Note how and when the unselected item is handled.

    protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
    {
        // omitted ...
    
        if (item.IsOnDropDown)
        {
            ScaleObjectSizesIfNeeded(item.DeviceDpi);
    
            bounds = LayoutUtils.DeflateRect(bounds, _scaledDropDownMenuItemPaintPadding);
    
            if (item.Selected)
            {
                // omitted ...
            }
            else
            {
                Rectangle fillRect = bounds;
    
                if (item.BackgroundImage is not null)
                {
                    ControlPaint.DrawBackgroundImage(
                        g,
                        item.BackgroundImage,
                        item.BackColor,
                        item.BackgroundImageLayout,
                        bounds,
                        fillRect);
                }
                else if (item.Owner is not null && item.BackColor != item.Owner.BackColor)
                {
                    using var brush = item.BackColor.GetCachedSolidBrushScope();
                    g.FillRectangle(brush, fillRect);
                }
            }
        }
        else
        {
            // omitted ...
        }
    }