|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Windows.Forms.Layout;
namespace System.Windows.Forms.ButtonInternal;
/// <summary>
/// This class is used for more than just <see cref="Button"/>. It is used for things that derive from
/// <see cref="ButtonBase"/>, parts of <see cref="ToolStripItem"/>, and parts of <see cref="DataGridView"/>.
/// </summary>
internal abstract partial class ButtonBaseAdapter
{
// SystemInformation.Border3DSize + 2 pixels for focus rect
protected const int ButtonBorderSize = 4;
internal ButtonBaseAdapter(ButtonBase control) => Control = control.OrThrowIfNull();
protected ButtonBase Control { get; }
/// <summary>
/// Returns a darkened color according to the required color contrast ratio.
/// </summary>
private protected static Color GetContrastingBorderColor(Color buttonBorderShadowColor) => Color.FromArgb(
buttonBorderShadowColor.A,
(int)(buttonBorderShadowColor.R * 0.8f),
(int)(buttonBorderShadowColor.G * 0.8f),
(int)(buttonBorderShadowColor.B * 0.8f));
internal void Paint(PaintEventArgs e)
{
if (Control.MouseIsDown)
{
PaintDown(e, CheckState.Unchecked);
}
else if (Control.MouseIsOver)
{
PaintOver(e, CheckState.Unchecked);
}
else
{
PaintUp(e, CheckState.Unchecked);
}
}
internal virtual Size GetPreferredSizeCore(Size proposedSize)
{
LayoutOptions? options = default;
using (var screen = GdiCache.GetScreenHdc())
using (PaintEventArgs e = new(screen, default))
{
options = Layout(e);
}
return options.GetPreferredSizeCore(proposedSize);
}
protected abstract LayoutOptions Layout(PaintEventArgs e);
internal abstract void PaintUp(PaintEventArgs e, CheckState state);
internal abstract void PaintDown(PaintEventArgs e, CheckState state);
internal abstract void PaintOver(PaintEventArgs e, CheckState state);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool IsHighContrastHighlighted() => SystemInformation.HighContrast
&& Application.RenderWithVisualStyles
&& (Control.Focused || Control.MouseIsOver || (Control.IsDefault && Control.Enabled));
internal static Brush CreateDitherBrush(Color color1, Color color2)
{
using Bitmap bitmap = new(2, 2);
bitmap.SetPixel(0, 0, color1);
bitmap.SetPixel(0, 1, color2);
bitmap.SetPixel(1, 1, color1);
bitmap.SetPixel(1, 0, color2);
return new TextureBrush(bitmap);
}
/// <summary>
/// Get <see cref="StringFormat"/> object for rendering text using GDI+ (<see cref="Graphics"/>).
/// </summary>
internal virtual StringFormat CreateStringFormat()
=> ControlPaint.CreateStringFormat(Control, Control.TextAlign, Control.ShowToolTip, Control.UseMnemonic);
/// <summary>
/// Get <see cref="TextFormatFlags"/> for rendering text using GDI (<see cref="TextRenderer"/>).
/// </summary>
internal virtual TextFormatFlags CreateTextFormatFlags()
=> ControlPaint.CreateTextFormatFlags(Control, Control.TextAlign, Control.ShowToolTip, Control.UseMnemonic);
internal static void DrawDitheredFill(Graphics g, Color color1, Color color2, Rectangle bounds)
{
using Brush brush = CreateDitherBrush(color1, color2);
g.FillRectangle(brush, bounds);
}
protected void Draw3DBorder(IDeviceContext deviceContext, Rectangle bounds, ColorData colors, bool raised)
{
if (Control.BackColor != SystemColors.Control && SystemInformation.HighContrast)
{
if (raised)
{
Draw3DBorderHighContrastRaised(deviceContext, ref bounds, colors);
}
else
{
ControlPaint.DrawBorderSimple(deviceContext, bounds, ControlPaint.Dark(Control.BackColor));
}
}
else
{
if (raised)
{
Draw3DBorderRaised(deviceContext, ref bounds, colors);
}
else
{
Draw3DBorderNormal(deviceContext, ref bounds, colors);
}
}
}
private void Draw3DBorderHighContrastRaised(IDeviceContext deviceContext, ref Rectangle bounds, ColorData colors)
{
bool stockColor = colors.ButtonFace.ToKnownColor() == SystemColors.Control.ToKnownColor();
bool disabledHighContrast = (!Control.Enabled) && SystemInformation.HighContrast;
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
// Draw counter-clock-wise
Point p1 = new(bounds.X + bounds.Width - 1, bounds.Y); // Upper inner right
Point p2 = new(bounds.X, bounds.Y); // Upper left
Point p3 = new(bounds.X, bounds.Y + bounds.Height - 1); // Bottom inner left
Point p4 = new(bounds.X + bounds.Width - 1, bounds.Y + bounds.Height - 1); // Inner bottom right
// Top + left
using CreatePenScope penTopLeft = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlLightLight : colors.Highlight);
hdc.DrawLine(penTopLeft, p1, p2); // Top (right-left)
hdc.DrawLine(penTopLeft, p2, p3); // Left (up-down)
// Bottom + right
using CreatePenScope penBottomRight = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlDarkDark : colors.ButtonShadowDark);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(penBottomRight, p3, p4); // Bottom (left-right)
hdc.DrawLine(penBottomRight, p4, p1); // Right (bottom-up)
// Draw inset using the background color to make the top and left lines thinner
using CreatePenScope insetPen = new(
stockColor
? SystemInformation.HighContrast ? SystemColors.ControlLight : SystemColors.Control
: SystemInformation.HighContrast ? colors.Highlight : colors.ButtonFace);
p1.Offset(-1, 2);
p2.Offset(1, 1);
p3.Offset(1, -1);
p4.Offset(-1, -1);
// Top + left inset
hdc.DrawLine(insetPen, p1, p2); // Top (right-left)
hdc.DrawLine(insetPen, p2, p3); // Left (up-down)
// Bottom + right inset
using CreatePenScope bottomRightInsetPen = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlDark : colors.ButtonShadow);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(bottomRightInsetPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(bottomRightInsetPen, p4, p1); // Right (bottom-up)
}
private static void Draw3DBorderNormal(IDeviceContext deviceContext, ref Rectangle bounds, ColorData colors)
{
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
// Draw counter-clock-wise
Point p1 = new(bounds.X + bounds.Width - 1, bounds.Y); // Upper inner right
Point p2 = new(bounds.X, bounds.Y); // Upper left
Point p3 = new(bounds.X, bounds.Y + bounds.Height - 1); // Bottom inner left
Point p4 = new(bounds.X + bounds.Width - 1, bounds.Y + bounds.Height - 1); // Inner bottom right
// Top + left
using CreatePenScope shadowPen = new(colors.ButtonShadowDark);
hdc.DrawLine(shadowPen, p1, p2); // Top (right-left)
hdc.DrawLine(shadowPen, p2, p3); // Left(up-down)
// Bottom + right
using CreatePenScope highlightPen = new(colors.Highlight);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(highlightPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(highlightPen, p4, p1); // Right (bottom-up)
// Draw inset
using CreatePenScope facePen = new(colors.ButtonFace);
p1.Offset(-1, 2);
p2.Offset(1, 1);
p3.Offset(1, -1);
p4.Offset(-1, -1);
// Top + left inset
hdc.DrawLine(facePen, p1, p2); // Top (right-left)
hdc.DrawLine(facePen, p2, p3); // Left (up-down)
// Bottom + right inset
using CreatePenScope insetPen = new(
colors.ButtonFace.ToKnownColor() == SystemColors.Control.ToKnownColor()
? SystemColors.ControlLight
: colors.ButtonFace);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(insetPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(insetPen, p4, p1); // Right (bottom-up)
}
private void Draw3DBorderRaised(IDeviceContext deviceContext, ref Rectangle bounds, ColorData colors)
{
bool stockColor = colors.ButtonFace.ToKnownColor() == SystemColors.Control.ToKnownColor();
bool disabledHighContrast = (!Control.Enabled) && SystemInformation.HighContrast;
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
// Draw counter-clock-wise.
Point p1 = new(bounds.X + bounds.Width - 1, bounds.Y); // Upper inner right
Point p2 = new(bounds.X, bounds.Y); // Upper left
Point p3 = new(bounds.X, bounds.Y + bounds.Height - 1); // Bottom inner left
Point p4 = new(bounds.X + bounds.Width - 1, bounds.Y + bounds.Height - 1); // Inner bottom right
// Top + left
using CreatePenScope topLeftPen = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlLightLight : colors.Highlight);
hdc.DrawLine(topLeftPen, p1, p2); // Top (right-left)
hdc.DrawLine(topLeftPen, p2, p3); // Left (up-down)
// Bottom + right
using CreatePenScope bottomRightPen = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlDarkDark : colors.ButtonShadowDark);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(bottomRightPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(bottomRightPen, p4, p1); // Right (bottom-up)
// Draw inset - use the back ground color here to have a thinner border.
p1.Offset(-1, 2);
p2.Offset(1, 1);
p3.Offset(1, -1);
p4.Offset(-1, -1);
using CreatePenScope topLeftInsetPen = new(
!stockColor
? colors.ButtonFace
: SystemInformation.HighContrast ? SystemColors.ControlLight : SystemColors.Control);
// Top + left inset
hdc.DrawLine(topLeftInsetPen, p1, p2); // Top (right-left)
hdc.DrawLine(topLeftInsetPen, p2, p3); // Left (up-down)
// Bottom + right inset
using CreatePenScope bottomRightInsetPen = new(
disabledHighContrast
? colors.WindowDisabled
: stockColor ? SystemColors.ControlDark : colors.ButtonShadow);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(bottomRightInsetPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(bottomRightInsetPen, p4, p1); // Right (bottom-up)
}
/// <summary>
/// Draws a border for the in the 3D style of the popup button.
/// </summary>
protected internal static void Draw3DLiteBorder(IDeviceContext deviceContext, Rectangle r, ColorData colors, bool up)
{
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
// Draw counter-clock-wise.
Point p1 = new(r.Right - 1, r.Top); // Upper inner right
Point p2 = new(r.Left, r.Top); // Upper left
Point p3 = new(r.Left, r.Bottom - 1); // Bottom inner left
Point p4 = new(r.Right - 1, r.Bottom - 1); // Inner bottom right
Color color = GetContrastingBorderColor(colors.ButtonShadow);
// Top, left
using CreatePenScope topLeftPen = new(up ? colors.Highlight : color);
hdc.DrawLine(topLeftPen, p1, p2); // top (right-left)
hdc.DrawLine(topLeftPen, p2, p3); // left (top-down)
// Bottom, right
using CreatePenScope bottomRightPen = new(up ? color : colors.Highlight);
p1.Offset(0, -1); // Need to paint last pixel too.
hdc.DrawLine(bottomRightPen, p3, p4); // Bottom (left-right)
hdc.DrawLine(bottomRightPen, p4, p1); // Right (bottom-up)
}
/// <summary>
/// Draws a flat border with specified border size.
/// </summary>
internal static void DrawFlatBorderWithSize(
PaintEventArgs e,
Rectangle bounds,
Color color,
int size)
{
// This function gets called only for FlatStyle == FlatStyle.Flat.
size = Math.Min(size, Math.Min(bounds.Width, bounds.Height));
Rectangle left = new(bounds.X, bounds.Y, size, bounds.Height);
Rectangle right = new(bounds.X + bounds.Width - size, bounds.Y, size, bounds.Height);
Rectangle top = new(bounds.X + size, bounds.Y, bounds.Width - size * 2, size);
Rectangle bottom = new(bounds.X + size, bounds.Y + bounds.Height - size, bounds.Width - size * 2, size);
if (color.HasTransparency())
{
Graphics g = e.GraphicsInternal;
using var brush = color.GetCachedSolidBrushScope();
g.FillRectangle(brush, left);
g.FillRectangle(brush, right);
g.FillRectangle(brush, top);
g.FillRectangle(brush, bottom);
return;
}
using DeviceContextHdcScope hdc = new(e);
using CreateBrushScope hbrush = new(color);
hdc.FillRectangle(left, hbrush);
hdc.FillRectangle(right, hbrush);
hdc.FillRectangle(top, hbrush);
hdc.FillRectangle(bottom, hbrush);
}
internal static void DrawFlatFocus(IDeviceContext deviceContext, Rectangle r, Color color)
{
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
using CreatePenScope focusPen = new(color);
hdc.DrawRectangle(r, focusPen);
}
/// <summary>
/// Draws a focus rectangle if the control has focus.
/// </summary>
private void DrawFocus(Graphics g, Rectangle r)
{
if (Control.Focused && Control.ShowFocusCues)
{
ControlPaint.DrawFocusRectangle(g, r, Control.ForeColor, Control.BackColor);
}
}
internal virtual void DrawImageCore(Graphics graphics, Image image, Rectangle imageBounds, Point imageStart, LayoutData layout)
{
Region oldClip = graphics.Clip;
if (!layout.Options.DotNetOneButtonCompat)
{
Rectangle bounds = new(
ButtonBorderSize,
ButtonBorderSize,
Control.Width - (2 * ButtonBorderSize),
Control.Height - (2 * ButtonBorderSize));
Region newClip = oldClip.Clone();
newClip.Intersect(bounds);
// If we don't do this, DrawImageUnscaled will happily draw the entire image, even though imageBounds
// is smaller than the image size.
newClip.Intersect(imageBounds);
graphics.Clip = newClip;
}
else
{
imageBounds.Width++;
imageBounds.Height++;
imageBounds.X = imageStart.X + 1;
imageBounds.Y = imageStart.Y + 1;
}
try
{
if (!Control.Enabled)
{
// Need to specify width and height
ControlPaint.DrawImageDisabled(graphics, image, imageBounds, unscaledImage: true);
}
else
{
graphics.DrawImage(image, imageBounds.X, imageBounds.Y, image.Width, image.Height);
}
}
finally
{
if (!layout.Options.DotNetOneButtonCompat)
{
graphics.Clip = oldClip;
}
}
}
internal static void DrawDefaultBorder(IDeviceContext deviceContext, Rectangle r, Color color, bool isDefault)
{
if (!isDefault)
{
return;
}
r.Inflate(1, 1);
if (color.HasTransparency())
{
Graphics? graphics = deviceContext.TryGetGraphics(create: true);
if (graphics is not null)
{
using var pen = color.GetCachedPenScope();
graphics.DrawRectangle(pen, r.X, r.Y, r.Width - 1, r.Height - 1);
return;
}
}
using CreatePenScope hpen = new(color);
using DeviceContextHdcScope hdc = deviceContext.ToHdcScope();
hdc.DrawRectangle(r, hpen);
}
/// <summary>
/// Draws the button's text. <paramref name="color"/> is the foreground color set with enabled/disabled state in mind.
/// </summary>
private void DrawText(PaintEventArgs e, LayoutData layout, Color color, ColorData colors)
{
Rectangle r = layout.TextBounds;
bool disabledText3D = layout.Options.ShadowedText;
if (Control.UseCompatibleTextRendering)
{
Graphics g = e.GraphicsInternal;
// Draw text using GDI+
using StringFormat stringFormat = CreateStringFormat();
// DrawString doesn't seem to draw where it says it does
if ((Control.TextAlign & LayoutUtils.AnyCenter) == 0)
{
r.X -= 1;
}
r.Width += 1;
if (disabledText3D && !Control.Enabled && !colors.Options.HighContrast)
{
using var highlightBrush = colors.Highlight.GetCachedSolidBrushScope();
r.Offset(1, 1);
g.DrawString(Control.Text, Control.Font, highlightBrush, r, stringFormat);
r.Offset(-1, -1);
using var shadowBrush = colors.ButtonShadow.GetCachedSolidBrushScope();
g.DrawString(Control.Text, Control.Font, shadowBrush, r, stringFormat);
}
else
{
using var brush = color.GetCachedSolidBrushScope();
g.DrawString(Control.Text, Control.Font, brush, r, stringFormat);
}
}
else
{
// Draw text using GDI (.NET Framework 2.0+ feature).
TextFormatFlags formatFlags = CreateTextFormatFlags();
if (disabledText3D && !Control.Enabled && !colors.Options.HighContrast)
{
if (Application.RenderWithVisualStyles)
{
// don't draw chiseled text if themed as win32 app does.
TextRenderer.DrawTextInternal(e, Control.Text, Control.Font, r, colors.ButtonShadow, formatFlags);
}
else
{
r.Offset(1, 1);
TextRenderer.DrawTextInternal(e, Control.Text, Control.Font, r, colors.Highlight, formatFlags);
r.Offset(-1, -1);
TextRenderer.DrawTextInternal(e, Control.Text, Control.Font, r, colors.ButtonShadow, formatFlags);
}
}
else
{
TextRenderer.DrawTextInternal(e, Control.Text, Control.Font, r, color, formatFlags);
}
}
}
internal void PaintButtonBackground(PaintEventArgs e, Rectangle bounds, Brush? background)
{
if (background is null)
{
Control.PaintBackground(e, bounds);
}
else
{
e.GraphicsInternal.FillRectangle(background, bounds);
}
}
internal void PaintField(
PaintEventArgs e,
LayoutData layout,
ColorData colors,
Color foreColor,
bool drawFocus)
{
Rectangle maxFocus = layout.Focus;
DrawText(e, layout, foreColor, colors);
if (drawFocus)
{
DrawFocus(e.GraphicsInternal, maxFocus);
}
}
/// <summary>
/// Draws the button's image.
/// </summary>
internal void PaintImage(PaintEventArgs e, LayoutData layout)
{
if (Control.Image is not null)
{
// Setup new clip region & draw
DrawImageCore(e.GraphicsInternal, Control.Image, layout.ImageBounds, layout.ImageStart, layout);
}
}
internal static LayoutOptions CommonLayout(
Rectangle clientRectangle,
Padding padding,
bool isDefault,
Font font,
string text,
bool enabled,
ContentAlignment textAlign,
RightToLeft rtl) => new()
{
Client = LayoutUtils.DeflateRect(clientRectangle, padding),
Padding = padding,
GrowBorderBy1PxWhenDefault = true,
IsDefault = isDefault,
BorderSize = 2,
PaddingSize = 0,
MaxFocus = true,
FocusOddEvenFixup = false,
Font = font,
Text = text,
ImageSize = Size.Empty,
CheckSize = 0,
CheckPaddingSize = 0,
CheckAlign = ContentAlignment.TopLeft,
ImageAlign = ContentAlignment.MiddleCenter,
TextAlign = textAlign,
HintTextUp = false,
ShadowedText = !enabled,
LayoutRTL = rtl == RightToLeft.Yes,
TextImageRelation = TextImageRelation.Overlay,
UseCompatibleTextRendering = false
};
internal virtual LayoutOptions CommonLayout()
{
LayoutOptions layout = new()
{
Client = LayoutUtils.DeflateRect(Control.ClientRectangle, Control.Padding),
Padding = Control.Padding,
GrowBorderBy1PxWhenDefault = true,
IsDefault = Control.IsDefault,
BorderSize = 2,
PaddingSize = 0,
MaxFocus = true,
FocusOddEvenFixup = false,
Font = Control.Font,
Text = Control.Text,
ImageSize = (Control.Image is null) ? Size.Empty : Control.Image.Size,
CheckSize = 0,
CheckPaddingSize = 0,
CheckAlign = ContentAlignment.TopLeft,
ImageAlign = Control.ImageAlign,
TextAlign = Control.TextAlign,
HintTextUp = false,
ShadowedText = !Control.Enabled,
LayoutRTL = Control.RightToLeft == RightToLeft.Yes,
TextImageRelation = Control.TextImageRelation,
UseCompatibleTextRendering = Control.UseCompatibleTextRendering
};
if (Control.FlatStyle != FlatStyle.System)
{
if (layout.UseCompatibleTextRendering)
{
using StringFormat format = Control.CreateStringFormat();
layout.StringFormat = format;
}
else
{
layout.GdiTextFormatFlags = Control.CreateTextFormatFlags();
}
}
return layout;
}
private static ColorOptions CommonRender(
IDeviceContext deviceContext,
Color foreColor,
Color backColor,
bool enabled) => new(deviceContext, foreColor, backColor)
{
Enabled = enabled
};
private ColorOptions CommonRender(IDeviceContext deviceContext) =>
new(deviceContext, Control.ForeColor, Control.BackColor)
{
Enabled = Control.Enabled
};
protected ColorOptions PaintRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
internal static ColorOptions PaintFlatRender(Graphics g, Color foreColor, Color backColor, bool enabled) =>
CommonRender(g, foreColor, backColor, enabled);
protected ColorOptions PaintFlatRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
internal static ColorOptions PaintPopupRender(Graphics g, Color foreColor, Color backColor, bool enabled) =>
CommonRender(g, foreColor, backColor, enabled);
protected ColorOptions PaintPopupRender(IDeviceContext deviceContext) => CommonRender(deviceContext);
}
|