diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.StockObject.cs b/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.StockObject.cs index 4e889903d5c..7c0b93c57ed 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.StockObject.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.StockObject.cs @@ -6,27 +6,33 @@ internal static partial class Interop { internal static partial class Gdi32 { - internal enum StockObject : uint + /// + /// Stock GDI object identifiers. + /// + /// + /// Note that in metafile records these values are OR'ed with 0x80000000. + /// + internal enum StockObject : int { - WHITE_BRUSH = 0x80000000, // 0x00FFFFFF - LTGRAY_BRUSH = 0x80000001, // 0x00C0C0C0 - GRAY_BRUSH = 0x80000002, // 0x00808080 - DKGRAY_BRUSH = 0x80000003, // 0x00404040 - BLACK_BRUSH = 0x80000004, // 0x00404040 - NULL_BRUSH = 0x80000005, // Same as HOLLOW_BRUSH - WHITE_PEN = 0x80000006, // 0x00FFFFFF - BLACK_PEN = 0x80000007, // 0x00000000 - NULL_PEN = 0x80000008, - OEM_FIXED_FONT = 0x8000000A, - ANSI_FIXED_FONT = 0x8000000B, - ANSI_VAR_FONT = 0x8000000C, - SYSTEM_FONT = 0x8000000D, - DEVICE_DEFAULT_FONT = 0x8000000E, - DEFAULT_PALETTE = 0x8000000F, - SYSTEM_FIXED_FONT = 0x80000010, - DEFAULT_GUI_FONT = 0x80000011, - DC_BRUSH = 0x80000012, - DC_PEN = 0x80000013 + WHITE_BRUSH = 0x00000000, // 0x00FFFFFF + LTGRAY_BRUSH = 0x00000001, // 0x00C0C0C0 + GRAY_BRUSH = 0x00000002, // 0x00808080 + DKGRAY_BRUSH = 0x00000003, // 0x00404040 + BLACK_BRUSH = 0x00000004, // 0x00000000 + NULL_BRUSH = 0x00000005, // Same as HOLLOW_BRUSH + WHITE_PEN = 0x00000006, // 0x00FFFFFF + BLACK_PEN = 0x00000007, // 0x00000000 + NULL_PEN = 0x00000008, + OEM_FIXED_FONT = 0x0000000A, + ANSI_FIXED_FONT = 0x0000000B, + ANSI_VAR_FONT = 0x0000000C, + SYSTEM_FONT = 0x0000000D, + DEVICE_DEFAULT_FONT = 0x0000000E, + DEFAULT_PALETTE = 0x0000000F, + SYSTEM_FIXED_FONT = 0x00000010, + DEFAULT_GUI_FONT = 0x00000011, + DC_BRUSH = 0x00000012, + DC_PEN = 0x00000013 } } } diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfRecord.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfRecord.cs index 5a6e1d139df..4530c71baef 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfRecord.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfRecord.cs @@ -91,6 +91,20 @@ public EMREXTTEXTOUTW* ExtTextOutWRecord => Type == Gdi32.EMR.EXTTEXTOUTW ? (EMREXTTEXTOUTW*)_lpmr : null; public EMRENUMRECORD* SetMapModeRecord => Type == Gdi32.EMR.SETMAPMODE ? (EMRENUMRECORD*)_lpmr : null; + public EMRRECTRECORD* FillPathRecord + => Type == Gdi32.EMR.FILLPATH ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* StrokeAndFillPathRecord + => Type == Gdi32.EMR.STROKEANDFILLPATH ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* StrokePathRecord + => Type == Gdi32.EMR.STROKEPATH ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* ExcludeClipRectRecord + => Type == Gdi32.EMR.EXCLUDECLIPRECT ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* IntersetClipRectRecord + => Type == Gdi32.EMR.INTERSECTCLIPRECT ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* EllipseRecord + => Type == Gdi32.EMR.ELLIPSE ? (EMRRECTRECORD*)_lpmr : null; + public EMRRECTRECORD* RectangleRecord + => Type == Gdi32.EMR.RECTANGLE ? (EMRRECTRECORD*)_lpmr : null; public override string ToString() => Type switch { @@ -119,6 +133,13 @@ public EMRENUMRECORD* SetMapModeRecord Gdi32.EMR.EXTCREATEFONTINDIRECTW => ExtCreateFontIndirectWRecord->ToString(), Gdi32.EMR.EXTTEXTOUTW => ExtTextOutWRecord->ToString(), Gdi32.EMR.SETMAPMODE => SetMapModeRecord->ToString(), + Gdi32.EMR.FILLPATH => FillPathRecord->ToString(), + Gdi32.EMR.STROKEANDFILLPATH => StrokeAndFillPathRecord->ToString(), + Gdi32.EMR.STROKEPATH => StrokePathRecord->ToString(), + Gdi32.EMR.EXCLUDECLIPRECT => ExcludeClipRectRecord->ToString(), + Gdi32.EMR.INTERSECTCLIPRECT => IntersetClipRectRecord->ToString(), + Gdi32.EMR.ELLIPSE => EllipseRecord->ToString(), + Gdi32.EMR.RECTANGLE => RectangleRecord->ToString(), _ => $"[EMR{Type}]" }; } diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRINDEXRECORD.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRINDEXRECORD.cs index d6354a0db29..ca458b4a2a1 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRINDEXRECORD.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRINDEXRECORD.cs @@ -26,7 +26,7 @@ internal struct EMRINDEXRECORD public uint index; public bool IsStockObject => (index & 0x80000000) != 0; - public Gdi32.StockObject StockObject => (Gdi32.StockObject)index; + public Gdi32.StockObject StockObject => (Gdi32.StockObject)(index & ~0x80000000); public override string ToString() => IsStockObject diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRRECTRECORD.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRRECTRECORD.cs new file mode 100644 index 00000000000..1a53cd46d6e --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/RecordTypes/EMRRECTRECORD.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System.Drawing; +using System.Runtime.InteropServices; +using static Interop; + +namespace System.Windows.Forms.Metafiles +{ + /// + /// Record that just has a single value. + /// + /// + /// Not an actual Win32 define, encapsulates: + /// + /// - EMRFILLPATH + /// - EMRSTROKEANDFILLPATH + /// - EMRSTROKEPATH + /// - EMREXCLUDECLIPRECT + /// - EMRINTERSECTCLIPRECT + /// - EMRELLIPSE + /// - EMRRECTANGLE + /// + [StructLayout(LayoutKind.Sequential)] + internal struct EMRRECTRECORD + { + public EMR emr; + public RECT rect; + + public override string ToString() => $"[EMR{emr.iType}] RECT: {rect}"; + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/RectangleValidator.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/RectangleValidator.cs new file mode 100644 index 00000000000..769ce917cd2 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/RectangleValidator.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System.Drawing; +using Xunit; +using static Interop; + +namespace System.Windows.Forms.Metafiles +{ + internal sealed class RectangleValidator : IEmfValidator + { + private readonly Flags _validate; + private readonly Rectangle _bounds; + private readonly int _penWidth; + private readonly Color _penColor; + private readonly Gdi32.PS _penStyle; + private readonly Gdi32.R2 _rop2; + private readonly Gdi32.BKMODE _backgroundMode; + private readonly Gdi32.BS _brushStyle; + private readonly Color _brushColor; + + public RectangleValidator( + RECT bounds, + Color penColor, + Color brushColor, + int penWidth = 1, + Gdi32.PS penStyle = default, + Gdi32.R2 rop2 = Gdi32.R2.COPYPEN, + Gdi32.BKMODE backgroundMode = Gdi32.BKMODE.TRANSPARENT, + Gdi32.BS brushStyle = default, + Flags validate = default) + { + _bounds = bounds; + _brushColor = brushColor; + _brushStyle = brushStyle; + _penWidth = penWidth; + _penColor = penColor; + _penStyle = penStyle; + _rop2 = rop2; + _backgroundMode = backgroundMode; + + if (validate != default) + { + _validate = validate; + return; + } + + // Default values for all of these are valid expectations so we always turn them on. + _validate = Flags.Bounds | Flags.PenStyle | Flags.BrushStyle; + + if (penWidth != 0) + { + _validate |= Flags.PenWidth; + } + + if (!penColor.IsEmpty) + { + _validate |= Flags.PenColor; + } + + if (!brushColor.IsEmpty) + { + _validate |= Flags.BrushColor; + } + + if (backgroundMode != default) + { + _validate |= Flags.BackgroundMode; + } + + if (_rop2 != default) + { + _validate |= Flags.RopMode; + } + } + + public bool ShouldValidate(Gdi32.EMR recordType) => recordType == Gdi32.EMR.RECTANGLE; + + public unsafe void Validate(ref EmfRecord record, DeviceContextState state, out bool complete) + { + // We're only checking one TextOut record, so this call completes our work. + complete = true; + + EMRRECTRECORD* rectangle = record.RectangleRecord; + + if (_validate.HasFlag(Flags.Bounds)) + { + Assert.Equal(_bounds, (Rectangle)rectangle->rect); + } + + if (_validate.HasFlag(Flags.BrushColor)) + { + Assert.Equal((COLORREF)_brushColor, state.SelectedBrush.lbColor); + } + + if (_validate.HasFlag(Flags.BrushStyle)) + { + Assert.Equal(_brushStyle, state.SelectedBrush.lbStyle); + } + + if (_validate.HasFlag(Flags.PenColor)) + { + Assert.Equal((COLORREF)_penColor, state.SelectedPen.lopnColor); + } + + if (_validate.HasFlag(Flags.PenWidth)) + { + Assert.Equal(_penWidth, state.SelectedPen.lopnWidth.X); + } + + if (_validate.HasFlag(Flags.PenStyle)) + { + Assert.Equal(_penStyle, state.SelectedPen.lopnStyle); + } + + if (_validate.HasFlag(Flags.BackgroundMode)) + { + Assert.Equal(_backgroundMode, state.BackgroundMode); + } + + if (_validate.HasFlag(Flags.RopMode)) + { + Assert.Equal(_rop2, state.Rop2Mode); + } + } + + [Flags] + public enum Flags : uint + { + Bounds = 0b00000000_00000000_00000000_00000001, + PenColor = 0b00000000_00000000_00000000_00000100, + PenWidth = 0b00000000_00000000_00000000_00001000, + PenStyle = 0b00000000_00000000_00000000_00010000, + RopMode = 0b00000000_00000000_00000000_00100000, + BackgroundMode = 0b00000000_00000000_00000000_01000000, + BrushColor = 0b00000000_00000000_00000000_10000000, + BrushStyle = 0b00000000_00000000_00000001_00000000 + } + } +} diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/Validate.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/Validate.cs index 89ed5358478..f21c36e274f 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/Validate.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/Validators/Validate.cs @@ -5,6 +5,7 @@ #nullable enable using System.Drawing; +using System.Net; using static Interop; namespace System.Windows.Forms.Metafiles @@ -56,6 +57,26 @@ internal static IEmfValidator LineTo( backgroundMode, validate); + internal static IEmfValidator Rectangle( + RECT bounds, + Color penColor, + Color brushColor, + int penWidth = 1, + Gdi32.PS penStyle = default, + Gdi32.R2 rop2 = Gdi32.R2.COPYPEN, + Gdi32.BKMODE backgroundMode = Gdi32.BKMODE.TRANSPARENT, + Gdi32.BS brushStyle = default, + RectangleValidator.Flags validate = default) => new RectangleValidator( + bounds, + penColor, + brushColor, + penWidth, + penStyle, + rop2, + backgroundMode, + brushStyle, + validate); + /// /// Simple wrapper to allow doing an arbitrary action for a given . /// diff --git a/src/System.Windows.Forms.Primitives/tests/UnitTests/Interop/Gdi32/GetStockObjectTests.cs b/src/System.Windows.Forms.Primitives/tests/UnitTests/Interop/Gdi32/GetStockObjectTests.cs new file mode 100644 index 00000000000..79cc55cb320 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/UnitTests/Interop/Gdi32/GetStockObjectTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; +using static Interop; + +namespace System.Windows.Forms.Gdi32Tests +{ + public class GetStockObjectTests + { + [Theory] + [InlineData((int)Gdi32.StockObject.BLACK_BRUSH, 0x00000000, (uint)Gdi32.BS.SOLID)] + [InlineData((int)Gdi32.StockObject.NULL_BRUSH, 0x00000000, (uint)Gdi32.BS.HOLLOW)] + [InlineData((int)Gdi32.StockObject.WHITE_BRUSH, 0x00FFFFFF, (uint)Gdi32.BS.SOLID)] + public void GetStockBrushes(int id, uint color, uint brushStyle) + { + Gdi32.HGDIOBJ hgdiobj = Gdi32.GetStockObject((Gdi32.StockObject)id); + Assert.False(hgdiobj.IsNull); + + Gdi32.GetObjectW(hgdiobj, out Gdi32.LOGBRUSH logBrush); + Assert.Equal(color, logBrush.lbColor); + Assert.Equal((Gdi32.BS)brushStyle, logBrush.lbStyle); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ButtonRenderingTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ButtonRenderingTests.cs index 3acb5bbdb6f..3c9859b2072 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ButtonRenderingTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ButtonRenderingTests.cs @@ -91,5 +91,34 @@ public unsafe void CaptureButtonOnForm() var details = emf.RecordsToString(); } + + [WinFormsFact] + public unsafe void Button_FlatStyle_WithText_Rectangle() + { + using Button button = new Button + { + Text = "Flat Style", + FlatStyle = FlatStyle.Flat, + }; + + using var emf = new EmfScope(); + DeviceContextState state = new DeviceContextState(emf); + + Rectangle bounds = button.Bounds; + + button.PrintToMetafile(emf); + + emf.Validate( + state, + Validate.SkipType(Gdi32.EMR.BITBLT), + Validate.TextOut("Flat Style"), + Validate.Rectangle( + new Rectangle(0, 0, button.Width - 2, button.Height - 2), + penColor: Color.Black, + penStyle: Gdi32.PS.ENDCAP_ROUND, + brushColor: Color.Empty, // Color doesn't really matter as we're using a null brush + brushStyle: Gdi32.BS.NULL, // Regressed in https://github.com/dotnet/winforms/pull/3667 + rop2: Gdi32.R2.COPYPEN)); + } } }