---
title: "HandyControl pitfalls: custom title bar + tab strip in title bar"
title_en: "HandyControl pitfalls: custom title bar + tab strip in title bar"
description: Three silent-failure pitfalls hit while building a Windows-Terminal-style UI with HandyControl 3.5.1 on .NET 6 WPF — hc:Window requiring an explicit WindowWin10 style, TemplateBinding failing on a ControlTemplate root element, and TabPanel's measure cache being poisoned by the first tab added after Window.Loaded.
sidebar_label: HandyControl title-bar tabs pitfalls
---

# HandyControl pitfalls: custom title bar + tab strip in title bar

> Recorded 2026-06-03, while implementing a Windows-Terminal-style UI in the ClassicTerminal project.
> Environment: HandyControl 3.5.1, .NET 6 WPF, Windows 10 LTSC 2019 (build 17763).
> HC source code: `C:\Users\Leyu_Tester\Desktop\HandyControl`
> Result: `ClassicTerminal/MainWindow.xaml` (commit `c8dcb8e`, dev branch)

---

## Pitfall 1: `hc:Window` custom title bar requires an explicit `WindowWin10` style

`hc:Window` provides `NonClientAreaContent` for embedding custom content into the title bar, but there are two hard requirements. Missing either one **silently falls back to the default Windows title bar** (no error, no warning):

```xml
<!-- ✅ correct -->
<hc:Window Style="{StaticResource WindowWin10}"
           ShowTitle="False" ShowIcon="False"
           NonClientAreaHeight="36"
           NonClientAreaBackground="#FF202020"
           NonClientAreaForeground="#FFAFAFAF">
    <hc:Window.NonClientAreaContent>
        <Grid Height="36">...</Grid>
    </hc:Window.NonClientAreaContent>
</hc:Window>
```

1. You **must** write `Style="{StaticResource WindowWin10}"` explicitly — HC's automatic style application in its static constructor (`StyleProperty.OverrideMetadata` + the `ResourceHelper` static cache) can fail in a real project.
2. You **must not** set the title-bar button color properties as local values on the `<hc:Window>` element: `OtherButtonBackground/Foreground/Hover*`, `CloseButtonBackground/Foreground/Hover*`. Local values take precedence over the Style and interfere with the trigger bindings inside `WindowWin10Template`, causing the whole template to fail to render. To change button colors, override via a Style instead (`BasedOn="{StaticResource WindowWin10}"`).

> For the detailed investigation, see the AI_Inspection_Cleaning_Demo doc:
> `AI_Inspection_Cleaning_Demo\docs\ui\HC-Window-CustomTitleBar-Guide.md`

Bonus knowledge:

- The code-behind partial class must change its base class to `HandyControl.Controls.Window`.
- The `ContentPresenter` hosting `NonClientAreaContent` has `WindowChrome.IsHitTestVisibleInChrome="True"`, so **do not set a Background on the root container** (not even `Transparent`) — in background-less empty areas, hit-testing passes through to the chrome caption, preserving the "drag the window" behavior; elements with a background receive events normally. This is exactly how Windows Terminal makes the empty area next to the tabs draggable.
- When the window is inactive, HC drops the `NonClientAreaContent` opacity to 0.8 (a template trigger) — this is normal behavior.

---

## Pitfall 2: `TemplateBinding` does not work on the root element of a ControlTemplate

While writing a slimmed-down template for `hc:TabControl`, using `hc:TabPanel` directly as the template root made every `TemplateBinding` (`TabItemWidth`, `TabItemHeight`, `IsTabFillEnabled`) **fail silently** — the panel never received the width/height settings, and tabs measured to 0 width and disappeared.

```xml
<!-- ❌ TabPanel as template root: every TemplateBinding fails -->
<ControlTemplate TargetType="hc:TabControl">
    <hc:TabPanel x:Name="PART_HeaderPanel"
                 TabItemWidth="{TemplateBinding TabItemWidth}" .../>
</ControlTemplate>

<!-- ✅ just wrap it in a Border -->
<ControlTemplate TargetType="hc:TabControl">
    <Border>
        <hc:TabPanel x:Name="PART_HeaderPanel"
                     MinHeight="{TemplateBinding TabItemHeight}"
                     TabItemHeight="{TemplateBinding TabItemHeight}"
                     TabItemWidth="{TemplateBinding TabItemWidth}"
                     IsTabFillEnabled="{TemplateBinding IsTabFillEnabled}"
                     HorizontalAlignment="Left" IsItemsHost="True"/>
    </Border>
</ControlTemplate>
```

