diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/AnchorLayoutHighDpiRegressionDemoForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/AnchorLayoutHighDpiRegressionDemoForm.cs deleted file mode 100644 index a86ce829d64..00000000000 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/AnchorLayoutHighDpiRegressionDemoForm.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System.Drawing; -#if NET -using System.Runtime.InteropServices; -#endif -using System.Windows.Forms; - -namespace Demo; - -/// -/// Demo form that reproduces the anchor-layout high-DPI regression (WI00955507). -/// See anchor-layout-highDpi-regression.md for the full analysis. -/// -/// -/// -/// The regression manifests when ScaleHelper.IsScalingRequirementMet returns -/// — i.e. when at least one monitor is at a DPI scale above 100%. -/// The demo application must run with HighDpiMode.SystemAware (see Program.cs). -/// -/// -/// Symptom: OtherReferenceTextBox is displaced below — or entirely outside — the -/// MiscGroupBox client area after the form is shown. The title bar reports the -/// layout outcome once the form becomes visible. -/// -/// -/// Fix: add "System.Windows.Forms.AnchorLayoutV2": true to the app's -/// runtimeconfig.json to enable the V2 deferred-anchor path. -/// -/// -/// Observed platform behavior (counter-intuitive): this demo fails on -/// .NET Framework 4.8 even at 100% DPI (96 dpi), while it passes on the WTG -/// .NET 10 fork. The forced transient MiscGroupBox height of 50 px means -/// the captured Bottom anchor offset is always a large positive value -/// (e.g. +230 px at 96 dpi), which displaces OtherReferenceTextBox far below -/// the group box regardless of DPI scale. .NET Framework has no mechanism to correct -/// a stale positive anchor. The WTG fork's Solution 3 repair in -/// DefaultLayout.AnchorLayoutCompat.cs detects the positive offset via -/// ShouldRefreshAnchorInfoForStalePositiveAnchors and recomputes it against the -/// stable parent DisplayRectangle on the next layout pass, which is why the -/// demo passes on .NET 10 even before any DPI scaling is involved. -/// -/// -public class AnchorLayoutHighDpiRegressionDemoForm : Form -{ -#if NET - private static readonly string s_frameworkDescription = RuntimeInformation.FrameworkDescription; -#endif - private static readonly int s_scaleDpi = GetScaleDpi(); - private static readonly Size s_miscGroupBoxSize = ScaledSize(324, 240); - private static readonly int s_expectedBottomMargin = Scale(10); - - private readonly GroupBox _miscGroupBox; - private readonly TextBox _otherReferenceTextBox; - private readonly CheckBox _goodsServicesIndicatorCheckBox; - private readonly CheckBox _royaltyPaymentIndicatorCheckBox; - private readonly TextBox _diagnosticsTextBox; - - // Captured in OnLoad (before the form becomes visible) to mirror the - // "before-show" diagnostics logged by DefaultLayoutTest. - private Rectangle _miscGroupBoxClientAtAnchorCapture; - private Rectangle _otherRefBoundsBeforeShow; - private Rectangle _miscGroupBoxClientBeforeShow; - - public AnchorLayoutHighDpiRegressionDemoForm() - { - Font = new Font(new FontFamily("Microsoft Sans Serif"), 8.25f); - - _diagnosticsTextBox = new TextBox - { - Name = "DiagnosticsTextBox", - Dock = DockStyle.Bottom, - Multiline = true, - ReadOnly = true, - ScrollBars = ScrollBars.Vertical, - Height = Scale(96), - TabStop = false - }; - - // Mirror the hierarchy and initialization order from the CargoWise repro: - // TabControl → TabPage → GroupBox, with the GroupBox already parented into the - // tab hierarchy before its bottom-anchored child is added. - TabControl invoiceTabControl = new() - { - Dock = DockStyle.Fill, - Name = "InvoiceTabControl", - MinimumSize = ScaledSize(480, 470), - Size = ScaledSize(480, 480) - }; - - TabPage otherInfoTabPage = new() - { - Text = "Other Info", - Name = "OtherInfoTabPage", - Location = ScaledPoint(4, 23), - Padding = new Padding(Scale(3)), - Size = ScaledSize(480, 323) - }; - - // The CargoWise host uses a custom group box with hidden-caption semantics. - // Using ClientRectangle as DisplayRectangle keeps the reduced repro aligned - // with the authoritative layout math. - _miscGroupBox = new CargoWiseLikeGroupBox - { - Name = "MiscGroupBox", - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left, - Location = ScaledPoint(7, 3), - Size = s_miscGroupBoxSize, - TabStop = false - }; - - otherInfoTabPage.Controls.Add(_miscGroupBox); - invoiceTabControl.Controls.Add(otherInfoTabPage); - - Label lastPortDateLabel = new() - { - Text = "Date of Direct Shipment:", - Location = ScaledPoint(16, 75), - AutoSize = true - }; - - TextBox lastPortDateTextBox = new() - { - Name = "LastPortDateTextBox", - Location = ScaledPoint(133, 71), - Size = ScaledSize(183, 20), - TabIndex = 2 - }; - - Label conditionsLabel = new() - { - Text = "Conditions:", - Location = ScaledPoint(16, 101), - AutoSize = true - }; - - TextBox conditionsOfSaleTextBox = new() - { - Name = "ConditionsOfSaleTextBox", - Location = ScaledPoint(133, 97), - Size = ScaledSize(183, 20), - TabIndex = 3 - }; - - Label termsLabel = new() - { - Text = "Terms:", - Location = ScaledPoint(16, 127), - AutoSize = true - }; - - TextBox termsOfPaymentTextBox = new() - { - Name = "TermsOfPaymentTextBox", - Location = ScaledPoint(133, 123), - Size = ScaledSize(183, 20), - TabIndex = 4 - }; - - _goodsServicesIndicatorCheckBox = new CheckBox - { - Name = "ServicesIndCheckBox", - Text = "Goods/Services Indicator", - Location = ScaledPoint(133, 149), - AutoSize = true, - TabIndex = 5, - UseVisualStyleBackColor = true - }; - - _royaltyPaymentIndicatorCheckBox = new CheckBox - { - Name = "RoyaltyIndCheckBox", - Text = "Royalty Payment Indicator", - Location = ScaledPoint(133, 169), - AutoSize = true, - TabIndex = 6, - UseVisualStyleBackColor = true - }; - - _otherReferenceTextBox = new TextBox - { - Name = "OtherReferenceTextBox", - Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Location = ScaledPoint(6, 142), - Multiline = true, - ScrollBars = ScrollBars.Vertical, - Size = ScaledSize(310, 88), - TabIndex = 7 - }; - - _miscGroupBox.Controls.Add(lastPortDateLabel); - _miscGroupBox.Controls.Add(lastPortDateTextBox); - _miscGroupBox.Controls.Add(conditionsLabel); - _miscGroupBox.Controls.Add(conditionsOfSaleTextBox); - _miscGroupBox.Controls.Add(termsLabel); - _miscGroupBox.Controls.Add(termsOfPaymentTextBox); - _miscGroupBox.Controls.Add(_goodsServicesIndicatorCheckBox); - _miscGroupBox.Controls.Add(_royaltyPaymentIndicatorCheckBox); - - _miscGroupBoxClientAtAnchorCapture = _miscGroupBox.ClientRectangle; - _miscGroupBox.Controls.Add(_otherReferenceTextBox); - - Controls.Add(invoiceTabControl); - invoiceTabControl.SelectedTab = otherInfoTabPage; - - Controls.Add(_diagnosticsTextBox); - - ClientSize = ScaledSize(500, 560); - Name = nameof(AnchorLayoutHighDpiRegressionDemoForm); - Text = "Anchor Layout High-DPI Regression Demo"; - StartPosition = FormStartPosition.CenterScreen; - } - - /// - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - // Capture the pre-show layout state. This mirrors the "before-show" logging in - // DefaultLayoutTest and is the first place where the anchor-offset bug is visible: - // if OtherReferenceTextBox.Bounds is already outside MiscGroupBox.ClientRectangle - // here, the anchor info was captured with a wrong transient parent height. - _otherRefBoundsBeforeShow = _otherReferenceTextBox.Bounds; - _miscGroupBoxClientBeforeShow = _miscGroupBox.ClientRectangle; - } - - /// - protected override void OnShown(EventArgs e) - { - base.OnShown(e); - LogLayoutOutcome(); - } - - private void LogLayoutOutcome() - { - Rectangle groupBoxClientAfterShow = _miscGroupBox.ClientRectangle; - Rectangle otherRefBoundsAfterShow = _otherReferenceTextBox.Bounds; - - bool withinGroupBox = groupBoxClientAfterShow.Contains(otherRefBoundsAfterShow); - bool overlapsServices = otherRefBoundsAfterShow.IntersectsWith(_goodsServicesIndicatorCheckBox.Bounds); - bool overlapsRoyalty = otherRefBoundsAfterShow.IntersectsWith(_royaltyPaymentIndicatorCheckBox.Bounds); - bool belowIndicators = otherRefBoundsAfterShow.Top >= _royaltyPaymentIndicatorCheckBox.Bounds.Bottom; - int expectedY = groupBoxClientAfterShow.Height - s_expectedBottomMargin - otherRefBoundsAfterShow.Height; - bool alignedToExpectedBottomMargin = otherRefBoundsAfterShow.Y == expectedY; - - bool pass = withinGroupBox - && !overlapsServices - && !overlapsRoyalty - && belowIndicators - && alignedToExpectedBottomMargin; - - using Graphics g = CreateGraphics(); - int dpi = (int)g.DpiX; - - string dpiNote = dpi == 96 ? " (DPI=100%: set a monitor >100% to see FAIL)" : string.Empty; - -#if NET - string runtimeInfo = $"TFM={s_frameworkDescription} | HighDpiMode={Application.HighDpiMode} | ScaleDpi={s_scaleDpi} | DeviceDpi={DeviceDpi} | GraphicsDpi={dpi}"; -#else - string runtimeInfo = $"TFM=.NET Framework | ScaleDpi={s_scaleDpi} | GraphicsDpi={dpi}"; -#endif - - Text = $"{(pass ? "PASS" : "FAIL")}{dpiNote} — DPI={dpi}"; - _diagnosticsTextBox.Text = - $"ANCHOR-CAPTURE: GroupBox={_miscGroupBoxClientAtAnchorCapture}{Environment.NewLine}" + - $"BEFORE-SHOW: GroupBox={_miscGroupBoxClientBeforeShow} OtherRef={_otherRefBoundsBeforeShow}{Environment.NewLine}" + - $"AFTER-SHOW: GroupBox={groupBoxClientAfterShow} OtherRef={otherRefBoundsAfterShow}{Environment.NewLine}" + - $"EXPECT: BottomMargin={s_expectedBottomMargin} ExpectedY={expectedY} BelowIndicators={belowIndicators}{Environment.NewLine}" + - runtimeInfo; - } - - private sealed class CargoWiseLikeGroupBox : GroupBox - { - public override Rectangle DisplayRectangle => ClientRectangle; - } - - // Scale a logical (96-dpi) pixel value to device pixels, matching the behaviour of - // ControlDpiScalingHelper.NewScaledPoint/NewScaledSize in the CargoWise codebase. - private static int GetScaleDpi() - { -#if NET - try - { - return (int)GetDpiForSystem(); - } - catch (EntryPointNotFoundException) - { - } -#endif - - using Graphics g = Graphics.FromHwnd(IntPtr.Zero); - - return (int)Math.Round(g.DpiX); - } - - private static int Scale(int value) => (int)Math.Round(value * s_scaleDpi / 96.0); - -#if NET - [DllImport("user32.dll")] - private static extern uint GetDpiForSystem(); -#endif - - private static Point ScaledPoint(int x, int y) => new(Scale(x), Scale(y)); - - private static Size ScaledSize(int w, int h) => new(Scale(w), Scale(h)); -} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs index 03a0ec0fcbd..df5d53c3d5e 100644 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; @@ -19,8 +19,6 @@ partial class MainForm private Label _dataGridDescriptionLabel = null!; private Button _statusBarButton = null!; private Label _statusBarDescriptionLabel = null!; - private Button _anchorLayoutHighDpiRegressionButton = null!; - private Label _anchorLayoutHighDpiRegressionDescriptionLabel = null!; protected override void Dispose(bool disposing) { @@ -45,8 +43,6 @@ private void InitializeComponent() _dataGridDescriptionLabel = new Label(); _statusBarButton = new Button(); _statusBarDescriptionLabel = new Label(); - _anchorLayoutHighDpiRegressionButton = new Button(); - _anchorLayoutHighDpiRegressionDescriptionLabel = new Label(); SuspendLayout(); // // _titleLabel @@ -140,31 +136,11 @@ private void InitializeComponent() _statusBarDescriptionLabel.Text = "Simple text mode, panel layout, owner-draw rendering, border styles, and sizing grip behavior."; _statusBarDescriptionLabel.TextAlign = ContentAlignment.TopCenter; // - // _anchorLayoutHighDpiRegressionButton - // - _anchorLayoutHighDpiRegressionButton.Location = new Point(210, 378); - _anchorLayoutHighDpiRegressionButton.Name = "_anchorLayoutHighDpiRegressionButton"; - _anchorLayoutHighDpiRegressionButton.Size = new Size(300, 60); - _anchorLayoutHighDpiRegressionButton.TabIndex = 10; - _anchorLayoutHighDpiRegressionButton.Text = "Anchor Layout"; - _anchorLayoutHighDpiRegressionButton.Click += AnchorLayoutHighDpiRegressionButton_Click; - // - // _anchorLayoutHighDpiRegressionDescriptionLabel - // - _anchorLayoutHighDpiRegressionDescriptionLabel.Location = new Point(210, 442); - _anchorLayoutHighDpiRegressionDescriptionLabel.Name = "_anchorLayoutHighDpiRegressionDescriptionLabel"; - _anchorLayoutHighDpiRegressionDescriptionLabel.Size = new Size(300, 40); - _anchorLayoutHighDpiRegressionDescriptionLabel.TabIndex = 11; - _anchorLayoutHighDpiRegressionDescriptionLabel.Text = "Launches the dedicated high-DPI anchor-layout regression repro in a separate process."; - _anchorLayoutHighDpiRegressionDescriptionLabel.TextAlign = ContentAlignment.TopCenter; - // // MainForm // AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(720, 500); - Controls.Add(_anchorLayoutHighDpiRegressionDescriptionLabel); - Controls.Add(_anchorLayoutHighDpiRegressionButton); Controls.Add(_statusBarDescriptionLabel); Controls.Add(_statusBarButton); Controls.Add(_toolBarLabel); diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs index 83dd13d17d0..103d772c650 100644 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs @@ -1,4 +1,4 @@ -using System.Windows.Forms; +using System.Windows.Forms; namespace Demo; @@ -32,10 +32,4 @@ private void StatusBarButton_Click(object? sender, EventArgs e) StatusBarForm form = new(); form.Show(this); } - - private void AnchorLayoutHighDpiRegressionButton_Click(object? sender, EventArgs e) - { - AnchorLayoutHighDpiRegressionDemoForm form = new(); - form.Show(this); - } } diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/Layout/AnchorLayoutHighDpiRegressionTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/Layout/AnchorLayoutHighDpiRegressionTests.cs deleted file mode 100644 index b2d9ce0c799..00000000000 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/Layout/AnchorLayoutHighDpiRegressionTests.cs +++ /dev/null @@ -1,739 +0,0 @@ -// 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.Reflection; -using System.Windows.Forms.Layout; -using System.Windows.Forms.Primitives; - - -namespace System.Windows.Forms.Legacy.Tests; - -/// -/// Issue-specific regression tests for WI00955507. -/// -/// -/// -/// These tests stay focused on the real CargoWise regression shape: a bottom/right-anchored -/// text box inside a group box inside a tab page, under high DPI, with transient parent -/// geometry during anchor capture. -/// -/// -/// See anchor-layout-highDpi-regression.md for the full analysis. -/// -/// -public class AnchorLayoutHighDpiRegressionTests -{ - private const string AnchorLayoutV2SwitchName = "System.Windows.Forms.AnchorLayoutV2"; - private static readonly int s_scaleDpi = GetScaleDpi(); - - private readonly ITestOutputHelper _output; - - public AnchorLayoutHighDpiRegressionTests(ITestOutputHelper output) - { - _output = output; - } - - /// - /// Sets the AnchorLayoutV2 AppContext switch and resets the - /// cached field so the new value is - /// actually observed — without relying on TestSwitch.LocalAppContext.DisableCaching - /// in a runtimeconfig file. - /// - private static void SetAnchorLayoutV2Switch(bool value) - { - // LocalAppContextSwitches caches switch values in a private static int field - // (0 = uncached, 1 = true, -1 = false). Reset it to 0 so the next access - // re-reads the value from AppContext rather than short-circuiting from cache. - typeof(LocalAppContextSwitches) - .GetField("s_anchorLayoutV2", BindingFlags.NonPublic | BindingFlags.Static)! - .SetValue(null, 0); - - AppContext.SetSwitch(AnchorLayoutV2SwitchName, value); - } - - private static int ScaleLogicalPixels(int value, float dpi) => (int)Math.Round(value * dpi / 96f); - - private static int ScaleLogicalPixels(int value) => (int)Math.Round(value * s_scaleDpi / 96f); - - private static Point ScaledPoint(int x, int y) => new(ScaleLogicalPixels(x), ScaleLogicalPixels(y)); - - private static Size ScaledSize(int width, int height) => new(ScaleLogicalPixels(width), ScaleLogicalPixels(height)); - - private static int GetScaleDpi() - { - using Graphics graphics = Graphics.FromHwnd(IntPtr.Zero); - - return (int)Math.Round(graphics.DpiX); - } - - /// - /// Integration-level regression: replicates the OtherReferenceTextBox scenario from - /// DefaultLayoutTest.cs (WI00955507) using standard WinForms controls. - /// A bottom/right-anchored inside a inside a - /// must remain fully contained within the group box after the form - /// is shown and layout is complete. - /// - [StaFact] - public void BottomRightAnchoredTextBoxInsideGroupBoxInsideTabPage_WithAnchorLayoutV2_RemainsWithinGroupBoxAfterFormIsShown() - { - SetAnchorLayoutV2Switch(true); - try - { - // Sizes chosen so that the textbox's design-time Bottom does NOT exceed the - // group box's final client height, which avoids a permanent deferral. - // This mirrors the net48 geometry from the CargoWise investigation. - using Form form = new() { ClientSize = new Size(400, 430) }; - using TabControl tabControl = new() { Dock = DockStyle.Fill }; - using TabPage tabPage = new(); - using GroupBox groupBox = new() - { - Location = new Point(7, 3), - Size = new Size(324, 240), - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left - }; - using TextBox otherRef = new() - { - // Anchor = Bottom | Left | Right — must float to the bottom of the group box. - Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Location = new Point(6, 142), - Size = new Size(310, 88), - Multiline = true - }; - - groupBox.Controls.Add(otherRef); - tabPage.Controls.Add(groupBox); - tabControl.TabPages.Add(tabPage); - tabControl.SelectedTab = tabPage; - form.Controls.Add(tabControl); - - form.Show(); - Application.DoEvents(); - form.PerformLayout(); - Application.DoEvents(); - form.Close(); - - Assert.True( - groupBox.ClientRectangle.Contains(otherRef.Bounds), - $"OtherRef should be fully inside GroupBox. " - + $"GroupBox.ClientRectangle={groupBox.ClientRectangle}, OtherRef.Bounds={otherRef.Bounds}"); - - Assert.True(otherRef.Visible, "OtherRef should be visible after form is shown."); - } - finally - { - SetAnchorLayoutV2Switch(false); - } - } - - /// - /// Mirrors the authoritative CargoWise initialization order under the V1 path using - /// standard WinForms controls plus a GroupBox whose - /// matches the hidden-caption semantics of the production host. - /// - [StaFact] - public void BottomRightAnchoredTextBoxInsideGroupBoxInsideTabPage_V1Path_CargoWiseGeometry_ShouldRemainWithinGroupBoxAfterFormIsShown() - { - bool setHighDpiModeResult = Application.SetHighDpiMode(HighDpiMode.SystemAware); - _output.WriteLine($"{nameof(AnchorLayoutHighDpiRegressionTests)}: SetHighDpiMode(SystemAware)={setHighDpiModeResult}, HighDpiMode={Application.HighDpiMode}"); - - using Form form = new() { ClientSize = ScaledSize(500, 560) }; - using TabControl tabControl = new() - { - Dock = DockStyle.Fill, - MinimumSize = ScaledSize(480, 470), - Size = ScaledSize(480, 480) - }; - using TabPage tabPage = new() - { - Location = ScaledPoint(4, 23), - Padding = new Padding(ScaleLogicalPixels(3)), - Size = ScaledSize(480, 323) - }; - using CargoWiseLikeGroupBox groupBox = new(); - using TextBox lastPortDate = new() - { - Location = ScaledPoint(133, 71), - Size = ScaledSize(183, 20), - TabIndex = 2 - }; - using TextBox conditionsOfSale = new() - { - Location = ScaledPoint(133, 97), - Size = ScaledSize(183, 20), - TabIndex = 3 - }; - using TextBox termsOfPayment = new() - { - Location = ScaledPoint(133, 123), - Size = ScaledSize(183, 20), - TabIndex = 4 - }; - using CheckBox servicesIndicator = new() - { - Text = "Goods/Services Indicator", - Location = ScaledPoint(133, 149), - AutoSize = true - }; - using CheckBox royaltyIndicator = new() - { - Text = "Royalty Payment Indicator", - Location = ScaledPoint(133, 169), - AutoSize = true - }; - using TextBox otherRef = new() - { - Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Location = ScaledPoint(6, 142), - Size = ScaledSize(310, 88), - Multiline = true, - ScrollBars = ScrollBars.Vertical, - TabIndex = 7 - }; - - groupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; - groupBox.Location = ScaledPoint(7, 3); - groupBox.Size = ScaledSize(324, 240); - groupBox.TabStop = false; - - tabPage.Controls.Add(groupBox); - tabControl.TabPages.Add(tabPage); - - groupBox.Controls.Add(lastPortDate); - groupBox.Controls.Add(conditionsOfSale); - groupBox.Controls.Add(termsOfPayment); - groupBox.Controls.Add(servicesIndicator); - groupBox.Controls.Add(royaltyIndicator); - groupBox.Controls.Add(otherRef); - - tabControl.SelectedTab = tabPage; - form.Controls.Add(tabControl); - - form.Show(); - Application.DoEvents(); - form.PerformLayout(); - Application.DoEvents(); - - using Graphics graphics = form.CreateGraphics(); - _output.WriteLine($"{nameof(AnchorLayoutHighDpiRegressionTests)}: GraphicsDpiX={graphics.DpiX}, GraphicsDpiY={graphics.DpiY}"); - - int expectedY = groupBox.ClientRectangle.Height - ScaleLogicalPixels(10, graphics.DpiY) - otherRef.Bounds.Height; - _output.WriteLine($"Post-show: GroupBox.ClientRectangle={groupBox.ClientRectangle}, GroupBox.DisplayRectangle={groupBox.DisplayRectangle}, OtherRef.Bounds={otherRef.Bounds}, ExpectedY={expectedY}, ServicesIndicator={servicesIndicator.Bounds}, RoyaltyIndicator={royaltyIndicator.Bounds}"); - - Assert.True( - groupBox.ClientRectangle.Contains(otherRef.Bounds), - $"CargoWise geometry: OtherRef should be fully inside GroupBox. " - + $"GroupBox.ClientRectangle={groupBox.ClientRectangle}, OtherRef.Bounds={otherRef.Bounds}"); - - Assert.True( - groupBox.DisplayRectangle.Contains(otherRef.Bounds), - $"CargoWise geometry: OtherRef should be fully inside GroupBox display area. " - + $"GroupBox.DisplayRectangle={groupBox.DisplayRectangle}, OtherRef.Bounds={otherRef.Bounds}"); - - Assert.Equal(expectedY, otherRef.Location.Y); - - Assert.False( - otherRef.Bounds.IntersectsWith(servicesIndicator.Bounds), - $"CargoWise geometry: OtherRef should not overlap ServicesIndicator. " - + $"ServicesIndicator={servicesIndicator.Bounds}, OtherRef={otherRef.Bounds}"); - - Assert.False( - otherRef.Bounds.IntersectsWith(royaltyIndicator.Bounds), - $"CargoWise geometry: OtherRef should not overlap RoyaltyIndicator. " - + $"RoyaltyIndicator={royaltyIndicator.Bounds}, OtherRef={otherRef.Bounds}"); - - Assert.True( - otherRef.Bounds.Top >= royaltyIndicator.Bounds.Bottom, - $"CargoWise geometry: OtherRef should be below RoyaltyIndicator. " - + $"RoyaltyIndicator={royaltyIndicator.Bounds}, OtherRef={otherRef.Bounds}"); - - Assert.Equal(ScaleLogicalPixels(6, graphics.DpiX), otherRef.Location.X); - - form.Close(); - } - - [StaFact] - public void ShouldRefreshAnchorInfoForStalePositiveAnchors_BottomAnchoredTrailingAxis_ReturnsTrue() - { - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 10, - top: 70, - right: -190, - bottom: 170, - displayRectangle: new Rectangle(0, 0, 300, 100)); - Rectangle bounds = new(10, 120, 100, 50); - Rectangle currentDisplayRectangle = new(0, 0, 300, 200); - - bool shouldRefresh = ShouldRefreshAnchorInfoForStalePositiveAnchors( - anchorInfo, - bounds, - currentDisplayRectangle, - AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right); - - Assert.True( - shouldRefresh, - $"A trailing-only bottom anchor with a positive cached bottom offset should be refreshed. " - + $"Bounds={bounds}, DisplayRectangle={currentDisplayRectangle}, CachedBottom={anchorInfo.Bottom}"); - } - - [StaFact] - public void ShouldRefreshAnchorInfoForStalePositiveAnchors_StretchAnchoredVerticalAxis_ReturnsFalse() - { - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 8, - top: 0, - right: 656, - bottom: 6, - displayRectangle: new Rectangle(6, 6, 788, 474)); - Rectangle bounds = new(14, 6, 648, 766); - Rectangle currentDisplayRectangle = new(6, 6, 772, 760); - - bool shouldRefresh = ShouldRefreshAnchorInfoForStalePositiveAnchors( - anchorInfo, - bounds, - currentDisplayRectangle, - AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left); - - Assert.False( - shouldRefresh, - $"A stretch-anchored vertical axis should not be treated as a stale positive bottom anchor. " - + $"Bounds={bounds}, DisplayRectangle={currentDisplayRectangle}, CachedBottom={anchorInfo.Bottom}"); - } - - [StaFact] - public void ShouldRefreshAnchorInfoForStalePositiveAnchors_RightAnchoredTrailingAxis_ReturnsTrue() - { - // Symmetric horizontal-axis case: Right without Left, stale positive right offset. - // Captured right = 140 against a parent width of 100 (transient), parent has since grown to 300. - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 190, - top: 10, - right: 140, - bottom: -40, - displayRectangle: new Rectangle(0, 0, 100, 200)); - Rectangle bounds = new(190, 10, 90, 40); - Rectangle currentDisplayRectangle = new(0, 0, 300, 200); - - bool shouldRefresh = ShouldRefreshAnchorInfoForStalePositiveAnchors( - anchorInfo, - bounds, - currentDisplayRectangle, - AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom); - - Assert.True( - shouldRefresh, - $"A trailing-only right anchor with a positive cached right offset should be refreshed. " - + $"Bounds={bounds}, DisplayRectangle={currentDisplayRectangle}, CachedRight={anchorInfo.Right}"); - } - - /// - /// Fast local regression for the current CargoWise-only issue: a stretch-anchored parent is - /// recovered once from its original captured display rectangle, then is incorrectly eligible - /// for a second stale-positive refresh that overwrites the recovered anchors. - /// - [StaFact] - public void StretchAnchoredGroupBox_RecoveredStretchAnchor_ShouldNotTriggerFollowUpPositiveRefresh() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(8, 40, 784, 772), - SimulatedDisplayRectangle = new Rectangle(6, 6, 772, 760) - }; - using CargoWiseLikeGroupBox groupBox = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left, - Bounds = new Rectangle(14, 6, 648, 100) - }; - - parent.Controls.Add(groupBox); - - SetSpecifiedBounds(parent, new Rectangle(8, 40, 784, 772)); - SetSpecifiedBounds(groupBox, new Rectangle(14, 6, 648, 480)); - - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo(8, 0, 656, -374, new Rectangle(6, 6, 788, 474)); - Rectangle currentDisplayRectangle = parent.DisplayRectangle; - - RefreshAnchorInfoForDisplayRectangleGrowth(groupBox, anchorInfo, currentDisplayRectangle, groupBox.Anchor, isStretchAnchorRefresh: true); - - Rectangle recoveredBounds = ComputeAnchoredBounds(anchorInfo, currentDisplayRectangle, groupBox.Anchor); - groupBox.Bounds = recoveredBounds; - SetSpecifiedBounds(groupBox, new Rectangle(14, 6, 648, 480)); - - bool shouldRefreshAgain = ShouldRefreshAnchorInfoForStalePositiveAnchors(anchorInfo, recoveredBounds, currentDisplayRectangle, groupBox.Anchor); - - _output.WriteLine($"Recovered stretch anchor bounds={recoveredBounds}, shouldRefreshAgain={shouldRefreshAgain}, recoveredBottom={anchorInfo.Bottom}, recoveredDisplayRect={anchorInfo.DisplayRectangle}"); - - Assert.False( - shouldRefreshAgain, - $"Recovered stretch anchor should not trigger a follow-up stale-positive refresh. " - + $"RecoveredBounds={recoveredBounds}, RecoveredBottom={anchorInfo.Bottom}, RecoveredDisplayRect={anchorInfo.DisplayRectangle}"); - } - - [StaFact] - public void StretchAnchoredControl_WithOversizedPositiveOffsets_RefreshesUsingSpecifiedDisplayRectangle() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 1458, 712), - SimulatedDisplayRectangle = new Rectangle(0, 0, 1458, 712) - }; - using CargoWiseLikeGroupBox stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(3, 160, 1530, 840) - }; - - parent.Controls.Add(stretchControl); - - // Designer-time (specified) geometry captured by the layout engine. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 651, 474)); - SetSpecifiedBounds(stretchControl, new Rectangle(3, 160, 645, 311)); - - // Stale V1 anchor info: positive trailing offsets make the control larger than parent. - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 3, - top: 160, - right: 75, - bottom: 50, - displayRectangle: new Rectangle(0, 0, 576, 400)); - - Rectangle currentDisplayRectangle = parent.DisplayRectangle; - - RefreshAnchorInfoForDisplayRectangleGrowth( - stretchControl, - anchorInfo, - currentDisplayRectangle, - stretchControl.Anchor, - isStretchAnchorRefresh: true, - useSpecifiedDisplayRectangleForStretchRefresh: true); - - Rectangle refreshedBounds = ComputeAnchoredBounds(anchorInfo, currentDisplayRectangle, stretchControl.Anchor); - - Assert.True(anchorInfo.Right < 0, $"Expected negative right anchor after refresh but got {anchorInfo.Right}."); - Assert.True(anchorInfo.Bottom < 0, $"Expected negative bottom anchor after refresh but got {anchorInfo.Bottom}."); - Assert.True( - parent.DisplayRectangle.Contains(refreshedBounds), - $"Refreshed stretch bounds should fit in parent display area. " - + $"ParentDisplayRect={parent.DisplayRectangle}, RefreshedBounds={refreshedBounds}"); - } - - [StaFact] - public void StretchAnchoredControl_WithSpecifiedBoundsMuchLargerThanBaseline_UsesCachedBoundsDuringStretchRefresh() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 1526, 453), - SimulatedDisplayRectangle = new Rectangle(0, 0, 1526, 453) - }; - using CargoWiseLikeGroupBox stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(7, 400, 1477, 0) - }; - - parent.Controls.Add(stretchControl); - - // CargoWise trace shape: runtime bounds fit the baseline while specified bounds are much larger. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 1492, 405)); - SetSpecifiedBounds(stretchControl, new Rectangle(7, 400, 1612, 777)); - - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 7, - top: 400, - right: -8, - bottom: -5, - displayRectangle: new Rectangle(0, 0, 1492, 405)); - - RefreshAnchorInfoForDisplayRectangleGrowth( - stretchControl, - anchorInfo, - parent.DisplayRectangle, - stretchControl.Anchor, - isStretchAnchorRefresh: true, - useSpecifiedDisplayRectangleForStretchRefresh: false); - - Rectangle refreshedBounds = ComputeAnchoredBounds(anchorInfo, parent.DisplayRectangle, stretchControl.Anchor); - - Assert.True( - anchorInfo.Right < 0, - $"Expected negative right anchor after refresh but got {anchorInfo.Right}."); - Assert.True( - anchorInfo.Bottom < 0, - $"Expected negative bottom anchor after refresh but got {anchorInfo.Bottom}."); - Assert.True( - parent.DisplayRectangle.Contains(refreshedBounds), - $"Refreshed stretch bounds should fit in parent display area. " - + $"ParentDisplayRect={parent.DisplayRectangle}, RefreshedBounds={refreshedBounds}"); - } - - [StaFact] - public void StretchAnchoredControl_WithStaleSpecifiedDisplayRectangle_DoesNotRefreshStretchAnchors() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 1492, 405), - SimulatedDisplayRectangle = new Rectangle(0, 0, 1492, 405) - }; - using CargoWiseLikeGroupBox stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(7, 400, 1477, 0) - }; - - parent.Controls.Add(stretchControl); - - // Stale specified metadata still points at the pre-shrink baseline. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 1627, 1185)); - SetSpecifiedBounds(stretchControl, new Rectangle(7, 400, 1612, 777)); - - DefaultLayout.SetAnchorInfo( - stretchControl, - CreateAnchorInfo( - left: 7, - top: 400, - right: -8, - bottom: -5, - displayRectangle: new Rectangle(0, 0, 1492, 405))); - - parent.Bounds = new Rectangle(0, 0, 1526, 453); - parent.SimulatedDisplayRectangle = new Rectangle(0, 0, 1526, 453); - - parent.PerformLayout(); - - DefaultLayout.AnchorInfo refreshedAnchorInfo = DefaultLayout.GetAnchorInfo(stretchControl)!; - - Assert.Equal(-8, refreshedAnchorInfo.Right); - Assert.Equal(-5, refreshedAnchorInfo.Bottom); - Assert.Equal(new Rectangle(7, 400, 1511, 48), stretchControl.Bounds); - } - - [StaFact] - public void StretchAnchoredControl_WithHealthyNegativeTrailingAnchor_StretchesWithoutRefreshingAnchors() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 1492, 405), - SimulatedDisplayRectangle = new Rectangle(0, 0, 1492, 405) - }; - using Panel stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(7, 175, 1477, 217) - }; - - parent.Controls.Add(stretchControl); - - // Baseline metadata is healthy; a growth pass should stretch using existing anchors. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 1492, 405)); - SetSpecifiedBounds(stretchControl, new Rectangle(7, 175, 1477, 217)); - - DefaultLayout.SetAnchorInfo( - stretchControl, - CreateAnchorInfo( - left: 7, - top: 175, - right: -8, - bottom: 392, - displayRectangle: new Rectangle(0, 0, 1492, 405))); - - parent.Bounds = new Rectangle(0, 0, 1526, 453); - parent.SimulatedDisplayRectangle = new Rectangle(0, 0, 1526, 453); - - parent.PerformLayout(); - - DefaultLayout.AnchorInfo refreshedAnchorInfo = DefaultLayout.GetAnchorInfo(stretchControl)!; - - Assert.Equal(-8, refreshedAnchorInfo.Right); - Assert.Equal(392, refreshedAnchorInfo.Bottom); - Assert.Equal(new Rectangle(7, 175, 1511, 217), stretchControl.Bounds); - } - - [StaFact] - public void StretchAnchoredControl_WithStaleSpecifiedSizeAndHealthyAnchors_DoesNotRefreshAnchors() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 1492, 405), - SimulatedDisplayRectangle = new Rectangle(0, 0, 1492, 405) - }; - using Panel stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(7, 175, 1477, 217) - }; - - parent.Controls.Add(stretchControl); - - // Parent specified metadata is current, but child specified size is stale and oversized. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 1492, 405)); - SetSpecifiedBounds(stretchControl, new Rectangle(7, 175, 1612, 217)); - - DefaultLayout.SetAnchorInfo( - stretchControl, - CreateAnchorInfo( - left: 7, - top: 175, - right: -8, - bottom: 392, - displayRectangle: new Rectangle(0, 0, 1492, 405))); - - parent.Bounds = new Rectangle(0, 0, 1526, 453); - parent.SimulatedDisplayRectangle = new Rectangle(0, 0, 1526, 453); - - parent.PerformLayout(); - - DefaultLayout.AnchorInfo refreshedAnchorInfo = DefaultLayout.GetAnchorInfo(stretchControl)!; - - Assert.Equal(-8, refreshedAnchorInfo.Right); - Assert.Equal(392, refreshedAnchorInfo.Bottom); - Assert.Equal(new Rectangle(7, 175, 1511, 217), stretchControl.Bounds); - } - - [StaFact] - public void StretchAnchoredControl_WithOversizedPositiveOffsetsAndNoDisplayGrowth_RefreshesUsingSpecifiedDisplayRectangle() - { - using StretchTestContainer parent = new() - { - Bounds = new Rectangle(0, 0, 576, 400), - SimulatedDisplayRectangle = new Rectangle(0, 0, 576, 400) - }; - using CargoWiseLikeGroupBox stretchControl = new() - { - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Bounds = new Rectangle(3, 160, 645, 311) - }; - - parent.Controls.Add(stretchControl); - - // Designer-time (specified) geometry from the CargoWise layout shape. - SetSpecifiedBounds(parent, new Rectangle(0, 0, 651, 474)); - SetSpecifiedBounds(stretchControl, new Rectangle(3, 160, 645, 311)); - - // Stale V1 anchors captured against the current display rectangle already contain - // positive trailing offsets, so there is no display-rectangle growth signal. - DefaultLayout.AnchorInfo anchorInfo = CreateAnchorInfo( - left: 3, - top: 160, - right: 72, - bottom: 71, - displayRectangle: parent.DisplayRectangle); - - Rectangle currentDisplayRectangle = parent.DisplayRectangle; - - RefreshAnchorInfoForDisplayRectangleGrowth( - stretchControl, - anchorInfo, - currentDisplayRectangle, - stretchControl.Anchor, - isStretchAnchorRefresh: true, - useSpecifiedDisplayRectangleForStretchRefresh: true); - - Rectangle refreshedBounds = ComputeAnchoredBounds(anchorInfo, currentDisplayRectangle, stretchControl.Anchor); - - Assert.True( - anchorInfo.Right < 0, - $"Expected negative right anchor after refresh but got {anchorInfo.Right}."); - Assert.True( - anchorInfo.Bottom < 0, - $"Expected negative bottom anchor after refresh but got {anchorInfo.Bottom}."); - Assert.True( - parent.DisplayRectangle.Contains(refreshedBounds), - $"Refreshed stretch bounds should fit in parent display area. " - + $"ParentDisplayRect={parent.DisplayRectangle}, RefreshedBounds={refreshedBounds}"); - } - - private static DefaultLayout.AnchorInfo CreateAnchorInfo(int left, int top, int right, int bottom, Rectangle displayRectangle) => - new() - { - Left = left, - Top = top, - Right = right, - Bottom = bottom, - DisplayRectangle = displayRectangle - }; - - private static void SetSpecifiedBounds(Control control, Rectangle bounds) => - CommonProperties.UpdateSpecifiedBounds(control, bounds.X, bounds.Y, bounds.Width, bounds.Height); - - private static bool ShouldRefreshAnchorInfoForStalePositiveAnchors(DefaultLayout.AnchorInfo anchorInfo, Rectangle bounds, Rectangle displayRectangle, AnchorStyles anchor) => - DefaultLayout.ShouldRefreshAnchorInfoForStalePositiveAnchors(anchorInfo, bounds, displayRectangle, anchor); - - private static void RefreshAnchorInfoForDisplayRectangleGrowth( - Control control, - DefaultLayout.AnchorInfo anchorInfo, - Rectangle displayRectangle, - AnchorStyles anchor, - bool isStretchAnchorRefresh, - bool useSpecifiedDisplayRectangleForStretchRefresh = false) => - DefaultLayout.RefreshAnchorInfoForDisplayRectangleGrowth( - control, - anchorInfo, - displayRectangle, - anchor, - isStretchAnchorRefresh, - useSpecifiedDisplayRectangleForStretchRefresh); - - private static Rectangle ComputeAnchoredBounds(DefaultLayout.AnchorInfo anchorInfo, Rectangle displayRectangle, AnchorStyles anchor) - { - int left = anchorInfo.Left + displayRectangle.X; - int top = anchorInfo.Top + displayRectangle.Y; - int right = anchorInfo.Right + displayRectangle.X; - int bottom = anchorInfo.Bottom + displayRectangle.Y; - - if ((anchor & AnchorStyles.Right) != AnchorStyles.None) - { - right += displayRectangle.Width; - - if ((anchor & AnchorStyles.Left) == AnchorStyles.None) - { - left += displayRectangle.Width; - } - } - else if ((anchor & AnchorStyles.Left) == AnchorStyles.None) - { - int center = displayRectangle.Width / 2; - right += center; - left += center; - } - - if ((anchor & AnchorStyles.Bottom) != AnchorStyles.None) - { - bottom += displayRectangle.Height; - - if ((anchor & AnchorStyles.Top) == AnchorStyles.None) - { - top += displayRectangle.Height; - } - } - else if ((anchor & AnchorStyles.Top) == AnchorStyles.None) - { - int center = displayRectangle.Height / 2; - bottom += center; - top += center; - } - - if (right < left) - { - right = left; - } - - if (bottom < top) - { - bottom = top; - } - - return new Rectangle(left, top, right - left, bottom - top); - } - - private sealed class CargoWiseLikeGroupBox : GroupBox - { - public override Rectangle DisplayRectangle => ClientRectangle; - } - - private sealed class StretchTestContainer : Panel - { - public Rectangle SimulatedDisplayRectangle { get; set; } - - public override Rectangle DisplayRectangle => SimulatedDisplayRectangle; - } -} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj index 359cb207ff6..81e02fec083 100644 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj @@ -10,7 +10,7 @@ 0.1.0 $(WtgPackageVersionPrefix)-dev WiseTech Global forked version of System.Windows.Forms that includes controls that were deprecated in .NET Core 3.1 and .NET 5, like DataGrid, Menu, ToolBar and StatusBar. - Rollback custom binding context support in BindingContext/RelatedCurrencyManager, PR link: https://github.com/WiseTechGlobal/winforms/pull/20 + Remove IsScalingRequirementMet from DefaultLayout.cs, remove the DefaultLayout.AnchorLayoutCompat.cs file, PR link: https://github.com/WiseTechGlobal/winforms/pull/38 WiseTech Global © WiseTech Global Limited. All rights reserved. README.md diff --git a/src/System.Windows.Forms.Legacy/anchor-layout-highDpi-regression.md b/src/System.Windows.Forms.Legacy/anchor-layout-highDpi-regression.md deleted file mode 100644 index e8c2a57409e..00000000000 --- a/src/System.Windows.Forms.Legacy/anchor-layout-highDpi-regression.md +++ /dev/null @@ -1,358 +0,0 @@ -# Anchor Layout High-DPI Regression: .NET Framework vs .NET 8+ - -Controls anchored to `Bottom` or `Right` can appear displaced or invisible in .NET 8+ on any -monitor scaled above 100% DPI, while the same code is correct under .NET Framework 4.8. - -The bug lives entirely in the **V1 anchor path** (`UpdateAnchorInfo` plus the compat repair in -`ComputeAnchoredBounds`) and does **not** affect `AnchorLayoutV2`. - -Two separate V1 compat failures exist in the .NET 10 path: - -- A **stale positive anchor capture** for controls anchored only to trailing edges (`Bottom` - without `Top`, or `Right` without `Left`). -- A **stretch-anchor recovery double-refresh** where a stretch-anchored parent is recovered from - the wrong baseline and then immediately re-qualifies for the stale-positive repair on the same - axis, collapsing the recovered height. - -`AnchorLayoutV2` eliminates the root cause (see [Solution 1](#solution-1--enable-anchorlayoutv2-recommended)). -The **fork compat repair** resolves both failures at the framework level without requiring an application -change (see [Solution 3](#solution-3--additive-layout-side-repair-current-fork-implementation)). - ---- - -## Problem Description - -### Symptoms - -On any machine where at least one monitor is scaled above 100% DPI (e.g. 200%), controls with -`Anchor` involving `Bottom` or `Right` inside deeply nested containers (e.g. `GroupBox` → -`TabPage` → `TabControl`) are positioned far outside their parent's client area when the form -becomes visible — or are completely invisible. - -Characteristic signs: - -| Observation | Detail | -|---|---| -| Control appears below visible area | `Bounds.Bottom` greatly exceeds `ClientRectangle.Height` | -| Problem is DPI-dependent | Invisible at 100% DPI, severe at 200%+ | -| Problem is already present before `Show()` | The bad position is set during form construction | -| The stale-anchor bug is .NET 8+ specific | The bad positive anchor capture does not occur under .NET Framework 4.8 | - -### Reproduction - -The displacement is proportional to the DPI scale factor. At 200% DPI, the child control's -coordinates are already DPI-scaled at the time of anchor capture, but the parent's -`DisplayRectangle` is still at a transient construction-time size. For example: - -``` -parent ClientRectangle.Height = 100 ← transient height during construction -control bounds (DPI-scaled) = {Y=284, Height=176} -bottomOffset = (284 + 176) − 100 = +360 ← positive: stale capture detected -→ final Y = finalParentHeight + 360 − 176 ← control far below visible area -``` - -The transient parent height occurs when a child control is added to a container **after** that -container has been inserted into a deeper hierarchy, but before any top-level form is attached. -At that moment the layout engine assigns a temporary, too-small height to the container, and the -V1 path captures the anchor offset against that height. - ---- - -## Root Cause - -### The Stable Behavior in .NET Framework 4.8 - -In .NET Framework, the `EnableAnchorLayoutHighDpiImprovements` switch defaults to `false`. The -legacy `ComputeAnchoredBounds` path always stores `Right` and `Bottom` as **negative distances** -from the parent's edges: - -``` -anchorInfo.Right = elementBounds.Right - parentWidth // e.g. −20 (20 px from right edge) -anchorInfo.Bottom = elementBounds.Bottom - parentHeight // e.g. −12 (12 px from bottom edge) -``` - -These negative offsets are stable regardless of when they are captured and are simply added to -the live `displayRect` dimensions at layout time, placing the control correctly. - -### The Broken Path in .NET 8+ - -`EnableAnchorLayoutHighDpiImprovements` no longer exists. The DPI-aware branch is now gated on -`ScaleHelper.IsScalingRequirementMet`, which returns `true` whenever any monitor is above 96 dpi -(100%) or the process is per-monitor-aware: - -```csharp -// ScaleHelper.cs -internal static bool IsScalingRequirementMet => IsScalingRequired || s_processPerMonitorAware; -internal static bool IsScalingRequired => InitialSystemDpi != OneHundredPercentLogicalDpi; -``` - -When `IsScalingRequirementMet` is `true`, `UpdateAnchorInfo` (V1) takes a special branch intended -to rescue controls that were pushed off-screen by a DPI-scale event: - -```csharp -if (IsAnchored(anchor, AnchorStyles.Right)) -{ - if (ScaleHelper.IsScalingRequirementMet - && (anchorInfo.Right - parentWidth > 0) // control appears beyond right edge - && (oldAnchorInfo.Right < 0)) // but had a valid negative anchor before - { - // Preserve old anchor to avoid losing the control off the right edge. - anchorInfo.Right = oldAnchorInfo.Right; - } - else - { - anchorInfo.Right -= parentWidth; // standard negative-distance calculation - } -} -``` - -**The guard fires incorrectly during initial form construction.** When a child control is added -while its parent already has a transient (too-small) `DisplayRectangle`, these conditions hold: - -1. The parent height is transiently small (e.g. `100`). -2. The control's `Y` coordinate is already DPI-scaled (e.g. `284` at 200%). -3. `anchorInfo.Bottom - parentHeight = (284 + 176) − 100 = +360 > 0` — the guard fires. -4. `oldAnchorInfo` is also invalid, so the preserved offset is wrong. - -On the next layout pass, this wrong positive offset is applied against the final (larger) parent -height, displacing the control far outside the visible area. - -### What the Compat Repair Fixes - -The compat repair in `ComputeAnchoredBounds` detects a stale positive trailing anchor and -recomputes it against a stable baseline. After the repair, the stale positive `Bottom` (or -`Right`) value is converted back into a stable negative margin. The control is then placed -correctly relative to the parent's final size. - -### The Stretch-Anchor Double-Refresh - -A second failure can occur on the same layout pass when a container is itself stretch-anchored -(`Top | Bottom` or `Left | Right`): - -1. The stretch anchor is recovered against the growth in the parent display rectangle. -2. After recovery, the `Bottom` (or `Right`) value is positive by design for a stretch-anchored - control — satisfying the stale-positive check. -3. A second refresh would then overwrite the recovered stretch anchors, shrinking the container - back down. - -This is not a separate layout system defect. It is the result of the compat repair firing twice -with different semantics on the same axis. The fix evaluates the stretch check first and -short-circuits the positive check when the stretch path fires (see Solution 3). - -### Why High DPI Amplifies the Error - -At 200% DPI all coordinates are approximately doubled. A moderate anchor offset error (e.g. +180) -becomes a large one (+360), pushing the control well outside any visible area. At 100% DPI the -same error is small enough to be nearly invisible. - -### Contributing Factors Summary - -| Factor | Role | -|---|---| -| `ScaleHelper.IsScalingRequirementMet` | Activates the DPI-aware branch for any non-100% DPI monitor | -| `UpdateAnchorInfo` V1 guard condition | Fires incorrectly during construction against a transient parent size | -| `ControlDpiScalingHelper` coordinate scaling | Multiplies all coordinates by the DPI factor, amplifying the captured error | -| Stretch-anchor follow-up refresh | Reapplies stale-positive logic on an axis already recovered as a stretch anchor, shrinking the parent again | - ---- - -## Solutions - -Three independent approaches exist, ordered from most to least recommended. - -### Decision Guide - -``` -Are you on .NET 8+ and can change the app configuration? - └─ Yes → Solution 1 (AnchorLayoutV2) — eliminates the root cause, zero code change -Are you unable to change configuration (e.g. third-party host)? - └─ Solution 3 (fork additive repair) — no app change required, smallest source delta -Do you need a workaround with no source changes at all? - └─ Solution 2 (DpiUnaware) — loses DPI sharpness on scaled monitors -``` - ---- - -### Solution 1 — Enable `AnchorLayoutV2` ✅ Recommended - -`AnchorLayoutV2` (introduced in .NET 8, opt-in) replaces the over-eager V1 capture with a -**deferred model**: anchor offsets are not committed until the parent's layout is resumed and its -`DisplayRectangle` is stable. This eliminates the transient-capture problem at the source. - -Enable it in `runtimeconfig.template.json` (or `runtimeconfig.json`): - -```json -{ - "configProperties": { - "System.Windows.Forms.AnchorLayoutV2": true - } -} -``` - -| Attribute | Value | -|---|---| -| Risk | Low — opt-in, no source change required | -| Scope | Per-application configuration | -| Verified | Yes — eliminates the transient-capture path entirely | -| Limitation | Requires the application to control its own `runtimeconfig` | - ---- - -### Solution 2 — Use `DpiUnaware` or `DpiUnawareGdiScaled` - -Setting the DPI mode to `DpiUnaware` (or `DpiUnawareGdiScaled`) makes -`IsScalingRequirementMet` return `false`, so .NET 8 follows the same stable anchor path as -.NET Framework. - -```csharp -Application.SetHighDpiMode(HighDpiMode.DpiUnaware); -// or -Application.SetHighDpiMode(HighDpiMode.DpiUnawareGdiScaled); -``` - -| Attribute | Value | -|---|---| -| Risk | Low — well-understood mode | -| Scope | Per-application startup | -| Verified | Yes — sidesteps the broken branch entirely | -| Limitation | UI appears blurry on scaled monitors (GDI bitmap stretching) | - ---- - -### Solution 3 — Additive Layout-Side Repair ⭐ Current Fork Implementation - -This is the **current implementation** in the WiseTech Global WinForms fork. It preserves the -original `UpdateAnchorInfo` behavior entirely and **adds a compat repair step** inside -`ComputeAnchoredBounds` that handles both stale positive trailing anchors and stale stretch-anchor -recovery when the parent's `DisplayRectangle` has grown to a stable size. - -**Key design principle:** additive and non-breaking — the existing V1 behavior is unchanged. - -The single call site in `DefaultLayout.cs`, inside `ComputeAnchoredBounds`, is: - -```csharp -// DefaultLayout.cs — ComputeAnchoredBounds -AnchorInfo layout = GetAnchorInfo(element)!; - -// ... read left/top/right/bottom from layout ... - -AnchorStyles anchor = TryRefreshAnchorInfoForDisplayRectangleGrowth(element, layout, displayRect); -``` - -All compat logic lives in `DefaultLayout.AnchorLayoutCompat.cs` (a partial class file) to keep the -change isolated from the main layout file: - -```csharp -// DefaultLayout.AnchorLayoutCompat.cs -private static AnchorStyles TryRefreshAnchorInfoForDisplayRectangleGrowth( - IArrangedElement element, AnchorInfo anchorInfo, Rectangle displayRect) -{ - AnchorStyles anchor = GetAnchor(element); - Rectangle bounds = GetCachedBounds(element); - - // Stretch check must run first. If it triggers, the positive check is skipped so that - // the recovered stretch anchors are not immediately overwritten by a second refresh. - bool shouldRefreshStretchAnchors = - ShouldRefreshAnchorInfoForStaleStretchAnchors(element, anchorInfo, bounds, displayRect, anchor); - - if (!shouldRefreshStretchAnchors - && !ShouldRefreshAnchorInfoForStalePositiveAnchors(anchorInfo, bounds, displayRect, anchor)) - { - return anchor; - } - - RefreshAnchorInfoForDisplayRectangleGrowth(element, anchorInfo, displayRect, anchor, shouldRefreshStretchAnchors); - - return anchor; -} -``` - -**How the repair works:** - -- `ShouldRefreshAnchorInfoForStalePositiveAnchors(AnchorInfo, Rectangle bounds, Rectangle displayRect, AnchorStyles)` - targets controls anchored only to trailing edges (`Right` without `Left`, `Bottom` without - `Top`). A positive trailing offset on such an axis — combined with evidence that the parent's - `displayRect` has grown past the recorded `anchorInfo.DisplayRectangle` — is the signature of a - stale transient capture. -- `ShouldRefreshAnchorInfoForStaleStretchAnchors(IArrangedElement, AnchorInfo, Rectangle bounds, Rectangle displayRect, AnchorStyles)` - covers the complementary case: a stretch-anchored control (`Left | Right` or `Top | Bottom`) - whose actual size is smaller than the size predicted by the specified bounds and the growth in - the parent display rectangle. It uses `GetDisplayRectangleForSpecifiedContainerBounds` to - reconstruct a stable reference rectangle from the container's `SpecifiedBounds`. -- The **stretch check runs before the positive check** (short-circuit `&&`). This is the key - guard against the double-refresh: once the stretch path fires for an axis, the positive path - cannot fire for the same axis in the same layout pass. -- `RefreshAnchorInfoForDisplayRectangleGrowth` delegates to `ResetAnchorInfo`, passing either the - original captured `DisplayRectangle` (stretch recovery) or a rectangle derived from the - container's `SpecifiedBounds` (trailing-edge recovery), then recomputes all four anchor values - so the trailing offsets become stable negative distances again. - -| Attribute | Value | -|---|---| -| Risk | Low — additive only, no existing behavior changed | -| Scope | Framework source change (no app change required) | -| Verified | Stale-anchor and stretch-anchor regression tests pass on `net10.0-windows` at 200% DPI | -| Limitation | Only targets the transient-capture class of failure; does not address other DPI layout edge cases | - ---- - -## Relationship Between Paths - -``` -.NET Framework 4.8 - └─ EnableAnchorLayoutHighDpiImprovements = false (disabled by default) - └─ Always uses legacy ComputeAnchoredBounds — stable, no DPI-aware guard - -.NET 8+ (default, DPI > 100%) - └─ IsScalingRequirementMet = true - └─ V1 UpdateAnchorInfo with DPI-aware guard — fires spuriously during construction - ├─ Solution 2: make IsScalingRequirementMet false (DpiUnaware) - └─ Solution 3: additive layout-side repair in ComputeAnchoredBounds ← current fork - -.NET 8+ with AnchorLayoutV2 = true - └─ UpdateAnchorInfoV2 — defers capture until parent layout is stable - └─ Solution 1: root-cause fix, no source change needed -``` - ---- - -## Verification - -### Manual Testing - -Requires a machine with at least one monitor set to 200% DPI: - -1. Launch the application and open a form with bottom/right-anchored controls inside nested containers. -2. **Without any fix:** the control is far below its parent or invisible. -3. **With Solution 1** (`AnchorLayoutV2`): control is correctly positioned inside the parent's client area. -4. **With Solution 2** (`DpiUnaware`): control is correctly positioned, but UI may appear blurry. -5. **With Solution 3** (fork repair): control is correctly positioned; test host requires no config change. - -### Automated Test Criteria - -For a bottom/right-anchored control inside a nested container at 200% DPI: - -- The control's `Bounds` are fully contained within the parent's `ClientRectangle` after `Show()`. -- The control does not overlap sibling controls that were correctly sized. -- For stretch-anchored containers: the container's height matches the size implied by the - specified bounds and the display-rectangle growth — it is not collapsed by a second refresh. - -### Fork Regression Tests - -| Test | Status | -|---|---| -| `...V1Path_ShouldRemainWithinGroupBoxAfterFormIsShown` | ✅ trailing-edge stale capture (Solution 3) | -| `...WithAnchorLayoutV2_RemainsWithinGroupBoxAfterFormIsShown` | ✅ V2 path unaffected | -| `...StretchAnchoredGroupBox_RecoveredStretchAnchor_ShouldNotTriggerFollowUpPositiveRefresh` | ✅ stretch double-refresh guard | - -No changes to `Control.cs` are required — the fix is entirely within `DefaultLayout.cs` and -`DefaultLayout.AnchorLayoutCompat.cs`. - ---- - -## References - -- [Anchor layout changes in .NET 8.0](https://github.com/dotnet/winforms/blob/main/docs/design/anchor-layout-changes-in-net80.md) -- `src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.cs` — `UpdateAnchorInfo`, `ComputeAnchoredBounds`, `UpdateAnchorInfoV2` -- `src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.AnchorLayoutCompat.cs` — `TryRefreshAnchorInfoForDisplayRectangleGrowth`, `ShouldRefreshAnchorInfoForStalePositiveAnchors`, `ShouldRefreshAnchorInfoForStaleStretchAnchors`, `RefreshAnchorInfoForDisplayRectangleGrowth`, `ResetAnchorInfo` -- `src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs` — `IsScalingRequirementMet` diff --git a/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.AnchorLayoutCompat.cs b/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.AnchorLayoutCompat.cs deleted file mode 100644 index e4e030ff3c2..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.AnchorLayoutCompat.cs +++ /dev/null @@ -1,338 +0,0 @@ -// 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; - -namespace System.Windows.Forms.Layout; - -internal partial class DefaultLayout -{ - private const int MinimumSignificantStretchDelta = 16; - - private static AnchorStyles TryRefreshAnchorInfoForDisplayRectangleGrowth(IArrangedElement element, AnchorInfo anchorInfo, Rectangle displayRect) - { - AnchorStyles anchor = GetAnchor(element); - Rectangle bounds = GetCachedBounds(element); - bool shouldRefreshStretchAnchors = ShouldRefreshAnchorInfoForStaleStretchAnchors( - element, - anchorInfo, - bounds, - displayRect, - anchor, - out bool useSpecifiedDisplayRectangleForStretchRefresh); - bool shouldRefreshPositiveAnchors = ShouldRefreshAnchorInfoForStalePositiveAnchors(anchorInfo, bounds, displayRect, anchor); - - if (!shouldRefreshStretchAnchors - && !shouldRefreshPositiveAnchors) - { - return anchor; - } - - RefreshAnchorInfoForDisplayRectangleGrowth( - element, - anchorInfo, - displayRect, - anchor, - shouldRefreshStretchAnchors, - useSpecifiedDisplayRectangleForStretchRefresh); - - return anchor; - } - - internal static bool ShouldRefreshAnchorInfoForStalePositiveAnchors(AnchorInfo anchorInfo, Rectangle bounds, Rectangle displayRect, AnchorStyles anchor) - { - bool hasStaleRightAnchor = IsAnchored(anchor, AnchorStyles.Right) - && !IsAnchored(anchor, AnchorStyles.Left) - && anchorInfo.Right > 0 - && (bounds.Right - displayRect.X <= displayRect.Width - || displayRect.Width > anchorInfo.DisplayRectangle.Width); - - bool hasStaleBottomAnchor = IsAnchored(anchor, AnchorStyles.Bottom) - && !IsAnchored(anchor, AnchorStyles.Top) - && anchorInfo.Bottom > 0 - && (bounds.Bottom - displayRect.Y <= displayRect.Height - || displayRect.Height > anchorInfo.DisplayRectangle.Height); - - return hasStaleRightAnchor || hasStaleBottomAnchor; - } - - private static bool ShouldRefreshAnchorInfoForStaleStretchAnchors( - IArrangedElement element, - AnchorInfo anchorInfo, - Rectangle bounds, - Rectangle displayRect, - AnchorStyles anchor, - out bool useSpecifiedDisplayRectangleForStretchRefresh) - { - useSpecifiedDisplayRectangleForStretchRefresh = false; - - if (!ScaleHelper.IsScalingRequirementMet || element.Container is not { } container) - { - return false; - } - - Rectangle specifiedContainerBounds = CommonProperties.GetSpecifiedBounds(container); - Rectangle specifiedElementBounds = CommonProperties.GetSpecifiedBounds(element); - - if (!HasValidSpecifiedBounds(specifiedContainerBounds, specifiedElementBounds)) - { - return false; - } - - Rectangle specifiedDisplayRect = GetDisplayRectangleForSpecifiedContainerBounds(container, displayRect, specifiedContainerBounds); - bool hasDisplayRectangleGrowth = HasDisplayRectangleGrowth(anchorInfo.DisplayRectangle, displayRect); - - int effectiveSpecifiedWidth = GetEffectiveSpecifiedSizeForStretchRefresh( - anchorInfo.DisplayRectangle.Width, - anchorInfo.Left, - anchorInfo.Right, - specifiedElementBounds.Width); - int effectiveSpecifiedHeight = GetEffectiveSpecifiedSizeForStretchRefresh( - anchorInfo.DisplayRectangle.Height, - anchorInfo.Top, - anchorInfo.Bottom, - specifiedElementBounds.Height); - - if (hasDisplayRectangleGrowth - && IsSpecifiedDisplayRectangleLikelyStale(specifiedDisplayRect, anchorInfo.DisplayRectangle)) - { - return false; - } - - bool hasStaleHorizontalStretchAnchor = IsAnchored(anchor, AnchorStyles.Left) - && IsAnchored(anchor, AnchorStyles.Right) - && anchorInfo.Right < 0 - && hasDisplayRectangleGrowth - && IsProjectedStretchAnchorSmallerThanExpected( - displayRect.Width, - anchorInfo.Left, - anchorInfo.Right, - effectiveSpecifiedWidth, - specifiedDisplayRect.Width); - - bool hasStaleVerticalStretchAnchor = IsAnchored(anchor, AnchorStyles.Top) - && IsAnchored(anchor, AnchorStyles.Bottom) - && anchorInfo.Bottom < 0 - && hasDisplayRectangleGrowth - && IsProjectedStretchAnchorSmallerThanExpected( - displayRect.Height, - anchorInfo.Top, - anchorInfo.Bottom, - effectiveSpecifiedHeight, - specifiedDisplayRect.Height); - - bool hasOversizedHorizontalStretchAnchor = IsAnchored(anchor, AnchorStyles.Left) - && IsAnchored(anchor, AnchorStyles.Right) - && anchorInfo.Right > 0 - && IsLargerThanExpectedStretchedSize(bounds.Width, specifiedElementBounds.Width, displayRect.Width, specifiedDisplayRect.Width); - - bool hasOversizedVerticalStretchAnchor = IsAnchored(anchor, AnchorStyles.Top) - && IsAnchored(anchor, AnchorStyles.Bottom) - && anchorInfo.Bottom > 0 - && IsLargerThanExpectedStretchedSize(bounds.Height, specifiedElementBounds.Height, displayRect.Height, specifiedDisplayRect.Height); - - useSpecifiedDisplayRectangleForStretchRefresh = hasOversizedHorizontalStretchAnchor || hasOversizedVerticalStretchAnchor; - - return hasStaleHorizontalStretchAnchor - || hasStaleVerticalStretchAnchor - || hasOversizedHorizontalStretchAnchor - || hasOversizedVerticalStretchAnchor; - } - - internal static void RefreshAnchorInfoForDisplayRectangleGrowth( - IArrangedElement element, - AnchorInfo anchorInfo, - Rectangle displayRect, - AnchorStyles anchor, - bool isStretchAnchorRefresh, - bool useSpecifiedDisplayRectangleForStretchRefresh = false) - { - Rectangle cachedElementBounds = GetCachedBounds(element); - Rectangle elementBounds = cachedElementBounds; - Rectangle anchorDisplayRect = displayRect; - Rectangle originalDisplayRect = anchorInfo.DisplayRectangle; - - if (element.Container is { } container) - { - Rectangle specifiedContainerBounds = CommonProperties.GetSpecifiedBounds(container); - Rectangle specifiedElementBounds = CommonProperties.GetSpecifiedBounds(element); - - if (HasValidSpecifiedBounds(specifiedContainerBounds, specifiedElementBounds)) - { - anchorDisplayRect = isStretchAnchorRefresh && !useSpecifiedDisplayRectangleForStretchRefresh - ? originalDisplayRect - : GetDisplayRectangleForSpecifiedContainerBounds(container, displayRect, specifiedContainerBounds); - - bool useCachedBoundsForStretchRefresh = isStretchAnchorRefresh - && ShouldUseCachedBoundsForStretchRefresh(anchor, cachedElementBounds, specifiedElementBounds, anchorDisplayRect); - - elementBounds = useCachedBoundsForStretchRefresh - ? cachedElementBounds - : specifiedElementBounds; - } - } - - ResetAnchorInfo(anchorInfo, elementBounds, anchorDisplayRect, anchor); - } - - private static bool ShouldUseCachedBoundsForStretchRefresh( - AnchorStyles anchor, - Rectangle cachedElementBounds, - Rectangle specifiedElementBounds, - Rectangle anchorDisplayRect) - { - bool isHorizontalStretch = IsAnchored(anchor, AnchorStyles.Left) - && IsAnchored(anchor, AnchorStyles.Right); - bool isVerticalStretch = IsAnchored(anchor, AnchorStyles.Top) - && IsAnchored(anchor, AnchorStyles.Bottom); - - bool specifiedIsSignificantlyWiderThanDisplay = specifiedElementBounds.Width > anchorDisplayRect.Width + GetSignificantStretchDelta(anchorDisplayRect.Width); - bool specifiedIsSignificantlyTallerThanDisplay = specifiedElementBounds.Height > anchorDisplayRect.Height + GetSignificantStretchDelta(anchorDisplayRect.Height); - bool cachedWidthFitsDisplay = cachedElementBounds.Width <= anchorDisplayRect.Width + 1; - bool cachedHeightFitsDisplay = cachedElementBounds.Height <= anchorDisplayRect.Height + 1; - - return (isHorizontalStretch && specifiedIsSignificantlyWiderThanDisplay && cachedWidthFitsDisplay) - || (isVerticalStretch && specifiedIsSignificantlyTallerThanDisplay && cachedHeightFitsDisplay); - } - - private static int GetSignificantStretchDelta(int size) - { - // Ignore tiny differences caused by borders/rounding, but treat large drifts as stale metadata. - return Math.Max(MinimumSignificantStretchDelta, size / MinimumSignificantStretchDelta); - } - - private static void ResetAnchorInfo(AnchorInfo anchorInfo, Rectangle elementBounds, Rectangle displayRect, AnchorStyles anchor) - { - anchorInfo.DisplayRectangle = displayRect; - anchorInfo.Left = elementBounds.Left - displayRect.X; - anchorInfo.Top = elementBounds.Top - displayRect.Y; - anchorInfo.Right = elementBounds.Right - displayRect.X; - anchorInfo.Bottom = elementBounds.Bottom - displayRect.Y; - - if (IsAnchored(anchor, AnchorStyles.Right)) - { - anchorInfo.Right -= displayRect.Width; - - if (!IsAnchored(anchor, AnchorStyles.Left)) - { - anchorInfo.Left -= displayRect.Width; - } - } - else if (!IsAnchored(anchor, AnchorStyles.Left)) - { - anchorInfo.Right -= displayRect.Width / 2; - anchorInfo.Left -= displayRect.Width / 2; - } - - if (IsAnchored(anchor, AnchorStyles.Bottom)) - { - anchorInfo.Bottom -= displayRect.Height; - - if (!IsAnchored(anchor, AnchorStyles.Top)) - { - anchorInfo.Top -= displayRect.Height; - } - } - else if (!IsAnchored(anchor, AnchorStyles.Top)) - { - anchorInfo.Bottom -= displayRect.Height / 2; - anchorInfo.Top -= displayRect.Height / 2; - } - } - - private static bool HasValidSpecifiedBounds(Rectangle specifiedContainerBounds, Rectangle specifiedElementBounds) - { - return specifiedContainerBounds.Width > 0 - && specifiedContainerBounds.Height > 0 - && specifiedElementBounds.Width > 0 - && specifiedElementBounds.Height > 0; - } - - private static bool HasDisplayRectangleGrowth(Rectangle originalDisplayRect, Rectangle currentDisplayRect) - { - return currentDisplayRect.Width > originalDisplayRect.Width - || currentDisplayRect.Height > originalDisplayRect.Height; - } - - private static bool IsSmallerThanExpectedStretchedSize(int actualSize, int specifiedSize, int currentDisplaySize, int specifiedDisplaySize) - { - if (specifiedSize <= 0 || currentDisplaySize <= 0 || specifiedDisplaySize <= 0) - { - return false; - } - - int expectedSize = specifiedSize + Math.Max(0, currentDisplaySize - specifiedDisplaySize); - - // Allow 1-pixel tolerance for integer rounding. - return actualSize + 1 < expectedSize; - } - - private static bool IsProjectedStretchAnchorSmallerThanExpected( - int currentDisplaySize, - int leadingAnchor, - int trailingAnchor, - int specifiedSize, - int specifiedDisplaySize) - { - if (specifiedSize <= 0 || currentDisplaySize <= 0 || specifiedDisplaySize <= 0) - { - return false; - } - - int projectedSize = Math.Max(0, currentDisplaySize + trailingAnchor - leadingAnchor); - - return IsSmallerThanExpectedStretchedSize(projectedSize, specifiedSize, currentDisplaySize, specifiedDisplaySize); - } - - private static int GetEffectiveSpecifiedSizeForStretchRefresh( - int anchorDisplaySize, - int leadingAnchor, - int trailingAnchor, - int specifiedSize) - { - if (anchorDisplaySize <= 0 || specifiedSize <= 0) - { - return specifiedSize; - } - - int baselineProjectedSize = Math.Max(0, anchorDisplaySize + trailingAnchor - leadingAnchor); - int staleSpecifiedDelta = GetSignificantStretchDelta(baselineProjectedSize); - - return specifiedSize > baselineProjectedSize + staleSpecifiedDelta - ? baselineProjectedSize - : specifiedSize; - } - - private static bool IsLargerThanExpectedStretchedSize(int actualSize, int specifiedSize, int currentDisplaySize, int specifiedDisplaySize) - { - if (specifiedSize <= 0 || currentDisplaySize <= 0 || specifiedDisplaySize <= 0) - { - return false; - } - - int expectedSize = Math.Max(0, specifiedSize + currentDisplaySize - specifiedDisplaySize); - - // Allow 1-pixel tolerance for integer rounding. - return actualSize > expectedSize + 1; - } - - private static bool IsSpecifiedDisplayRectangleLikelyStale(Rectangle specifiedDisplayRect, Rectangle anchorDisplayRect) - { - int widthDelta = GetSignificantStretchDelta(anchorDisplayRect.Width); - int heightDelta = GetSignificantStretchDelta(anchorDisplayRect.Height); - - return specifiedDisplayRect.Width > anchorDisplayRect.Width + widthDelta - || specifiedDisplayRect.Height > anchorDisplayRect.Height + heightDelta; - } - - private static Rectangle GetDisplayRectangleForSpecifiedContainerBounds(IArrangedElement container, Rectangle displayRect, Rectangle specifiedContainerBounds) - { - int nonClientWidth = Math.Max(0, container.Bounds.Width - displayRect.Width); - int nonClientHeight = Math.Max(0, container.Bounds.Height - displayRect.Height); - - int displayWidth = Math.Max(0, specifiedContainerBounds.Width - nonClientWidth); - int displayHeight = Math.Max(0, specifiedContainerBounds.Height - nonClientHeight); - - return new Rectangle(displayRect.X, displayRect.Y, displayWidth, displayHeight); - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.cs b/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.cs index b0b05183a93..3336f4938d4 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Layout/DefaultLayout.cs @@ -242,7 +242,7 @@ private static Rectangle ComputeAnchoredBounds(IArrangedElement element, Rectang int right = layout.Right + displayRect.X; int bottom = layout.Bottom + displayRect.Y; - AnchorStyles anchor = TryRefreshAnchorInfoForDisplayRectangleGrowth(element, layout, displayRect); + AnchorStyles anchor = GetAnchor(element); if (IsAnchored(anchor, AnchorStyles.Right)) { @@ -729,15 +729,6 @@ private static void UpdateAnchorInfo(IArrangedElement element) SetAnchorInfo(element, anchorInfo); } - Rectangle cachedBounds = GetCachedBounds(element); - AnchorInfo oldAnchorInfo = new() - { - Left = anchorInfo.Left, - Top = anchorInfo.Top, - Right = anchorInfo.Right, - Bottom = anchorInfo.Bottom - }; - Rectangle elementBounds = element.Bounds; anchorInfo.Left = elementBounds.Left; anchorInfo.Top = elementBounds.Top; @@ -748,10 +739,6 @@ private static void UpdateAnchorInfo(IArrangedElement element) int parentWidth = parentDisplayRect.Width; int parentHeight = parentDisplayRect.Height; - // Keep the display rectangle that produced these V1 anchors so the compat refresh path - // can detect when the parent later grows and the stored positive right/bottom anchors are stale. - anchorInfo.DisplayRectangle = parentDisplayRect; - // The anchors is relative to the parent DisplayRectangle, so offset the anchors // by the DisplayRect origin anchorInfo.Left -= parentDisplayRect.X; @@ -762,24 +749,12 @@ private static void UpdateAnchorInfo(IArrangedElement element) AnchorStyles anchor = GetAnchor(element); if (IsAnchored(anchor, AnchorStyles.Right)) { - if (ScaleHelper.IsScalingRequirementMet && (anchorInfo.Right - parentWidth > 0) && (oldAnchorInfo.Right < 0)) - { - // Parent was resized to fit its parent, or screen, we need to reuse old anchors info to prevent losing control beyond right edge. - anchorInfo.Right = oldAnchorInfo.Right; - if (!IsAnchored(anchor, AnchorStyles.Left)) - { - // Control might have been resized, update Left anchors. - anchorInfo.Left = oldAnchorInfo.Right - cachedBounds.Width; - } - } - else - { - anchorInfo.Right -= parentWidth; + // This differs from the official WinForms implementation because we encountered an issue with DPI handling. See WI01100144. + anchorInfo.Right -= parentWidth; - if (!IsAnchored(anchor, AnchorStyles.Left)) - { - anchorInfo.Left -= parentWidth; - } + if (!IsAnchored(anchor, AnchorStyles.Left)) + { + anchorInfo.Left -= parentWidth; } } else if (!IsAnchored(anchor, AnchorStyles.Left)) @@ -790,26 +765,12 @@ private static void UpdateAnchorInfo(IArrangedElement element) if (IsAnchored(anchor, AnchorStyles.Bottom)) { - if (ScaleHelper.IsScalingRequirementMet && (anchorInfo.Bottom - parentHeight > 0) && (oldAnchorInfo.Bottom < 0)) - { - // The parent was resized to fit its parent or the screen, we need to reuse the old anchors info - // to prevent positioning the control beyond the bottom edge. - anchorInfo.Bottom = oldAnchorInfo.Bottom; + // This differs from the official WinForms implementation because we encountered an issue with DPI handling. See WI01100144. + anchorInfo.Bottom -= parentHeight; - if (!IsAnchored(anchor, AnchorStyles.Top)) - { - // The control might have been resized, update the Top anchor. - anchorInfo.Top = oldAnchorInfo.Bottom - cachedBounds.Height; - } - } - else + if (!IsAnchored(anchor, AnchorStyles.Top)) { - anchorInfo.Bottom -= parentHeight; - - if (!IsAnchored(anchor, AnchorStyles.Top)) - { - anchorInfo.Top -= parentHeight; - } + anchorInfo.Top -= parentHeight; } } else if (!IsAnchored(anchor, AnchorStyles.Top))