From 6d342519fb3212b06cdb01cbfc6339765e3ed3c1 Mon Sep 17 00:00:00 2001 From: "Ricardo Bossan (BEYONDSOFT CONSULTING INC) (from Dev Box)" Date: Tue, 14 Jul 2026 21:25:15 -0300 Subject: [PATCH 1/2] Fixes #6382 An `OwnerDrawFixed` `ListBox` keeps its fixed `ItemHeight` (the reporter's sample sets `25`) when the control is autoscaled. At higher DPI the font grows but the rows stay their design-time height, so the items look cramped. .NET Framework scaled the item height; .NET (Core) doesn't. - Scale the `OwnerDrawFixed` item height by `factor.Height` in `ListBox.ScaleControl`; the same scaling pass that already scales padding and margin. The result is clamped to `[1, 255]`, and a height-axis guard keeps a partial pass from applying the factor twice. - `ScaleControl` is the entry point that both `AutoScaleMode.Font`/`Dpi` and PerMonitorV2 route through, so no per-DPI-message hook is needed. - Owner-draw fixed `ListBox` items are the correct height at higher DPI instead of the design-time height. Long-standing customer report; does not occur on .NET Framework. - Versus .NET Framework: yes (Framework scaled it, Core didn't). Not a recent .NET regression. - Low. Runs only for `OwnerDrawFixed` during a real scaling pass, reuses the `ItemHeight` setter, and clamps so it can't throw. `Normal`/`OwnerDrawVariable` unaffected. Sample is `HighDpiMode.SystemAware` (reads DPI at startup): change the scale, then **relaunch** to 150%: rows stay at `ItemHeight = 25` while the font scales up (cramped). 150%: item height scales (25 > ~38), keeping the 100% proportions. - Unit tests - No accessibility surface change; item height only affects layout. - Windows 11, .NET (main). --- .../Forms/Controls/ListBoxes/ListBox.cs | 7 +++ .../System/Windows/Forms/ListBoxTests.cs | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs index ef7c6daab90..6c619b02b1f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs @@ -2071,6 +2071,13 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) UpdateFontCache(); } + // In OwnerDrawFixed the native list box doesn't rescale the fixed item height when the control + // autoscales, so scale it here (clamped to [1, 255]; height-axis guard avoids a double-apply). #6382 + if (_drawMode == DrawMode.OwnerDrawFixed && factor.Height != 1F && (specified & BoundsSpecified.Height) != 0) + { + ItemHeight = Math.Clamp((int)Math.Round(_itemHeight * factor.Height), 1, 255); + } + base.ScaleControl(factor, specified); } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs index 1ad48b85b77..3fe37049db4 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs @@ -1845,6 +1845,52 @@ public void ListBox_ItemHeight_ShouldSerializeValue_Success() Assert.False(property.ShouldSerializeValue(control)); } + [WinFormsFact] + public void ListBox_ScaleControl_OwnerDrawFixed_ScalesItemHeight() + { + // #6382: an OwnerDrawFixed item height must scale with the control during autoscaling. + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 25 }; + + control.ScaleControl(new SizeF(1.5f, 1.5f), BoundsSpecified.All); + + Assert.Equal(38, control.ItemHeight); // 25 * 1.5 = 37.5 -> 38 + } + + [WinFormsFact] + public void ListBox_ScaleControl_OwnerDrawFixed_WithHandle_UpdatesNativeItemHeight() + { + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 20 }; + Assert.NotEqual(IntPtr.Zero, control.Handle); + Assert.Equal(20, (int)PInvokeCore.SendMessage(control, PInvoke.LB_GETITEMHEIGHT)); + + control.ScaleControl(new SizeF(2f, 2f), BoundsSpecified.All); + + Assert.Equal(40, control.ItemHeight); + Assert.Equal(40, (int)PInvokeCore.SendMessage(control, PInvoke.LB_GETITEMHEIGHT)); + } + + [WinFormsFact] + public void ListBox_ScaleControl_OwnerDrawFixed_ClampsItemHeightToMax() + { + // Scaling must not throw when the result exceeds the 255 maximum; it clamps instead. + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 200 }; + + control.ScaleControl(new SizeF(2f, 2f), BoundsSpecified.All); + + Assert.Equal(255, control.ItemHeight); + } + + [WinFormsFact] + public void ListBox_ScaleControl_Normal_DoesNotChangeItemHeight() + { + using SubListBox control = new() { DrawMode = DrawMode.Normal }; + int before = control.ItemHeight; + + control.ScaleControl(new SizeF(1.5f, 1.5f), BoundsSpecified.All); + + Assert.Equal(before, control.ItemHeight); + } + public static IEnumerable Items_CustomCreateItemCollection_TestData() { yield return new object[] { null }; From 660795a85d249eb5c20c791e0c244f927e69df11 Mon Sep 17 00:00:00 2001 From: "Ricardo Bossan (BEYONDSOFT CONSULTING INC) (from Dev Box)" Date: Wed, 15 Jul 2026 13:14:10 -0300 Subject: [PATCH 2/2] Round scaled ListBox ItemHeight away from zero Math.Round uses banker's rounding by default, so .5 midpoints could round down (15 * 1.5 -> 22) and re-introduce the cramped rows this fixes. Add tests covering the midpoints (25 -> 38, 15 -> 23) and the height-axis guard that keeps a Height-excluded scaling pass from applying the factor twice. --- .../Forms/Controls/ListBoxes/ListBox.cs | 4 ++- .../System/Windows/Forms/ListBoxTests.cs | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs index 6c619b02b1f..130c1487f39 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ListBoxes/ListBox.cs @@ -2073,9 +2073,11 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) // In OwnerDrawFixed the native list box doesn't rescale the fixed item height when the control // autoscales, so scale it here (clamped to [1, 255]; height-axis guard avoids a double-apply). #6382 + // Round away from zero: banker's rounding would round .5 midpoints down (15 * 1.5 -> 22), which + // re-introduces the cramped rows this fixes. if (_drawMode == DrawMode.OwnerDrawFixed && factor.Height != 1F && (specified & BoundsSpecified.Height) != 0) { - ItemHeight = Math.Clamp((int)Math.Round(_itemHeight * factor.Height), 1, 255); + ItemHeight = Math.Clamp((int)Math.Round(_itemHeight * factor.Height, MidpointRounding.AwayFromZero), 1, 255); } base.ScaleControl(factor, specified); diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs index 3fe37049db4..1307f7eee7d 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ListBoxTests.cs @@ -1849,11 +1849,11 @@ public void ListBox_ItemHeight_ShouldSerializeValue_Success() public void ListBox_ScaleControl_OwnerDrawFixed_ScalesItemHeight() { // #6382: an OwnerDrawFixed item height must scale with the control during autoscaling. - using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 25 }; + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 20 }; control.ScaleControl(new SizeF(1.5f, 1.5f), BoundsSpecified.All); - Assert.Equal(38, control.ItemHeight); // 25 * 1.5 = 37.5 -> 38 + Assert.Equal(30, control.ItemHeight); } [WinFormsFact] @@ -1869,6 +1869,30 @@ public void ListBox_ScaleControl_OwnerDrawFixed_WithHandle_UpdatesNativeItemHeig Assert.Equal(40, (int)PInvokeCore.SendMessage(control, PInvoke.LB_GETITEMHEIGHT)); } + [WinFormsTheory] + [InlineData(25, 38)] // 37.5 rounds up + [InlineData(15, 23)] // 22.5 rounds up, not down to 22 as banker's rounding would + public void ListBox_ScaleControl_OwnerDrawFixed_RoundsMidpointsAwayFromZero(int itemHeight, int expected) + { + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = itemHeight }; + + control.ScaleControl(new SizeF(1.5f, 1.5f), BoundsSpecified.All); + + Assert.Equal(expected, control.ItemHeight); + } + + [WinFormsFact] + public void ListBox_ScaleControl_OwnerDrawFixed_HeightExcluded_DoesNotScaleItemHeight() + { + // A scaling pass that excludes the height axis must leave the item height alone, otherwise the + // included/excluded pass pair in ContainerControl.PerformAutoScale would apply the factor twice. + using SubListBox control = new() { DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 25 }; + + control.ScaleControl(new SizeF(1.5f, 1.5f), BoundsSpecified.Width); + + Assert.Equal(25, control.ItemHeight); + } + [WinFormsFact] public void ListBox_ScaleControl_OwnerDrawFixed_ClampsItemHeightToMax() {