Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/System.Windows.Forms/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
override System.Windows.Forms.Button.BackgroundImage.set -> void
override System.Windows.Forms.ButtonBase.OnBackColorChanged(System.EventArgs! e) -> void
override System.Windows.Forms.ButtonBase.OnForeColorChanged(System.EventArgs! e) -> void
static System.Windows.Forms.Application.SetColorMode(System.Windows.Forms.SystemColorMode systemColorMode) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ public virtual DialogResult DialogResult
}
}

public override Image? BackgroundImage
{
set
{
base.BackgroundImage = value;

if (Application.IsDarkModeEnabled)
{
// BackgroundImage changes may affect rendering logic,
// so we manually update the OwnerDraw flag to ensure correct visual behavior.
UpdateOwnerDraw();
}
}
}

/// <summary>
/// Defines, whether the control is owner-drawn. Based on this,
/// the UserPaint flags get set, which in turn makes it later
Expand All @@ -161,7 +176,6 @@ private protected override bool OwnerDraw
get
{
if (Application.IsDarkModeEnabled

// The SystemRenderer cannot render images. So, we flip to our
// own DarkMode renderer, if we need to render images, except if...
&& Image is null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,21 +547,6 @@ internal void PaintField(
/// </summary>
internal void PaintImage(PaintEventArgs e, LayoutData layout)
{
if (Application.IsDarkModeEnabled && Control.DarkModeRequestState is true && Control.BackgroundImage is not null)
{
Rectangle bounds = Control.ClientRectangle;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I do not understand is:
When the Button has a border, why would the background "shine through"?

We paint:

  • Background
  • BackgroundImage
  • Border

If we are not deflating the Bounds, aren't we then painting over the Image? What if the Image got important markers in its outer border?

Copy link
Member Author

@LeafShi1 LeafShi1 Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the previous logic, our drawing order was incorrect.

  1. Background color
  2. Border
  3. Background image

This has now been corrected.

Under the original logic

So, under dark mode, the background color size > the border size > the background image size.

bounds.Inflate(-ButtonBorderSize, -ButtonBorderSize);
ControlPaint.DrawBackgroundImage(
e.GraphicsInternal,
Control.BackgroundImage,
Color.Transparent,
Control.BackgroundImageLayout,
Control.ClientRectangle,
bounds,
Control.DisplayRectangle.Location,
Control.RightToLeft);
}

if (Control.Image is not null)
{
// Setup new clip region & draw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ private Color GetButtonBackColor(PushButtonState state)
return textColor;
}

internal void PaintBackgroundImage(PaintEventArgs e, int borderSize, Rectangle paddedBounds)
{
if (Application.IsDarkModeEnabled && Control.DarkModeRequestState is true && Control.BackgroundImage is not null)
{
Rectangle bounds = paddedBounds;
if (Control.FlatStyle == FlatStyle.Popup)
{
bounds.Inflate(-1, -1);
}
else
{
bounds.Inflate(-1 - borderSize, -1 - borderSize);
}

ControlPaint.DrawBackgroundImage(
e.GraphicsInternal,
Control.BackgroundImage,
Color.Transparent,
Control.BackgroundImageLayout,
paddedBounds,
bounds,
bounds.Location,
Control.RightToLeft);
}
}

internal override void PaintUp(PaintEventArgs e, CheckState state)
{
try
Expand All @@ -86,13 +112,18 @@ internal override void PaintUp(PaintEventArgs e, CheckState state)
g,
Control.ClientRectangle,
Control.FlatStyle,
Control.FlatAppearance.BorderSize,
pushButtonState,
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
GetButtonBackColor(pushButtonState),
_ => PaintImage(e, layout),
_ => PaintBackgroundImage(
e,
Control.FlatAppearance.BorderSize,
Control.ClientRectangle),
() => PaintField(
e,
layout,
Expand Down Expand Up @@ -124,13 +155,18 @@ internal override void PaintDown(PaintEventArgs e, CheckState state)
g,
Control.ClientRectangle,
Control.FlatStyle,
Control.FlatAppearance.BorderSize,
PushButtonState.Pressed,
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
GetButtonBackColor(PushButtonState.Pressed),
_ => PaintImage(e, layout),
_ => PaintBackgroundImage(
e,
Control.FlatAppearance.BorderSize,
Control.ClientRectangle),
() => PaintField(
e,
layout,
Expand All @@ -156,19 +192,23 @@ internal override void PaintOver(PaintEventArgs e, CheckState state)
var g = e.GraphicsInternal;
var smoothingMode = g.SmoothingMode;
g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias;

LayoutData layout = CommonLayout().Layout();
ButtonDarkModeRenderer.RenderButton(
g,
Control.ClientRectangle,
Control.FlatStyle,
Control.FlatAppearance.BorderSize,
PushButtonState.Hot,
Control.IsDefault,
Control.Focused,
Control.ShowFocusCues,
Control.Parent?.BackColor ?? Control.BackColor,
GetButtonBackColor(PushButtonState.Hot),
_ => PaintImage(e, layout),
_ => PaintBackgroundImage(
e,
Control.FlatAppearance.BorderSize,
Control.ClientRectangle),
() => PaintField(
e,
layout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ public void RenderButton(
Graphics graphics,
Rectangle bounds,
FlatStyle flatStyle,
int borderSize,
PushButtonState state,
bool isDefault,
bool focused,
bool showFocusCues,
Color parentBackgroundColor,
Color backColor,
Action<Rectangle> paintImage,
Action<Rectangle> paintBackgroundImage,
Action paintField)
{
ArgumentNullException.ThrowIfNull(graphics);
Expand All @@ -61,11 +63,15 @@ public void RenderButton(
height: bounds.Height - padding.Vertical);

// Draw button background and get content bounds
Rectangle contentBounds = DrawButtonBackground(graphics, paddedBounds, state, isDefault, backColor);
Rectangle contentBounds = DrawButtonBackground(graphics, paddedBounds, borderSize, state, isDefault, backColor);

paintBackgroundImage(paddedBounds);

// Paint image and field using the provided delegates
paintImage(contentBounds);

DrawButtonBorder(graphics, bounds, borderSize, state, isDefault);

paintField();

if (focused && showFocusCues)
Expand All @@ -76,7 +82,9 @@ public void RenderButton(
}
}

public abstract Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, Color backColor);
public abstract void DrawButtonBorder(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault);

public abstract Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault, Color backColor);

public abstract void DrawFocusIndicator(Graphics graphics, Rectangle contentBounds, bool isDefault);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,19 @@ namespace System.Windows.Forms;
internal sealed class FlatButtonDarkModeRenderer : ButtonDarkModeRendererBase
{
private const int FocusIndicatorInflate = -3;
private const int CornerRadius = 6;
private static readonly Size s_corner = new(CornerRadius, CornerRadius);

private protected override Padding PaddingCore { get; } = new(0);

public override Rectangle DrawButtonBackground(
Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, Color backColor)
Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault, Color backColor)
{
// fill background
using var back = backColor.GetCachedSolidBrushScope();
graphics.FillRectangle(back, bounds);

// draw border identical to Win32
DrawButtonBorder(graphics, bounds, state, isDefault);
Rectangle rectangle = bounds;
rectangle.Inflate(-1 - borderSize, -1 - borderSize);

graphics.FillRectangle(back, rectangle);
// return inner content area (border + 1 px system padding)
return Rectangle.Inflate(bounds, -3, -3);
}
Expand Down Expand Up @@ -77,33 +75,29 @@ public override Color GetBackgroundColor(PushButtonState state, bool isDefault)
_ => DefaultColors.StandardBackColor
};

private static void DrawButtonBorder(Graphics g, Rectangle bounds, PushButtonState state, bool isDefault)
public override void DrawButtonBorder(Graphics g, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault)
{
g.SmoothingMode = SmoothingMode.AntiAlias;

// Win32 draws its stroke fully *inside* the control → inset by 1 px
Rectangle outer = Rectangle.Inflate(bounds, -1, -1);

DrawSingleBorder(g, outer, GetBorderColor(state));
DrawSingleBorder(g, outer, borderSize, GetBorderColor(state));

// Default button gets a second 1‑px border one pixel further inside
if (isDefault)
{
Rectangle inner = Rectangle.Inflate(outer, -1, -1);
DrawSingleBorder(g, inner, DefaultColors.AcceptFocusIndicatorBackColor);
DrawSingleBorder(g, inner, borderSize, DefaultColors.AcceptFocusIndicatorBackColor);
}
}

private static void DrawSingleBorder(Graphics g, Rectangle rect, Color color)
private static void DrawSingleBorder(Graphics g, Rectangle rect, int borderSize, Color color)
{
g.SmoothingMode = SmoothingMode.AntiAlias;

using var path = new GraphicsPath();
path.AddRoundedRectangle(rect, s_corner);

// a 1‑px stroke, aligned *inside*, is exactly what Win32 draws
using var pen = new Pen(color) { Alignment = PenAlignment.Inset };
g.DrawPath(pen, path);
using var pen = new Pen(color, borderSize) { Alignment = PenAlignment.Inset };
g.DrawRectangle(pen, rect);
}

private static Color GetBorderColor(PushButtonState state) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,27 @@ void RenderButton(
Graphics graphics,
Rectangle bounds,
FlatStyle flatStyle,
int BorderSize,
PushButtonState state,
bool isDefault,
bool focused,
bool showFocusCues,
Color parentBackgroundColor,
Color backColor,
Action<Rectangle> paintImage,
Action<Rectangle> paintBackgroundImage,
Action paintField);

/// <summary>
/// Draws button background with appropriate styling.
/// </summary>
/// <param name="graphics">Graphics context to draw on</param>
/// <param name="bounds">Bounds of the button</param>
/// <param name="borderSize">BorderSize of the button</param>
/// <param name="state">State of the button (normal, hot, pressed, disabled)</param>
/// <param name="isDefault">True if button is the default button</param>
/// <returns>The content bounds (area inside the button for text/image)</returns>
Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, Color backColor);
Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault, Color backColor);

/// <summary>
/// Draws focus indicator appropriate for this style.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ internal class PopupButtonDarkModeRenderer : ButtonDarkModeRendererBase
/// <summary>
/// Draws button background with popup styling, including subtle 3D effect.
/// </summary>
public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, Color backColor)
public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault, Color backColor)
{
// Use padding from ButtonDarkModeRenderer
Padding padding = PaddingCore;
Rectangle paddedBounds = Rectangle.Inflate(bounds, -padding.Left, -padding.Top);
Rectangle paddedBounds = Rectangle.Inflate(bounds, -1 - padding.Left, -1 - padding.Top);

// Content rect will be used to position text and images
Rectangle contentBounds = Rectangle.Inflate(paddedBounds, -padding.Left, -padding.Top);
Expand All @@ -52,9 +52,6 @@ public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle boun
using var brush = backColor.GetCachedSolidBrushScope();
graphics.FillPath(brush, path);

// Draw 3D effect borders
DrawButtonBorder(graphics, paddedBounds, state, isDefault);

// Return content bounds (area inside the button for text/image)
return contentBounds;
}
Expand Down Expand Up @@ -120,7 +117,7 @@ public override Color GetBackgroundColor(PushButtonState state, bool isDefault)
/// <summary>
/// Draws the 3D effect border for the button.
/// </summary>
private static void DrawButtonBorder(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault)
public override void DrawButtonBorder(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault)
{
// Save original smoothing mode to restore later
SmoothingMode originalMode = graphics.SmoothingMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class SystemButtonDarkModeRenderer : ButtonDarkModeRendererBase
/// <summary>
/// Draws button background with system styling (larger rounded corners).
/// </summary>
public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, Color backColor)
public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault, Color backColor)
{
// Shrink for DarkBorderGap and FocusBorderThickness
Rectangle fillBounds = Rectangle.Inflate(bounds, -SystemStylePadding, -SystemStylePadding);
Expand Down Expand Up @@ -108,6 +108,9 @@ public override Color GetBackgroundColor(PushButtonState state, bool isDefault)
_ => DefaultColors.StandardBackColor
};

public override void DrawButtonBorder(Graphics graphics, Rectangle bounds, int borderSize, PushButtonState state, bool isDefault) =>
DrawButtonBorder(graphics, bounds, state, isDefault, false);

/// <summary>
/// Draws the button border based on the current state, using anti-aliasing and an additional inner border.
/// </summary>
Expand Down