WPF 佈局系統雙階段機制是自訂控件正確渲染的基石。本文以可拖曳矩形控件為例,詳細剖析 ScrollViewer 內部原理、GetLayoutClip 裁剪、MeasureOverride/ArrangeOverride 協作流程,並說明 RenderSize、DesiredSize、InvalidateMeasure/Arrange 的實戰用法。
ScrollViewer 模板為 Grid 包含 ScrollContentPresenter 與兩個 ScrollBar。ScrollContentPresenter 將內容視為背景,透過移動位置實現捲動,可視區域外內容被裁剪。任何繼承 UIElement 的控件皆可覆寫 GetLayoutClip 自訂裁剪幾何:
protected override Geometry GetLayoutClip(Size layoutSlotSize)
=> new RectangleGeometry(new Rect(RenderSize));
ScrollContentPresenter 覆寫 ArrangeOverride 將子元素座標偏移至可視區域外:
protected override Size ArrangeOverride(Size arrangeBounds)
{
var child = GetVisualChild(0) as UIElement;
if (child != null)
{
var point = new Point(-HorizontalOffset, -VerticalOffset); // 負偏移
child.Arrange(new Rect(point, child.DesiredSize));
}
return arrangeBounds; // 回傳可用空間大小
}
參數 arrangeBounds 為父容器給予的可用空間,回傳值通常為控件 RenderSize。
DesiredSize(即 ActualWidth/Height 來源)由 MeasureOverride 產生。自訂控件範例:
protected override Size MeasureOverride(Size constraint)
{
var w = Math.Max(double.IsInfinity(constraint.Width) ? RenderSize.Width : constraint.Width,
_previewRect.Right);
var h = Math.Max(double.IsInfinity(constraint.Height) ? RenderSize.Height : constraint.Height,
_previewRect.Bottom);
return new Size(w, h);
}
constraint 為父容器限制尺寸(可能為 Infinity),需自行處理。
Measure → 更新 DesiredSize → 呼叫父容器 OnChildDesiredSizeChanged → 沿視覺樹向上觸發 InvalidateMeasure(DispatcherPriority.Render 非同步)。ArrangeOverride 中避免呼叫自身 Measure,否則導致 InvalidateArrange → Measure 循環。WPF 內部以 MeasureInProgress/MeasureDirty 旗標防死循環。OnRenderSizeChanged 處理。OnRender 中繪製背景、邊界。MeasureOverride 面對 Infinity 限制時,參考 RenderSize 判斷可用空間。| 模式 | 優點 | 缺點 |
|---|---|---|
| 自訂 ScrollBar 即時重繪 | 大量資料僅繪製可視區,記憶體省 | 每次捲動需重繪,CPU 佔用高 |
| ScrollViewer 裁剪模式 | 捲動不重繪,流暢 | 初始載入大量資料慢、記憶體佔用大 |
組合控件中善用 TranslateTransform、ScaleTransform、RotateTransform 搭配 CompositionTarget.Rendering 或動畫類別,可實現豐富動態佈局效果。
文章附上範例專案原始碼,包含可拖曳矩形控件、支援 ScrollViewer 的自訂面板,可直接編譯運行觀察 Measure/Arrange 呼叫順序與裁剪效果。