From 325152913132605a469c8a14ca22ca265005d048 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Mon, 7 Sep 2026 14:43:43 +0800 Subject: [PATCH 1/3] Fix Issue 14167: [HDPI] Cursor editor some items are not displayed in propertyGrid with special DPI settings --- .../Drawing/Design/CursorEditor.CursorUI.cs | 65 +++++++++++++++-- .../Drawing/Design/CursorEditorTests.cs | 73 +++++++++++++++++++ 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs b/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs index 72a39e9f665..e1b35555831 100644 --- a/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs +++ b/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs @@ -41,7 +41,7 @@ public CursorUI() } } - _cursorWidth = GetCursorWidthForDpi(Cursors.Default, DeviceDpi); + RecalculateCursorWidth(DeviceDpi); } public object? Value { get; private set; } @@ -50,7 +50,17 @@ public void End() { _editorService = null; Value = null; - _cursorWidthCache.Clear(); + ClearCursorCaches(); + } + + protected override void OnVisibleChanged(EventArgs e) + { + base.OnVisibleChanged(e); + + if (Visible && IsHandleCreated) + { + BeginInvoke((MethodInvoker)ForceRedrawVisibleItems); + } } protected override void OnClick(EventArgs e) @@ -75,7 +85,17 @@ protected override void OnDrawItem(DrawItemEventArgs e) e.Graphics.FillRectangle(SystemBrushes.Control, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, _cursorWidth, e.Bounds.Height - 4)); e.Graphics.DrawRectangle(SystemPens.WindowText, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, _cursorWidth - 1, e.Bounds.Height - 4 - 1)); - cursor.DrawStretched(e.Graphics, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, _cursorWidth, e.Bounds.Height - 4)); + using (DeviceContextHdcScope dc = new(e.Graphics, applyGraphicsState: false)) + { + PInvokeCore.DrawIconEx( + (HDC)dc, + e.Bounds.X + 2, + e.Bounds.Y + 2, + cursor, + _cursorWidth, + e.Bounds.Height - 4); + } + e.Graphics.DrawString(text, font, brushText, e.Bounds.X + _cursorWidth + 4, e.Bounds.Y + (e.Bounds.Height - font.Height) / 2); } } @@ -113,6 +133,9 @@ public void Start(IWindowsFormsEditorService editorService, object? value) _editorService = editorService; Value = value; + // Rebuild width/cache for every drop-down session so owner-draw rows are ready on first paint. + RecalculateCursorWidth(DeviceDpi); + // Select the current cursor if (value is not null) { @@ -125,14 +148,46 @@ public void Start(IWindowsFormsEditorService editorService, object? value) } } } + + Invalidate(); + Update(); + } + + private void ForceRedrawVisibleItems() + { + if (!IsDisposed && IsHandleCreated && Visible) + { + Invalidate(); + Update(); + } + } + + private void ClearCursorCaches() + { + _cursorWidthCache.Clear(); + } + + private void RecalculateCursorWidth(int dpi) + { + int cursorWidth = GetCursorWidthForDpi(Cursors.Default, dpi); + + foreach (object item in Items) + { + if (item is Cursor cursor) + { + cursorWidth = Math.Max(cursorWidth, GetCursorWidthForDpi(cursor, dpi)); + } + } + + _cursorWidth = cursorWidth; } protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew) { base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew); - // Recalculate the representative width using the new DPI; all rows continue to share it to avoid the layout issues seen in #14167. - _cursorWidth = GetCursorWidthForDpi(Cursors.Default, deviceDpiNew); + // Recalculate the shared column width using the new DPI and the widest standard cursor. + RecalculateCursorWidth(deviceDpiNew); Invalidate(); } } diff --git a/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs b/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs index 554fd02348a..af71a97b55c 100644 --- a/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs +++ b/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs @@ -4,6 +4,8 @@ #nullable disable using System.ComponentModel; +using System.Collections; +using System.Reflection; using System.Windows.Forms.Design; using System.Windows.Forms.TestUtilities; using Moq; @@ -74,4 +76,75 @@ public void CursorEditor_GetPaintValueSupported_Invoke_ReturnsFalse(ITypeDescrip CursorEditor editor = new(); Assert.False(editor.GetPaintValueSupported(context)); } + + [WinFormsFact] + public void CursorEditor_CursorUI_CursorWidth_UsesWidestStandardCursor() + { + Type type = typeof(CursorEditor).GetNestedType("CursorUI", BindingFlags.NonPublic | BindingFlags.Instance)!; + using ListBox cursorUI = (ListBox)Activator.CreateInstance(type)!; + int dpi = cursorUI.DeviceDpi; + + MethodInfo getCursorWidthForDpiMethod = type.GetMethod("GetCursorWidthForDpi", BindingFlags.NonPublic | BindingFlags.Instance)!; + FieldInfo cursorWidthField = type.GetField("_cursorWidth", BindingFlags.NonPublic | BindingFlags.Instance)!; + + int expectedWidth = 0; + foreach (object item in cursorUI.Items) + { + if (item is Cursor cursor) + { + int width = (int)getCursorWidthForDpiMethod.Invoke(cursorUI, [cursor, dpi])!; + expectedWidth = Math.Max(expectedWidth, width); + } + } + + Assert.NotEqual(0, expectedWidth); + Assert.Equal(expectedWidth, (int)cursorWidthField.GetValue(cursorUI)!); + } + + [WinFormsFact] + public void CursorEditor_CursorUI_Start_RebuildsCursorWidthCache() + { + Type type = typeof(CursorEditor).GetNestedType("CursorUI", BindingFlags.NonPublic | BindingFlags.Instance)!; + using ListBox cursorUI = (ListBox)Activator.CreateInstance(type)!; + + FieldInfo cursorWidthCacheField = type.GetField("_cursorWidthCache", BindingFlags.NonPublic | BindingFlags.Instance)!; + MethodInfo endMethod = type.GetMethod("End", BindingFlags.Public | BindingFlags.Instance)!; + MethodInfo startMethod = type.GetMethod("Start", BindingFlags.Public | BindingFlags.Instance)!; + + IDictionary cache = (IDictionary)cursorWidthCacheField.GetValue(cursorUI)!; + Assert.NotEqual(0, cache.Count); + + endMethod.Invoke(cursorUI, null); + Assert.Equal(0, cache.Count); + + Mock mockEditorService = new(MockBehavior.Strict); + startMethod.Invoke(cursorUI, [mockEditorService.Object, Cursors.Default]); + Assert.NotEqual(0, cache.Count); + } + + [WinFormsFact] + public void CursorEditor_CursorUI_OnDrawItem_RestoresGraphicsClip() + { + Type type = typeof(CursorEditor).GetNestedType("CursorUI", BindingFlags.NonPublic | BindingFlags.Instance)!; + using ListBox cursorUI = (ListBox)Activator.CreateInstance(type)!; + MethodInfo onDrawItemMethod = type.GetMethod("OnDrawItem", BindingFlags.NonPublic | BindingFlags.Instance)!; + + using Bitmap image = new(600, 400); + using Graphics graphics = Graphics.FromImage(image); + graphics.SetClip(new Rectangle(10, 10, 580, 380)); + Rectangle initialClipBounds = Rectangle.Round(graphics.ClipBounds); + + using DrawItemEventArgs args = new( + graphics, + SystemFonts.DefaultFont, + new Rectangle(0, 0, 500, cursorUI.ItemHeight), + index: 0, + DrawItemState.Default, + SystemColors.WindowText, + SystemColors.Window); + + onDrawItemMethod.Invoke(cursorUI, [args]); + + Assert.Equal(initialClipBounds, Rectangle.Round(graphics.ClipBounds)); + } } From bbfdda3db3ad3e4db5b06ebbc59f230bb6de5878 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Mon, 7 Sep 2026 15:57:57 +0800 Subject: [PATCH 2/3] Fix test cases --- .../UnitTests/System/Drawing/Design/CursorEditorTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs b/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs index af71a97b55c..9e0c599275a 100644 --- a/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs +++ b/src/System.Windows.Forms.Design/tests/UnitTests/System/Drawing/Design/CursorEditorTests.cs @@ -112,14 +112,14 @@ public void CursorEditor_CursorUI_Start_RebuildsCursorWidthCache() MethodInfo startMethod = type.GetMethod("Start", BindingFlags.Public | BindingFlags.Instance)!; IDictionary cache = (IDictionary)cursorWidthCacheField.GetValue(cursorUI)!; - Assert.NotEqual(0, cache.Count); + Assert.NotEmpty(cache); endMethod.Invoke(cursorUI, null); - Assert.Equal(0, cache.Count); + Assert.Empty(cache); Mock mockEditorService = new(MockBehavior.Strict); startMethod.Invoke(cursorUI, [mockEditorService.Object, Cursors.Default]); - Assert.NotEqual(0, cache.Count); + Assert.NotEmpty(cache); } [WinFormsFact] From d85f8e5c96a2a8e6e9b9acede39070f81d747383 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Mon, 7 Sep 2026 16:10:33 +0800 Subject: [PATCH 3/3] Handle feedback --- .../src/System/Drawing/Design/CursorEditor.CursorUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs b/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs index e1b35555831..ca5372d3f88 100644 --- a/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs +++ b/src/System.Windows.Forms.Design/src/System/Drawing/Design/CursorEditor.CursorUI.cs @@ -85,7 +85,7 @@ protected override void OnDrawItem(DrawItemEventArgs e) e.Graphics.FillRectangle(SystemBrushes.Control, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, _cursorWidth, e.Bounds.Height - 4)); e.Graphics.DrawRectangle(SystemPens.WindowText, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, _cursorWidth - 1, e.Bounds.Height - 4 - 1)); - using (DeviceContextHdcScope dc = new(e.Graphics, applyGraphicsState: false)) + using (DeviceContextHdcScope dc = new(e, applyGraphicsState: false)) { PInvokeCore.DrawIconEx( (HDC)dc,