From 5b42ff746df56cf1c4097f1ae442dd8d2f55f0d2 Mon Sep 17 00:00:00 2001 From: Kurt Date: Mon, 9 Mar 2026 12:25:15 -0500 Subject: [PATCH] Handle handle leaks on dragdrop cursor icon Also pass the tooltips to the components container so that they dispose of anything if needed too. A user had a long-running script/session that drag-dropped a few thousand times, which exhausted the Windows GDI handle limit (10,000 per process). --- PKHeX.Drawing/ImageUtil.cs | 2 +- .../Controls/SAV Editor/SAVEditor.cs | 4 +- .../Controls/SAV Editor/SlotChangeManager.cs | 13 ++++--- PKHeX.WinForms/Controls/Slots/BitmapCursor.cs | 38 +++++++++++++++++++ PKHeX.WinForms/Controls/Slots/DragManager.cs | 36 +++++++++++++++--- PKHeX.WinForms/MainWindow/Main.cs | 3 +- .../Subforms/PKM Editors/Text.Designer.cs | 9 +++++ PKHeX.WinForms/Subforms/PKM Editors/Text.cs | 4 +- PKHeX.WinForms/Subforms/ReportGrid.cs | 2 + PKHeX.WinForms/Subforms/SAV_FolderList.cs | 4 +- .../Save Editors/Gen8/SAV_FlagWork8b.cs | 2 +- .../Subforms/Save Editors/SAV_EventFlags.cs | 2 +- .../Subforms/Save Editors/SAV_EventFlags2.cs | 2 +- .../Subforms/Save Editors/SAV_EventWork.cs | 2 +- 14 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 PKHeX.WinForms/Controls/Slots/BitmapCursor.cs diff --git a/PKHeX.Drawing/ImageUtil.cs b/PKHeX.Drawing/ImageUtil.cs index a8a6cc8eb..deedb149d 100644 --- a/PKHeX.Drawing/ImageUtil.cs +++ b/PKHeX.Drawing/ImageUtil.cs @@ -136,7 +136,7 @@ public int GetAverageColor() } } - private static Span GetSpan(IntPtr ptr, int length) + private static Span GetSpan(nint ptr, int length) => MemoryMarshal.CreateSpan(ref Unsafe.AddByteOffset(ref Unsafe.NullRef(), ptr), length); public static Bitmap LayerImage(Bitmap baseLayer, Bitmap overLayer, int x, int y, double transparency) diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs index 69dcad124..1dbb4d96c 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs @@ -86,6 +86,7 @@ public SAVEditor() SL_Extra.ViewIndex = -2; menu = new ContextMenuSAV { Manager = M }; + components!.Add(menu); InitializeEvents(); } @@ -1497,7 +1498,8 @@ private async void TabMouseMove(object sender, MouseEventArgs e) { using var img = new Bitmap(Box.Width, Box.Height); Box.DrawToBitmap(img, new Rectangle(0, 0, Box.Width, Box.Height)); - using var cursor = Cursor = new Cursor(img.GetHicon()); + using var dragCursor = new BitmapCursor(img); + Cursor = dragCursor.Cursor; await File.WriteAllBytesAsync(newFile, bin).ConfigureAwait(true); DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newFile }), DragDropEffects.Copy); } diff --git a/PKHeX.WinForms/Controls/SAV Editor/SlotChangeManager.cs b/PKHeX.WinForms/Controls/SAV Editor/SlotChangeManager.cs index 4d9d08951..33e7f1b1b 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SlotChangeManager.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SlotChangeManager.cs @@ -85,7 +85,7 @@ public void QueryContinueDrag(object? sender, QueryContinueDragEventArgs e) public void DragEnter(object? sender, DragEventArgs e) { - if (sender is null) + if (sender is not Control c) return; if ((e.AllowedEffect & DragDropEffects.Copy) != 0) // external file e.Effect = DragDropEffects.Copy; @@ -93,7 +93,7 @@ public void DragEnter(object? sender, DragEventArgs e) e.Effect = DragDropEffects.Move; if (Drag.Info.IsDragDropInProgress) - Drag.SetCursor(((Control)sender).FindForm(), Drag.Info.Cursor); + Drag.SetCursor(c, Drag.Info.Cursor); } private static SlotViewInfo GetSlotInfo(T pb) where T : Control @@ -156,7 +156,7 @@ private void HandleMovePKM(PictureBox pb, bool encrypt) // drop finished, clean up Drag.Info.Source = null; Drag.Reset(); - Drag.ResetCursor(pb.FindForm()); + Drag.ResetCursor(pb); // Browser apps need time to load data since the file isn't moved to a location on the user's local storage. // Tested 10ms -> too quick, 100ms was fine. 500ms should be safe? @@ -210,7 +210,7 @@ private bool TryMakeDragDropPKM(PictureBox pb, ReadOnlySpan data, string n ArgumentNullException.ThrowIfNull(img); File.WriteAllBytes(newfile, data); - Drag.SetCursor(pb.FindForm(), new Cursor(img.GetHicon())); + Drag.SetOwnedCursor(pb, img); Hover.Stop(); pb.Image = null; pb.BackgroundImage = SpriteUtil.Spriter.Drag; @@ -223,7 +223,7 @@ private bool TryMakeDragDropPKM(PictureBox pb, ReadOnlySpan data, string n { pb.Image = img; pb.BackgroundImage = LastSlot.OriginalBackground; - Drag.ResetCursor(pb.FindForm()); + Drag.ResetCursor(pb); return external; } @@ -347,7 +347,7 @@ private bool TrySetPKMDestination(PictureBox pb, DropModifier mod) // Copy from temp to destination slot. var type = info.IsDragSwap ? SlotTouchType.Swap : SlotTouchType.Set; Env.Slots.Set(info.Destination!.Slot, pk, type); - Drag.ResetCursor(pb.FindForm()); + Drag.ResetCursor(pb); return true; } @@ -381,6 +381,7 @@ public void SwapBoxes(int index, int other, SaveFile SAV) public void Dispose() { + Drag.Dispose(); Hover.Dispose(); SE.Dispose(); LastSlot.OriginalBackground?.Dispose(); diff --git a/PKHeX.WinForms/Controls/Slots/BitmapCursor.cs b/PKHeX.WinForms/Controls/Slots/BitmapCursor.cs new file mode 100644 index 000000000..7969e3234 --- /dev/null +++ b/PKHeX.WinForms/Controls/Slots/BitmapCursor.cs @@ -0,0 +1,38 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace PKHeX.WinForms.Controls; + +/// +/// Managed implementation that acquires the cursor appearance from a and properly disposes of the underlying icon handle when done. +/// +public sealed class BitmapCursor : IDisposable +{ + public Cursor Cursor { get; } + private nint IconHandle; + + public BitmapCursor(Bitmap bitmap) + { + IconHandle = bitmap.GetHicon(); // creates a handle we need to dispose of. + Cursor = new Cursor(IconHandle); + } + + // Need to use DestroyIcon as Cursor does not take ownership of the icon handle and will not destroy it when disposed. + + #pragma warning disable SYSLIB1054 + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyIcon(nint hIcon); + #pragma warning restore SYSLIB1054 + + public void Dispose() + { + Cursor.Dispose(); + if (IconHandle == nint.Zero) + return; + _ = DestroyIcon(IconHandle); + IconHandle = nint.Zero; + } +} diff --git a/PKHeX.WinForms/Controls/Slots/DragManager.cs b/PKHeX.WinForms/Controls/Slots/DragManager.cs index cdec2dd15..19d8a45d0 100644 --- a/PKHeX.WinForms/Controls/Slots/DragManager.cs +++ b/PKHeX.WinForms/Controls/Slots/DragManager.cs @@ -1,3 +1,4 @@ +using System; using System.Drawing; using System.Windows.Forms; @@ -6,7 +7,7 @@ namespace PKHeX.WinForms.Controls; /// /// Manages drag-and-drop operations for slot controls. /// -public sealed class DragManager +public sealed class DragManager : IDisposable { /// /// Gets the current slot change information for drag-and-drop operations. @@ -17,6 +18,7 @@ public sealed class DragManager /// Occurs when an external drag-and-drop operation is requested. /// public event DragEventHandler? RequestExternalDragDrop; + private BitmapCursor? OwnedCursor; /// /// Requests a drag-and-drop operation. @@ -30,17 +32,26 @@ public sealed class DragManager /// /// The form to set the cursor for. /// The cursor to set. - public void SetCursor(Form? f, Cursor? z) + public void SetCursor(Control f, Cursor? z) { - f?.Cursor = z; + if (OwnedCursor is not null && !ReferenceEquals(OwnedCursor.Cursor, z)) + DisposeOwnedCursor(); + f.Cursor = z; Info.Cursor = z; } + public void SetOwnedCursor(Control f, Bitmap bitmap) + { + DisposeOwnedCursor(); + OwnedCursor = new BitmapCursor(bitmap); + SetCursor(f, OwnedCursor.Cursor); + } + /// /// Resets the cursor for the specified form to the default cursor. /// /// The form to reset the cursor for. - public void ResetCursor(Form? sender) + public void ResetCursor(Control sender) { SetCursor(sender, Cursors.Default); } @@ -50,13 +61,28 @@ public void ResetCursor(Form? sender) /// public void Initialize() { + DisposeOwnedCursor(); Info = new SlotChangeInfo(); } /// /// Resets the drag manager's slot change info. /// - public void Reset() => Info.Reset(); + public void Reset() + { + DisposeOwnedCursor(); + Info.Reset(); + } + + private void DisposeOwnedCursor() + { + if (OwnedCursor is null) + return; + OwnedCursor.Dispose(); + OwnedCursor = null; + } + + public void Dispose() => DisposeOwnedCursor(); /// /// Gets or sets the mouse down position for drag detection. diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index f0b489897..be874b389 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -106,6 +106,7 @@ private void FormLoadAddEvents() mnu.RequestEditorSaveAs += MainMenuSave; dragout.ContextMenuStrip = mnu.mnuL; C_SAV.menu.RequestEditorLegality = DisplayLegalityReport; + components.Add(mnu); } public void LoadInitialFiles(StartupArguments args) @@ -1239,7 +1240,7 @@ private async void Dragout_MouseDown(object sender, MouseEventArgs e) var pb = (PictureBox)sender; if (pb.Image is Bitmap img) - C_SAV.M.Drag.Info.Cursor = Cursor = new Cursor(img.GetHicon()); + C_SAV.M.Drag.SetOwnedCursor(pb, img); DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newfile }), DragDropEffects.Copy); } diff --git a/PKHeX.WinForms/Subforms/PKM Editors/Text.Designer.cs b/PKHeX.WinForms/Subforms/PKM Editors/Text.Designer.cs index 266d1c90e..fda1485a2 100644 --- a/PKHeX.WinForms/Subforms/PKM Editors/Text.Designer.cs +++ b/PKHeX.WinForms/Subforms/PKM Editors/Text.Designer.cs @@ -28,6 +28,8 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { + components = new System.ComponentModel.Container(); + Tip = new System.Windows.Forms.ToolTip(components); TB_Text = new System.Windows.Forms.TextBox(); CB_Species = new System.Windows.Forms.ComboBox(); B_Cancel = new System.Windows.Forms.Button(); @@ -47,6 +49,12 @@ private void InitializeComponent() ((System.ComponentModel.ISupportInitialize)NUD_Generation).BeginInit(); SuspendLayout(); // + // Tip + // + Tip.AutoPopDelay = 32767; + Tip.InitialDelay = 200; + Tip.IsBalloon = false; + // // TB_Text // TB_Text.Location = new System.Drawing.Point(114, 14); @@ -262,5 +270,6 @@ private void InitializeComponent() private System.Windows.Forms.Label L_Generation; private System.Windows.Forms.Button B_ClearTrash; private System.Windows.Forms.Label L_String; + private System.Windows.Forms.ToolTip Tip; } } diff --git a/PKHeX.WinForms/Subforms/PKM Editors/Text.cs b/PKHeX.WinForms/Subforms/PKM Editors/Text.cs index 4ffee417e..4cf0ba8d2 100644 --- a/PKHeX.WinForms/Subforms/PKM Editors/Text.cs +++ b/PKHeX.WinForms/Subforms/PKM Editors/Text.cs @@ -10,7 +10,6 @@ namespace PKHeX.WinForms; public partial class TrashEditor : Form { private readonly IStringConverter Converter; - private readonly ToolTip Tip = new() { InitialDelay = 200, IsBalloon = false, AutoPopDelay = 32_767 }; private readonly List Bytes = []; public string FinalString { get; private set; } public byte[] FinalBytes { get; private set; } @@ -93,8 +92,7 @@ private void AddCharEditing(Font f, EntityContext context) l.Size = new Size(20, 20); l.Click += (_, _) => { if (TB_Text.Text.Length < TB_Text.MaxLength) TB_Text.AppendText(l.Text); }; FLP_Characters.Controls.Add(l); - var tt = new ToolTip(); - tt.SetToolTip(l, $"Insert {l.Text} (0x{c:X4})"); + Tip.SetToolTip(l, $"Insert {l.Text} (0x{c:X4})"); } } diff --git a/PKHeX.WinForms/Subforms/ReportGrid.cs b/PKHeX.WinForms/Subforms/ReportGrid.cs index 59915fa67..0cf0e5d3a 100644 --- a/PKHeX.WinForms/Subforms/ReportGrid.cs +++ b/PKHeX.WinForms/Subforms/ReportGrid.cs @@ -52,6 +52,8 @@ private void GetContextMenu() ContextMenuStrip mnu = new(); mnu.Items.Add(mnuHide); mnu.Items.Add(mnuRestore); + components ??= new System.ComponentModel.Container(); + components.Add(mnu); dgData.ContextMenuStrip = mnu; } diff --git a/PKHeX.WinForms/Subforms/SAV_FolderList.cs b/PKHeX.WinForms/Subforms/SAV_FolderList.cs index 062a34828..c6210967d 100644 --- a/PKHeX.WinForms/Subforms/SAV_FolderList.cs +++ b/PKHeX.WinForms/Subforms/SAV_FolderList.cs @@ -31,6 +31,7 @@ public SAV_FolderList(Action openSaveFile) var drives = Environment.GetLogicalDrives(); Paths = GetPathList(drives, backups); + components ??= new System.ComponentModel.Container(); dgDataRecent.ContextMenuStrip = GetContextMenu(dgDataRecent); dgDataBackup.ContextMenuStrip = GetContextMenu(dgDataBackup); dgDataRecent.Sorted += (_, _) => GetFilterText(dgDataRecent); @@ -111,7 +112,7 @@ private void AddButton(string name, string path) }; FLP_Buttons.Controls.Add(button); - var hover = new ToolTip {AutoPopDelay = 30_000}; + var hover = new ToolTip(components) {AutoPopDelay = 30_000}; button.MouseHover += (_, _) => hover.Show(path, button); } @@ -202,6 +203,7 @@ private ContextMenuStrip GetContextMenu(DataGridView dgv) mnu.Items.Add(mnuOpen); mnu.Items.Add(mnuBrowseAt); mnu.Items.Add(mnuDelete); + components.Add(mnu); return mnu; } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_FlagWork8b.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_FlagWork8b.cs index c127e2ab1..f9c079d2f 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_FlagWork8b.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_FlagWork8b.cs @@ -290,7 +290,7 @@ private static void ApplyWorkFilter(TableLayoutPanel panel, IReadOnlyList<(strin panel.ResumeLayout(); } - private TextBox CreateSearchBox(Action applyFilter) + private static TextBox CreateSearchBox(Action applyFilter) { var box = new TextBox { diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags.cs index d16a6dbe3..75965d502 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags.cs @@ -332,7 +332,7 @@ private static void ApplyConstFilter(TableLayoutPanel panel, IReadOnlyList<(stri panel.ResumeLayout(); } - private TextBox CreateSearchBox(Action applyFilter) + private static TextBox CreateSearchBox(Action applyFilter) { var box = new TextBox { diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags2.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags2.cs index e7dee4048..da6506de4 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags2.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventFlags2.cs @@ -334,7 +334,7 @@ private static void ApplyConstFilter(TableLayoutPanel panel, IReadOnlyList<(stri panel.ResumeLayout(); } - private TextBox CreateSearchBox(Action applyFilter) + private static TextBox CreateSearchBox(Action applyFilter) { var box = new TextBox { diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventWork.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventWork.cs index 4839f289f..ae61dd899 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_EventWork.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_EventWork.cs @@ -250,7 +250,7 @@ private static void ApplyWorkFilter(TableLayoutPanel panel, IReadOnlyList<(strin panel.ResumeLayout(); } - private TextBox CreateSearchBox(Action applyFilter) + private static TextBox CreateSearchBox(Action applyFilter) { var box = new TextBox {