From 8766bb92de4277d06e112d60b3b4901f71995f32 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Mon, 13 Jul 2026 14:35:27 +0800 Subject: [PATCH 1/4] Fix Issue 14721: TreeView.CustomDraw leaks one HFONT per visible item per paint when NodeFont is set, leading to GDI handle exhaustion and crash --- .../Windows/Forms/Controls/TreeView/TreeView.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs index 4d6a3c53602..91c61491aee 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs @@ -2782,12 +2782,16 @@ private unsafe void CustomDraw(ref Message m) if (renderinfo is not null && renderinfo.Font is not null) { // Mess with the DC directly... - PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, renderinfo.FontHandle); + Debug.Assert(node._propBag is not null); + if (node._propBag is not null && node._propBag.Font is not null) + { + PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, node._propBag.FontHandle); - // There is a problem in winctl that clips node fonts if the fontSize - // is larger than the treeView font size. The behavior is much better in comctl 5 and above. - m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; - return; + // There is a problem in winctl that clips node fonts if the fontSize + // is larger than the treeView font size. The behavior is much better in comctl 5 and above. + m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; + return; + } } // fall through and do the default drawing work From 75d8941380372e8fe059430b5d2b21b3f958fe7a Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Mon, 13 Jul 2026 16:29:31 +0800 Subject: [PATCH 2/4] Handle feedback --- .../Windows/Forms/OwnerDrawPropertyBag.cs | 15 ++++++++++++++- .../Forms/OwnerDrawPropertyBagTests.cs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBag.cs b/src/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBag.cs index a5e39cb6475..c78c4232e50 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBag.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBag.cs @@ -12,6 +12,7 @@ namespace System.Windows.Forms; [Serializable] // This class is participating in resx serialization scenarios for listview/treeview items. public class OwnerDrawPropertyBag : MarshalByRefObject, ISerializable { + private Font? _font; private Control.FontHandleWrapper? _fontWrapper; private static readonly Lock s_internalSyncObject = new(); @@ -38,7 +39,19 @@ internal OwnerDrawPropertyBag() { } - public Font? Font { get; set; } + public Font? Font + { + get => _font; + set + { + if (!ReferenceEquals(_font, value)) + { + _font = value; + _fontWrapper?.Dispose(); + _fontWrapper = null; + } + } + } public Color ForeColor { get; set; } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBagTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBagTests.cs index b95c33e3378..650d207037c 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBagTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/OwnerDrawPropertyBagTests.cs @@ -4,6 +4,7 @@ #nullable disable using System.Drawing; +using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; using System.Windows.Forms.TestUtilities; @@ -56,6 +57,24 @@ public void OwnerDrawPropertyBag_Font_Set_GetReturnsExpected(Font value) Assert.Equal(value is null, bag.IsEmpty()); } + [WinFormsFact] + public void OwnerDrawPropertyBag_Font_SetDifferentValue_ResetsCachedFontWrapper() + { + using SubTreeView treeView = new(); + OwnerDrawPropertyBag bag = treeView.GetItemRenderStyles(null, 0); + using Font firstFont = new(SystemFonts.MenuFont, FontStyle.Regular); + using Font secondFont = new(SystemFonts.MenuFont, FontStyle.Bold); + + bag.Font = firstFont; + _ = bag.GetType().GetProperty("FontHandle", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(bag); + + FieldInfo fontWrapperField = bag.GetType().GetField("_fontWrapper", BindingFlags.Instance | BindingFlags.NonPublic)!; + Assert.NotNull(fontWrapperField.GetValue(bag)); + + bag.Font = secondFont; + Assert.Null(fontWrapperField.GetValue(bag)); + } + [WinFormsTheory] [CommonMemberData(typeof(CommonTestHelper), nameof(CommonTestHelper.GetColorWithEmptyTheoryData))] public void OwnerDrawPropertyBag_ForeColor_Set_GetReturnsExpected(Color value) From 8966fb050ee2a2c01e90eb38b8f1e158c67f4202 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Tue, 14 Jul 2026 10:05:07 +0800 Subject: [PATCH 3/4] Optimize the code --- .../Forms/Controls/TreeView/TreeView.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs index 91c61491aee..eb4e8c14c8f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs @@ -2782,16 +2782,12 @@ private unsafe void CustomDraw(ref Message m) if (renderinfo is not null && renderinfo.Font is not null) { // Mess with the DC directly... - Debug.Assert(node._propBag is not null); - if (node._propBag is not null && node._propBag.Font is not null) - { - PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, node._propBag.FontHandle); + PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, renderinfo.FontHandle); - // There is a problem in winctl that clips node fonts if the fontSize - // is larger than the treeView font size. The behavior is much better in comctl 5 and above. - m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; - return; - } + // There is a problem in winctl that clips node fonts if the fontSize + // is larger than the treeView font size. The behavior is much better in comctl 5 and above. + m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; + return; } // fall through and do the default drawing work @@ -2867,12 +2863,12 @@ private unsafe void CustomDraw(ref Message m) /// protected OwnerDrawPropertyBag GetItemRenderStyles(TreeNode? node, int state) { - OwnerDrawPropertyBag retval = new(); if (node is null || node._propBag is null) { - return retval; + return new(); } + OwnerDrawPropertyBag retval = node._propBag; // we only change colors if we're displaying things normally if ((state & (int)(NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_SELECTED | From 8615b64d5f1671d47e7f4eed9d83b4e194705ec2 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Wed, 15 Jul 2026 11:26:17 +0800 Subject: [PATCH 4/4] Handle feedback --- .../Forms/Controls/TreeView/TreeView.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs index eb4e8c14c8f..91c61491aee 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeView.cs @@ -2782,12 +2782,16 @@ private unsafe void CustomDraw(ref Message m) if (renderinfo is not null && renderinfo.Font is not null) { // Mess with the DC directly... - PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, renderinfo.FontHandle); + Debug.Assert(node._propBag is not null); + if (node._propBag is not null && node._propBag.Font is not null) + { + PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, node._propBag.FontHandle); - // There is a problem in winctl that clips node fonts if the fontSize - // is larger than the treeView font size. The behavior is much better in comctl 5 and above. - m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; - return; + // There is a problem in winctl that clips node fonts if the fontSize + // is larger than the treeView font size. The behavior is much better in comctl 5 and above. + m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT; + return; + } } // fall through and do the default drawing work @@ -2863,12 +2867,12 @@ private unsafe void CustomDraw(ref Message m) /// protected OwnerDrawPropertyBag GetItemRenderStyles(TreeNode? node, int state) { + OwnerDrawPropertyBag retval = new(); if (node is null || node._propBag is null) { - return new(); + return retval; } - OwnerDrawPropertyBag retval = node._propBag; // we only change colors if we're displaying things normally if ((state & (int)(NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_SELECTED |