# 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.

- Skill: `dotnet/using-and-extending-gdi-plus` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dotnet/using-and-extending-gdi-plus`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dotnet/using-and-extending-gdi-plus/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: .NET (Microsoft) (https://skillmd.com/u/dotnet)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dotnet/using-and-extending-gdi-plus

---


# 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)

