Skip to main content

using-and-extending-gdi-plus

Instructions for using GDI, GDI+ for drawing primitives, shapes and processing/rendering bitmaps and rendering Fonts. Also, tenets for adding new APIs to System.Drawing for modernization and improving drawing, imaging, and font features.

소스 정보

저장소
dotnet/winforms
최근 소스 활동
2026년 3월 27일 18:32
감지된 SKILL.md 언어
영어
스타
4,867
포크
1,122

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
using-and-extending-gdi-plus
description
Instructions for using GDI, GDI+ for drawing primitives, shapes and processing/rendering bitmaps and rendering Fonts. Also, tenets for adding new APIs to System.Drawing for modernization and improving drawing, imaging, and font features.
metadata
{"author":"dotnet-winforms","version":"2.0"}
# Using and Extending GDI+ in WinForms These rules apply when **using GDI+ for drawing** in the WinForms runtime and when **adding new drawing APIs** to `System.Drawing`. For control-level API additions (properties, events, etc.), see the `new-control-api` skill. For writing tests for rendering APIs, see the `gdi-rendering-tests` skill. > **Golden rule:** Cache what you can, dispose what you must, and always restore > graphics state after modifying it. --- ## 1. Object Caching for Performance ### 1.1 Cached Pen and SolidBrush objects WinForms provides a ref-counted caching mechanism for `Pen` and `SolidBrush` objects (`RefCountedCache<TObject, TCacheEntryData, TKey>`). Cached objects have implicit conversions that let them behave like their GDI+ counterparts while reducing allocation overhead. ### 1.2 Prefer SystemPens and SystemBrushes `SystemPens` and `SystemBrushes` already provide cached objects. Always prioritize their usage (unless you need a non-default pen width). Objects obtained from these APIs **must not** be disposed: ```csharp // Good — already cached via SystemPens. e.Graphics.DrawLine(SystemPens.ButtonHighlight, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); ``` ### 1.3 Using scoped cached objects When `SystemPens`/`SystemBrushes` do not have the color you need, prefer cached scopes over direct instantiation: ```csharp // INCORRECT — direct instantiation. using (var pen = new Pen(Color.Red)) { g.DrawLine(pen, p1, p2); } // CORRECT — cached scope. using var pen = Color.Red.GetCachedPenScope(); g.DrawLine(pen, p1, p2); ``` Available cached scopes: ```csharp // Default-width pen (width = 1) using var pen = color.GetCachedPenScope(); // Custom-width pen (integer only) using var thickPen = color.GetCachedPenScope(2); // SolidBrush using var brush = color.GetCachedSolidBrushScope(); ``` Always use `var` for the scope type and always apply `using`. **When caching is not possible:** If the pen needs additional configuration during its lifetime (e.g., `DashStyle`, `CustomStartCap`, `Inset`), you must use a non-cached pen/brush and dispose it explicitly. ### 1.4 Refactoring helpers to return scoped objects When refactoring methods that return `Pen` or `SolidBrush`: * For **private** helpers — change the return type to the scope type directly. * For **public/internal** helpers — add a new scope-returning method and preserve the original. ```csharp // Before private Pen GetHighlightPen() => new Pen(SystemColors.Highlight); // After private PenCache.Scope GetHighlightPenScope() => SystemColors.Highlight.GetCachedPenScope(); ``` --- ## 2. GraphicsInternal Usage ### 2.1 When to use GraphicsInternal In WinForms control painting, prefer `e.GraphicsInternal` over `e.Graphics` for performance. `GraphicsInternal` avoids unnecessary state saves: ```csharp void Paint(PaintEventArgs e) { e.GraphicsInternal.DrawRectangle(pen, rect); } ``` **Caveat:** Do not pass `GraphicsInternal` to other methods — callees cannot distinguish it from a regular `Graphics` instance. ### 2.2 State management with GraphicsInternal If you must modify the clip or transform, save and restore state: ```csharp GraphicsState? previousState = null; try { previousState = graphicsInternal.Save(); graphicsInternal.TranslateTransform(x, y); graphicsInternal.SetClip(rect); // … draw … } finally { if (previousState is not null) { graphicsInternal.Restore(previousState); } } ``` --- ## 3. Graphics Quality Settings When changing quality settings, **always** restore the original value: ```csharp SmoothingMode originalMode = g.SmoothingMode; try { g.SmoothingMode = SmoothingMode.AntiAlias; // … draw … } finally { g.SmoothingMode = originalMode; } ``` Settings that must be preserved and restored: * `SmoothingMode` * `TextRenderingHint` * `InterpolationMode` * `CompositingQuality` * `PixelOffsetMode` --- ## 4. Resource Management ### 4.1 Disposable objects Always dispose GDI+ objects that cannot be cached: ```csharp using var customPen = new Pen(Color.Red) { DashStyle = DashStyle.Dash }; g.DrawLine(customPen, p1, p2); ``` ### 4.2 GraphicsPath objects Never cache `GraphicsPath` objects — always create, use, and dispose locally: ```csharp using GraphicsPath path = new(); path.AddEllipse(rect); g.FillPath(brush, path); ``` ### 4.3 Avoid premature disposal Ensure objects remain valid throughout their usage. Do not pass a scoped object to something that may use it after the scope ends: ```csharp // INCORRECT — brush may be used after scope ends. using (var brush = color.GetCachedSolidBrushScope()) { someObject.SomeFutureOperation(brush); } // CORRECT — immediate use within scope. using var brush = color.GetCachedSolidBrushScope(); g.FillRectangle(brush, rect); ``` --- ## 5. Adding New Drawing Primitives to Graphics When adding new drawing APIs (e.g., `DrawXxx` / `FillXxx` methods) to the `Graphics` class or new path operations to `GraphicsPath`, follow the established pattern from the `RoundedRectangle` API addition. ### 5.1 .NET version guard — mandatory All new public APIs in `System.Drawing` **must** be guarded with a preprocessor directive for the target .NET version. Currently, new APIs target at least **.NET 11**: ```csharp #if NET11_0_OR_GREATER /// <summary> /// Draws the outline of the specified rounded rectangle. /// </summary> public void DrawRoundedRectangle(Pen pen, RectangleF rect, SizeF corner) { using GraphicsPath path = new(); path.AddRoundedRectangle(rect, corner); DrawPath(pen, path); } #endif ``` > **Why?** The `System.Drawing.Common` package ships as part of the shared > framework. Version guards ensure new APIs are only available on the .NET > version they were approved for, preventing accidental use on older runtimes, > specifically, when we need to service parts of main at a later point in > time back into an earlier version, or if we're including for a new version, > whose branch has not snapped, yet. ### 5.2 Provide both integer and float overloads Every new drawing primitive must have two public overloads: 1. An **integer overload** (`Rectangle`, `Point`, `Size`) that delegates to the float overload. 2. A **float overload** (`RectangleF`, `PointF`, `SizeF`) with the actual implementation. The integer overload uses `<inheritdoc cref="..."/>` to inherit documentation from the float overload: ```csharp #if NET11_0_OR_GREATER /// <inheritdoc cref="DrawRoundedRectangle(Pen, RectangleF, SizeF)"/> public void DrawRoundedRectangle(Pen pen, Rectangle rect, Size corner) => DrawRoundedRectangle(pen, (RectangleF)rect, corner); /// <summary> /// Draws the outline of the specified rounded rectangle. /// </summary> /// <param name="pen">The <see cref="Pen"/> to draw the outline with.</param> /// <param name="rect">The bounds of the rounded rectangle.</param> /// <param name="corner"> /// The size of the ellipse used to round the corners of the rectangle. /// </param> public void DrawRoundedRectangle(Pen pen, RectangleF rect, SizeF corner) { using GraphicsPath path = new(); path.AddRoundedRectangle(rect, corner); DrawPath(pen, path); } #endif ``` ### 5.3 Provide Draw and Fill pairs When adding a new shape primitive, provide both `Draw` (outline) and `Fill` (interior) methods. The `Fill` variant takes a `Brush` instead of a `Pen`: ```csharp #if NET11_0_OR_GREATER public void FillRoundedRectangle(Brush brush, RectangleF rect, SizeF corner) { using GraphicsPath path = new(); path.AddRoundedRectangle(rect, corner); FillPath(brush, path); } #endif ``` ### 5.4 Add corresponding GraphicsPath method If the new primitive is path-based, add the `Add[Shape]` method to `GraphicsPath` as well, using the same version guard and integer/float overload pattern: ```csharp #if NET11_0_OR_GREATER /// <inheritdoc cref="AddRoundedRectangle(RectangleF, SizeF)"/> public void AddRoundedRectangle(Rectangle rect, Size corner) => AddRoundedRectangle((RectangleF)rect, corner); /// <summary> /// Adds a rounded rectangle to this path. /// </summary> /// <param name="rect">The bounds of the rectangle to add.</param> /// <param name="corner"> /// The size of the ellipse used to round the corners of the rectangle. /// </param> public void AddRoundedRectangle(RectangleF rect, SizeF corner) { StartFigure(); // … arc calls for each corner … CloseFigure(); } #endif ``` ### 5.5 XML documentation * The **float overload** carries the full `<summary>`, `<param>`, and optional `<remarks>` documentation. * The **integer overload** uses `<inheritdoc cref="FloatOverload"/>` to avoid duplicating docs. * Use `<see cref="..."/>` for cross-references to `Pen`, `Brush`, `GraphicsPath`, etc. ### 5.6 Placement in source files Place new methods **adjacent to existing related methods**: * `DrawRoundedRectangle` next to `DrawRectangle`. * `FillRoundedRectangle` next to `FillRectangle`. * `AddRoundedRectangle` next to `AddRectangle` / `AddRectangles`. --- ## 6. Complete Examples ### Basic rendering with cached resources ```csharp private void PaintControl(PaintEventArgs e) { var g = e.GraphicsInternal; using var borderPen = SystemColors.ActiveBorder.GetCachedPenScope(); using var fillBrush = SystemColors.Control.GetCachedSolidBrushScope(); g.FillRectangle(fillBrush, ClientRectangle); g.DrawRectangle(borderPen, 0, 0, Width - 1, Height - 1); } ``` ### Complex rendering with state management ```csharp private void DrawComplexControl(PaintEventArgs e) { GraphicsState? previousState = null; SmoothingMode originalMode = SmoothingMode.Default; try { var g = e.GraphicsInternal; previousState = g.Save(); originalMode = g.SmoothingMode; g.SmoothingMode = SmoothingMode.AntiAlias; g.TranslateTransform(10, 10); using var pen = Color.Blue.GetCachedPenScope(2); using var brush = Color.LightBlue.GetCachedSolidBrushScope(); using GraphicsPath path = new(); path.AddEllipse(0, 0, 100, 50); g.FillPath(brush, path); g.DrawPath(pen, path); } finally { if (previousState is not null) { e.GraphicsInternal.Restore(previousState); } e.GraphicsInternal.SmoothingMode = originalMode; } } ``` --- ## 7. Checklist Before Submitting * [ ] New APIs guarded with `#if NET11_0_OR_GREATER` (or the appropriate target version) * [ ] Both integer and float overloads provided * [ ] Draw and Fill pairs provided for shape primitives * [ ] Corresponding `GraphicsPath.Add[Shape]` method added if path-based * [ ] XML documentation on all new public members * [ ] Integer overload uses `<inheritdoc cref="..."/>` * [ ] Cached pens/brushes used where possible; disposed when not cacheable * [ ] `GraphicsPath` objects created and disposed locally (never cached) * [ ] Graphics state (smoothing, clip, transform) saved and restored * [ ] New methods placed adjacent to related existing methods * [ ] Tests written for both integer and float overloads (see `gdi-rendering-tests` skill)
GitHub에서 보기