組合控件是 WPF 自訂控件的主流形式:以現有控件組合、樣式模板、視覺樹操作構建複雜 UI。本文以「中國象棋聯機對戰」為例,完整展示從棋盤繪製、棋子拖曳動畫、擊測選取、網路通訊到 MVVM 視窗關閉通知的工程級實作。
棋盤背景使用 DrawingVisual 繪製,棋子為獨立 UIElement 加入視覺樹:
// 建立棋盤視覺資料
var boardVisual = new DrawingVisual();
using (var dc = boardVisual.RenderOpen()) { DrawBoard(dc); }
// 覆寫視覺樹介面
protected override int VisualChildrenCount => 1 + Chessmen.Count; // 棋盤 + 棋子
protected override Visual GetVisualChild(int index)
=> index == 0 ? boardVisual : Chessmen[index - 1];
MeasureOverride/ArrangeOverride 負責棋子佈局:
protected override Size MeasureOverride(Size available)
{
foreach (var c in Chessmen) c.Measure(available);
return new Size(CellWidth * 10, CellWidth * 11);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var c in Chessmen)
c.Arrange(new Rect(
new Point(c.Location.X * CellWidth - c.DesiredSize.Width/2 + CellWidth,
c.Location.Y * CellWidth - c.DesiredSize.Height/2 + CellWidth),
c.DesiredSize));
return DesiredSize;
}
棋子繼承棋盤 FontFamily 等屬性,利用 DependencyProperty.AddOwner + FrameworkPropertyMetadataOptions.Inherits:
// 棋盤註冊
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily", ...,
new FrameworkPropertyMetadata(SystemFonts.MessageFontFamily, FrameworkPropertyMetadataOptions.Inherits));
// 棋子接管
public static readonly DependencyProperty FontFamilyProperty =
ChineseChessboard.FontFamilyProperty.AddOwner(typeof(ChessmanControl), ...);
OnMouseLeftButtonDown:VisualTreeHelper.HitTest 取得棋子。TranslateTransform 套用至棋子 RenderTransform。DoubleAnimation(X/Y)移動至目標格,FillBehavior.Stop 避免動畫結束殘留。RenderTransform、更新棋子集合、呼叫 InvalidateArrange() 觸發實際佈局。ScaleTransform(-1,1)、ScaleTransform(1,-1)、ScaleTransform(-1,-1) 鏡像得到四象限。FormattedText.MaxTextWidth = FontSize 擠壓換行,旋轉時修正長方形邊界偏移。BitmapImage + pack://application:,,,/Assembly;component/Images/xxx.jpg + Freeze() 提升跨執行緒效能。DispatcherTimer 定時觸發,CompositionTarget.Rendering 逐幀切換 Opacity 閃爍最後移動棋子,超時或換方自動停止。
Socket.SendTo 目標 IPAddress.Broadcast。UdpClient + ReuseAddress 綁定,背景執行緒 Receive 迴圈。Bind 直到成功(可改良為 Port 0 自動分配)。Serialization/Deserialization,資料類別標記 [Serializable]。問題:View 事件(Closing)如何綁定 ViewModel 命令?
解法:附加屬性 WindowRegionMetadata.BeforeClose 綁定 VM PersistAction,App.OnStartup 改用 WindowRegionBehavior.CreateWindow 建立視窗並注入 DataContext。
文章附上完整原始碼下載,包含:ChineseChessboard、ChessmanControl、UDPClass、WindowRegionBehavior、View/ViewModel、Shell 視窗等,可直接編譯運行體驗雙人聯機對戰。
組合控件開發核心在於:視覺樹承載繪圖、邏輯樹傳遞屬性、擊測實現互動、動畫豐富體驗、網路實現聯機、MVVM 解耦業務。象棋專案雖簡單,但涵蓋 WPF 控件開發所有關鍵技術點,是絕佳的進階練習題材。