Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,33 @@ internal static partial class Interop
{
internal static partial class Gdi32
{
internal enum StockObject : uint
/// <summary>
/// Stock GDI object identifiers.
/// </summary>
/// <remarks>
/// Note that in metafile records these values are OR'ed with 0x80000000.
/// </remarks>
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ public EMREXTTEXTOUTW* ExtTextOutWRecord
=> Type == Gdi32.EMR.EXTTEXTOUTW ? (EMREXTTEXTOUTW*)_lpmr : null;
public EMRENUMRECORD<Gdi32.MM>* SetMapModeRecord
=> Type == Gdi32.EMR.SETMAPMODE ? (EMRENUMRECORD<Gdi32.MM>*)_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
{
Expand Down Expand Up @@ -119,6 +133,13 @@ public EMRENUMRECORD<Gdi32.MM>* 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}]"
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

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

I had a comment about distinction between "regular" stock objects to EMF stock objects, but it looks like it did not get saved...

Because your explanation about the high order 1 in EMF stock objects is in a different file, this code will be more readable if you qualify what kind of stock object it is.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add some comments in master clarifying that EMF takes the twos complement to smuggle stock object identifiers.


public override string ToString()
=> IsStockObject
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Record that just has a single <see cref="RECT"/> value.
/// </summary>
/// <remarks>
/// Not an actual Win32 define, encapsulates:
///
/// - EMRFILLPATH
/// - EMRSTROKEANDFILLPATH
/// - EMRSTROKEPATH
/// - EMREXCLUDECLIPRECT
/// - EMRINTERSECTCLIPRECT
/// - EMRELLIPSE
/// - EMRRECTANGLE
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
internal struct EMRRECTRECORD
{
public EMR emr;
public RECT rect;

public override string ToString() => $"[EMR{emr.iType}] RECT: {rect}";
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable enable

using System.Drawing;
using System.Net;
using static Interop;

namespace System.Windows.Forms.Metafiles
Expand Down Expand Up @@ -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);

/// <summary>
/// Simple wrapper to allow doing an arbitrary action for a given <paramref name="recordType"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,34 @@ public unsafe void CaptureButtonOnForm()

var details = emf.RecordsToString();
Copy link
Member Author

Choose a reason for hiding this comment

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

This is how you get an idea of the records involved. Once you PrintToMetafile(emf) call emf.RecordsToString() to dump it out in the debugger.

}

[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));
}
}
}