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?
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 ...
}
}