This is an existing WPF limitation (not an HC bug); HC's own `TabControlPlusTemplate` also wraps the TabPanel in a Border. When writing a custom template, keeping the `PART_HeaderPanel` name is enough to keep HC's drag-to-reorder and FluidMove animation working (`TabControl.OnApplyTemplate` null-checks all the other PARTs, so they can be omitted).

---

## Pitfall 3: `TabPanel`'s measure cache gets poisoned by the first tab added after the window is loaded

**Symptom**: the tab containers are generated correctly, the styles are correct, and `TabPanel`'s internal `_oldSize` computes the correct 190×36 — but `TabPanel.DesiredSize` stays stuck at `0,36` → the whole tab strip has 0 width and is invisible. Forcing `InvalidateMeasure()`, or even setting `ForceUpdate = true` via reflection, cannot recover it.

**Root cause** (HC `TabPanel.cs`):

```csharp
public TabPanel()
{
    Loaded += (s, e) =>
    {
        if (_isLoaded) return;
        ForceUpdate = true;
        Measure(new Size(DesiredSize.Width, ActualHeight));  // ← the culprit
        ForceUpdate = false;
        ...
    };
}
```

`TabPanel.Loaded` re-measures itself **using its own current `DesiredSize` as the measure constraint**. Typical HC usage (tabs exist in XAML or before Loaded) is fine; but our first tab is created in `Window.Loaded`, and WPF broadcasts Loaded parent-first — by the time `TabPanel.Loaded` fires, the panel is still empty (`DesiredSize.Width == 0`), so it **measures itself with a width constraint of 0**:

1. WPF's `MeasureCore` clamps `DesiredSize` to the measure constraint → DesiredSize gets clamped to 0.
2. An element's measure constraint is cached; subsequent re-measures triggered by `InvalidateMeasure()` still reuse **the previous constraint (0)**.
3. The parents (Border / TabControl) have valid measures of their own and will never re-measure this panel with a proper constraint (∞) again → permanently stuck at 0 width.

**Fix**: after creating a tab, invalidate the panel **together with its entire ancestor chain up to the TabControl**, forcing the parents to re-measure with a clean constraint:

```csharp
/// <summary>Call after creating a tab; repairs HC TabPanel's poisoned measure cache.</summary>
private void RefreshTabStrip() {
    Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => {
        DependencyObject d = FindVisualChild<HandyControl.Controls.TabPanel>(tabControl);
        while (d is UIElement el) {
            el.InvalidateMeasure();
            if (ReferenceEquals(d, tabControl)) break;
            d = VisualTreeHelper.GetParent(d);
        }
    }));
}
```

**Debugging takeaway**: looking only at `ActualWidth`/`ActualHeight` is misleading (arranged children look normal); the key indicator is the mismatch between `DesiredSize` and the internal `_oldSize` — `_oldSize=190,36` but `DesiredSize=0,36` means `MeasureOverride`'s return value was clamped away by `MeasureCore`'s constraint. The problem lies in "who measured the panel with what constraint", not in the panel computing the wrong size itself.

---

## Side notes (not pitfalls, but useful)

- **The `hc:` namespace already covers `HandyControl.Interactivity`** — `hc:ControlCommands.Close`, `hc:Interaction.Behaviors`, and `hc:FluidMoveBehavior` are all usable with the `hc:` prefix directly; no extra `clr-namespace` declaration needed.
- **Close button in a custom TabItem template**: attaching `Command="hc:ControlCommands.Close"` reuses HC's close pipeline (fires the `Closing`/`Closed` routed events, automatically removes from ItemsSource) — no custom click handler needed.
- **`hc:TabPanel` tab-width mechanism**: with `IsTabFillEnabled="False"`, tabs use a fixed width (`TabItemWidth`, default 200); with `True`, the TabControl's full width is divided evenly. Windows-Terminal style uses fixed width + `HorizontalAlignment="Left"`.
- **Middle-click to close a tab**: set `CanBeClosedByMiddleButton="True"` on `hc:TabControl`.
- **HC TabItem's drag-to-reorder** lives in the mouse-event handlers in `TabItem.cs` (at the container level), independent of the template — completely rewriting the ControlTemplate does not break dragging.
