Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public CursorUI()
}
}

_cursorWidth = GetCursorWidthForDpi(Cursors.Default, DeviceDpi);
RecalculateCursorWidth(DeviceDpi);
}

public object? Value { get; private set; }
Expand All @@ -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)
Expand All @@ -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, 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);
}
}
Expand Down Expand Up @@ -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)
{
Expand All @@ -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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.NotEmpty(cache);

endMethod.Invoke(cursorUI, null);
Assert.Empty(cache);

Mock<IWindowsFormsEditorService> mockEditorService = new(MockBehavior.Strict);
startMethod.Invoke(cursorUI, [mockEditorService.Object, Cursors.Default]);
Assert.NotEmpty(cache);
}

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