From c3cd791d8cee73b221d48d7ba31b5e9d6e619ecc Mon Sep 17 00:00:00 2001 From: Bhanu Santin Date: Fri, 18 Sep 2026 15:22:21 -0700 Subject: [PATCH] fix(TextAreaControl): do not cache the measuring device context A Graphics from Control.CreateGraphics() wraps a DC borrowed from the system cache and is bound to the control's current window handle. Keeping one for the lifetime of the control leaves a stale HDC behind once that handle goes away, so the next layout pass throws "Parameter is not valid" out of TextRenderer.MeasureText before it measures anything. Scope it to the layout pass instead, which is the pattern every other CreateGraphics() call in this library already uses. The ??= is kept, so a pass which only reads cached line widths still creates no device context at all. Refs: gitextensions/gitextensions#13266 Co-Authored-By: Claude Opus 5 (1M context) --- Project/Src/Gui/TextAreaControl.cs | 50 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Project/Src/Gui/TextAreaControl.cs b/Project/Src/Gui/TextAreaControl.cs index c6d624c..d7241b6 100644 --- a/Project/Src/Gui/TextAreaControl.cs +++ b/Project/Src/Gui/TextAreaControl.cs @@ -35,13 +35,6 @@ public class TextAreaControl : Panel private int[] lineLengthCache; - /// - /// Kept for the lifetime of the control so that measuring line widths does not create a - /// device context per layout pass. Created on first use, because the handle it needs does - /// not exist yet while the control is being constructed. - /// - private Graphics measureGraphics; - private TextEditorControl motherTextEditorControl; private Point scrollToPosOnNextUpdate; @@ -114,12 +107,6 @@ protected override void Dispose(bool disposing) hRuler.Dispose(); hRuler = null; } - - if (measureGraphics != null) - { - measureGraphics.Dispose(); - measureGraphics = null; - } } base.Dispose(disposing); @@ -294,24 +281,37 @@ static ScrollVisibilities GetScrollVisibilities(bool h, bool v) lineLengthCache = new int[lastLineIndex + LineLengthCacheAdditionalSize]; var maxLength = 0; - for (var lineIndex = firstLineIndex; lineIndex <= lastLineIndex; lineIndex++) + + // The device context must not outlive this layout pass: a Graphics from + // Control.CreateGraphics() is bound to the control's current window handle and to a DC + // borrowed from the system cache, so keeping it across calls leaves a stale HDC which + // makes TextRenderer.MeasureText throw "Parameter is not valid". + Graphics measureGraphics = null; + try { - var lineSegment = Document.GetLineSegment(lineIndex); - if (Document.FoldingManager.IsLineVisible(lineIndex)) + for (var lineIndex = firstLineIndex; lineIndex <= lastLineIndex; lineIndex++) { - if (lineLengthCache[lineIndex] > 0) - { - maxLength = Math.Max(maxLength, lineLengthCache[lineIndex]); - } - else + var lineSegment = Document.GetLineSegment(lineIndex); + if (Document.FoldingManager.IsLineVisible(lineIndex)) { - measureGraphics ??= TextArea.CreateGraphics(); - var visualLength = view.GetVisualWidthColumns(measureGraphics, lineSegment); - lineLengthCache[lineIndex] = Math.Max(1, visualLength); - maxLength = Math.Max(maxLength, visualLength); + if (lineLengthCache[lineIndex] > 0) + { + maxLength = Math.Max(maxLength, lineLengthCache[lineIndex]); + } + else + { + measureGraphics ??= TextArea.CreateGraphics(); + var visualLength = view.GetVisualWidthColumns(measureGraphics, lineSegment); + lineLengthCache[lineIndex] = Math.Max(1, visualLength); + maxLength = Math.Max(maxLength, visualLength); + } } } } + finally + { + measureGraphics?.Dispose(); + } var hScrollBarVisible = HScrollBar.Value != 0 || maxLength > visibleColumnCount || (hScrollBarVisibleNow && VScrollBar.IsMouseDown); vScrollBarVisible = vScrollBarVisible || (hScrollBarVisible && hScrollBarWouldCauseVScrollBar);