diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.HENHMETAFILE.cs b/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.HENHMETAFILE.cs index 9d6d0fe59ea..bcd21574b5b 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.HENHMETAFILE.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Gdi32/Interop.HENHMETAFILE.cs @@ -17,6 +17,7 @@ internal static partial class Gdi32 public bool IsNull => Handle == IntPtr.Zero; public static explicit operator IntPtr(HENHMETAFILE hmetafile) => hmetafile.Handle; + public static explicit operator HENHMETAFILE(IntPtr hmetafile) => new HENHMETAFILE(hmetafile); } } } diff --git a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfScope.cs b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfScope.cs index e199b0ebad6..672907b368a 100644 --- a/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfScope.cs +++ b/src/System.Windows.Forms.Primitives/tests/TestUtilities/Metafiles/EmfScope.cs @@ -14,7 +14,7 @@ namespace System.Windows.Forms.Metafiles internal class EmfScope : DisposalTracking.Tracker, IDisposable { public Gdi32.HDC HDC { get; } - private Gdi32.HENHMETAFILE _hmf; + private Gdi32.HENHMETAFILE _hemf; public unsafe EmfScope() : this (Gdi32.CreateEnhMetaFileW(hdc: default, lpFilename: null, lprc: null, lpDesc: null)) @@ -24,7 +24,12 @@ public unsafe EmfScope() public EmfScope(Gdi32.HDC hdc) { HDC = hdc; - _hmf = default; + _hemf = default; + } + + public EmfScope(Gdi32.HENHMETAFILE hemf) + { + _hemf = hemf; } public unsafe static EmfScope Create() => new EmfScope(); @@ -33,17 +38,17 @@ public Gdi32.HENHMETAFILE HENHMETAFILE { get { - if (HDC.IsNull) + if (_hemf.IsNull) { - return default; - } + if (HDC.IsNull) + { + return default; + } - if (_hmf.IsNull) - { - _hmf = Gdi32.CloseEnhMetaFile(HDC); + _hemf = Gdi32.CloseEnhMetaFile(HDC); } - return _hmf; + return _hemf; } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/TextRenderer.cs b/src/System.Windows.Forms/src/System/Windows/Forms/TextRenderer.cs index 15944662045..814f25695af 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/TextRenderer.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/TextRenderer.cs @@ -4,7 +4,6 @@ using System.Drawing; using System.Drawing.Text; -using System.Windows.Forms.Internal; using static Interop; namespace System.Windows.Forms @@ -301,7 +300,7 @@ public static void DrawText(IDeviceContext dc, string? text, Font? font, Rectang // This MUST come before retreiving the HDC, which locks the Graphics object Gdi32.QUALITY quality = FontQualityFromTextRenderingHint(dc); - using var hdc = new DeviceContextHdcScope(dc); + using var hdc = new DeviceContextHdcScope(dc, GetApplyStateFlags(dc, flags)); DrawTextInternal(hdc, text, font, bounds, foreColor, quality, backColor, flags); } @@ -534,7 +533,9 @@ public static Size MeasureText(IDeviceContext dc, ReadOnlySpan text, Font? // This MUST come before retreiving the HDC, which locks the Graphics object Gdi32.QUALITY quality = FontQualityFromTextRenderingHint(dc); - using var hdc = new DeviceContextHdcScope(dc); + // Applying state may not impact text size measurements. Rather than risk missing some + // case we'll apply as we have historically to avoid suprise regressions. + using var hdc = new DeviceContextHdcScope(dc, GetApplyStateFlags(dc, flags)); using var hfont = GdiCache.GetHFONT(font, quality, hdc); return hdc.HDC.MeasureText(text, hfont, proposedSize, flags); } @@ -596,5 +597,29 @@ private static Gdi32.QUALITY GetDefaultFontQuality() return SystemInformation.FontSmoothingType == 0x0002 ? Gdi32.QUALITY.CLEARTYPE : Gdi32.QUALITY.ANTIALIASED; } + + /// + /// Gets the proper flags for the given . + /// + private static ApplyGraphicsProperties GetApplyStateFlags(IDeviceContext deviceContext, TextFormatFlags textFormatFlags) + { + if (deviceContext is not Graphics) + { + return ApplyGraphicsProperties.None; + } + + var apply = ApplyGraphicsProperties.None; + if (textFormatFlags.HasFlag(TextFormatFlags.PreserveGraphicsClipping)) + { + apply |= ApplyGraphicsProperties.Clipping; + } + + if (textFormatFlags.HasFlag(TextFormatFlags.PreserveGraphicsTranslateTransform)) + { + apply |= ApplyGraphicsProperties.TranslateTransform; + } + + return apply; + } } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextRendererTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextRendererTests.cs index 8fa66ab2bed..1479f6c156b 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextRendererTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/TextRendererTests.cs @@ -4,7 +4,10 @@ using System.Collections.Generic; using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Drawing.Text; +using System.IO; using System.Windows.Forms.Metafiles; using Moq; using WinForms.Common.Tests; @@ -829,5 +832,87 @@ public void TextRenderer_MeasureText_Padding(TextFormatFlags flags, Size expecte { TextFormatFlags.LeftAndRightPadding, new Size(82, 13) }, { TextFormatFlags.NoPadding, new Size(71, 13) } }; + + [WinFormsTheory] + [MemberData(nameof(TextRenderer_DrawText_ApplyState_TestData))] + public void TextRenderer_DrawText_ApplyState(TextFormatFlags flags, Rectangle expectedBounds) + { + using var hdc = new Interop.Gdi32.CreateDcScope(default); + DeviceContextState state = new DeviceContextState(hdc); + + using MemoryStream stream = new MemoryStream(1024); + using (Metafile metafileRecorder = new Metafile(stream, hdc.HDC.Handle, EmfType.EmfOnly)) + using (Graphics graphics = Graphics.FromImage(metafileRecorder)) + { + using Matrix matrix = new Matrix(); + matrix.Translate(5, 10); + graphics.Transform = matrix; + using Region region = new(new Rectangle(1, 2, 6, 8)); + graphics.Clip = region; + + TextRenderer.DrawText( + graphics, + "Landshark", + SystemFonts.DefaultFont, + new Rectangle(0, 0, int.MaxValue, int.MaxValue), + Color.Red, + flags); + } + + // Need to queue the stream back to the beginning for the reader + stream.Position = 0; + using Metafile metafile = new Metafile(stream); + using var emf = new EmfScope((Interop.Gdi32.HENHMETAFILE)metafile.GetHenhmetafile()); + + emf.Validate( + state, + Validate.TextOut( + "Landshark", + expectedBounds, + State.FontFace(SystemFonts.DefaultFont.Name), + State.TextColor(Color.Red))); + } + + public static TheoryData TextRenderer_DrawText_ApplyState_TestData + => new TheoryData + { + { TextFormatFlags.Default, new Rectangle(3, 0, 49, 12) }, + { TextFormatFlags.PreserveGraphicsTranslateTransform, new Rectangle(8, 10, 49, 12) }, + { TextFormatFlags.PreserveGraphicsClipping, new Rectangle(6, 12, 5, 0) }, + { TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.PreserveGraphicsTranslateTransform, new Rectangle(8, 12, 3, 7) }, + }; + + [WinFormsTheory] + [MemberData(nameof(TextRenderer_MeasureText_ApplyState_TestData))] + public void TextRenderer_MeasureText_ApplyState(TextFormatFlags flags, Size expectedSize) + { + using var image = new Bitmap(200, 50); + using Graphics graphics = Graphics.FromImage(image); + using Matrix matrix = new Matrix(); + matrix.Translate(5, 10); + graphics.Transform = matrix; + using Region region = new(new Rectangle(1, 2, 6, 8)); + graphics.Clip = region; + + Size size = TextRenderer.MeasureText( + graphics, + "Landshark", + SystemFonts.DefaultFont, + new Size(int.MaxValue, int.MaxValue), + flags); + + Assert.Equal(expectedSize, size); + } + + public static TheoryData TextRenderer_MeasureText_ApplyState_TestData + => new TheoryData + { + // State application doesn't practially impact size measurements, but we still want to have a regession test + // here in case something sneaks in. + { TextFormatFlags.Default, new Size(57, 13) }, + { TextFormatFlags.PreserveGraphicsTranslateTransform, new Size(57, 13) }, + { TextFormatFlags.PreserveGraphicsClipping, new Size(57, 13) }, + { TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.PreserveGraphicsTranslateTransform, new Size(57, 13) }, + }; } }