首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

WPF/Silverlight 圖表開發極致:完全在 XAML 中用 DataTemplate 建構折線圖

     編輯:關於.NET

前言:為什麼堅持「全 XAML」定義圖表?

WPF/Silverlight 的核心優勢在於 分離關注點:UI 結構用 XAML,邏輯用 Code-behind。ItemsControl + DataTemplate 是這套哲學的集大成者——任何集合資料,透過 DataTemplate 即可在標記中定義「每一筆資料長什麼樣」。

本文挑戰最複雜的 折線圖:需連接離散點、座標軸動態刻度、多種資料型別 (數值/日期時間)。目標:零代碼繪圖邏輯,純 XAML 組合出專業級折線圖。

折線圖的三大難題

  1. 點與線的分離:DataTemplate 只能存取單一資料項,但 Polyline 需要 PointCollection (所有點) — 如何跨項存取?
  2. 座標轉換:資料值 → 畫布座標,需知圖表寬高、資料極值、軸策略 (是否含零、對稱、自動刻距)
  3. 座標軸本身也是集合:刻度標籤、格線位移皆需動態計算

架構重構:LineChartGenerator — 純 XAML 可用的資料轉換引擎

輸入:ItemsSource (業務物件集合) + Width/Height + HorizontalAxis/VerticalAxis 策略

輸出四個可繫結集合:

  • Points : PointCollection → 供 Polyline 繪製連線
  • ItemPoints : ItemPointCollection (含 Item 原始物件 + Point 座標) → 供資料點 DataTemplate 定位
  • HorizontalAxis.AxisItems : AxisItemCollection (含 Item 標籤值 + Offset 偏移) → 供 X 軸 ItemsControl
  • VerticalAxis.AxisItems : 同理 → 供 Y 軸 ItemsControl

佈局:4x4 Grid,四個 ItemsControl 分佔儲存格,共享寬高繫結確保對齊。

範例一:美國人口普查折線圖 (數值軸)

資料模型

struct CensusDatum { int Year; int Population; }
class CensusData : ObservableCollection<CensusDatum> { ... } // 1790-2010 十年一次資料

XAML 資源定義

<Window.Resources>
  <src:CensusData x:Key="censusData" />

  <charts:LineChartGenerator
    x:Key="generator"
    ItemsSource="{Binding Source={StaticResource censusData}}"
    Width="300" Height="200">

    <charts:LineChartGenerator.HorizontalAxis>
      <charts:AutoAxis PropertyName="Year" />
    </charts:LineChartGenerator.HorizontalAxis>

    <charts:LineChartGenerator.VerticalAxis>
      <charts:IncrementAxis PropertyName="Population"
        Increment="50000000" IsFlipped="True" />
    </charts:LineChartGenerator.VerticalAxis>
  </charts:LineChartGenerator>
</Window.Resources>
  • AutoAxis:直接使用資料實際值 (適合年份)
  • IncrementAxis:固定刻距 (5000 萬),IsFlipped="True" 讓 Y 軸由下往上增長 (符合直覺)

主圖表區:Polyline + 資料點 ItemsControl

<Grid Width="{Binding Source={StaticResource generator}, Path=Width}"
      Height="{Binding Source={StaticResource generator}, Path=Height}">

  <!-- 連線 -->
  <Polyline Points="{Binding Source={StaticResource generator}, Path=Points}"
            Stroke="Blue" />

  <!-- 資料點 -->
  <ItemsControl ItemsSource="{Binding Source={StaticResource generator}, Path=ItemPoints}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate><Grid IsItemsHost="True" /></ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Path Fill="Red" RenderTransform="2 0 0 2 0 0">
          <Path.Data>
            <EllipseGeometry Center="{Binding Point}" RadiusX="4" RadiusY="4"
                             Transform="0.5 0 0 0.5 0 0" />
          </Path.Data>
          <Path.ToolTip>
            <StackPanel Orientation="Horizontal">
              <TextBlock Text="{Binding Item.Year}" />
              <TextBlock Text="{Binding Item.Population, StringFormat=': {0:N0}'}" />
            </StackPanel>
          </Path.ToolTip>
        </Path>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</Grid>

關鍵細節:EllipseGeometry.Transform 抵銷 Path.RenderTransform 縮放,避免邊緣被裁切。

座標軸 ItemsControl (以 X 軸為例)

<ItemsControl Grid.Row="2" Grid.Column="1" Margin="4 0"
    ItemsSource="{Binding Source={StaticResource generator}, Path=HorizontalAxis.AxisItems}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate><Grid IsItemsHost="True" /></ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <StackPanel>
        <Line Y2="10" Stroke="Black" />  <!-- 刻度線 -->
        <TextBlock Text="{Binding Item}" FontSize="8"
                   LayoutTransform="0 -1 1 0 0 0"
                   RenderTransform="1 0 0 1 -6 1" />  <!-- 旋轉標籤 -->
        <StackPanel.RenderTransform>
          <TranslateTransform X="{Binding Offset}" />  <!-- 水平定位 -->
        </StackPanel.RenderTransform>
      </StackPanel>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>
PopulationLineChart 成品

資料型別一致性:支援數值、DateTime、XML 節點

核心洞見:IConvertible + DateTime 特例

  • 所有數值型別 + DateTime 皆實作 IConvertible → 可統一轉 double 計算
  • DateTime 無法直接 ToDouble → 用 Ticks (long) 做中介,雙向轉換精確到毫秒
  • XML 節點 (XmlDataProvider 來源) → 先取字串,嘗試 DateTime.TryParseExact (格式 "R","s","u","o"),再轉數值

軸策略體系

abstract class AxisStrategy : DependencyObject
{
    string PropertyName { get; set; }      // 對應資料屬性
    double Margin { get; set; }            // 極值外擴比例
    bool IncludeZero { get; set; }         // 強制含零
    bool IsSymmetricAroundZero { get; set; } // 對稱正負極值
    ObservableCollection<AxisItem> AxisItems { get; } // 輸出集合
    protected abstract void CalculateAxisItems(Type propertyType, ref double min, ref double max);
}
  • AutoAxis:無參數,直接用資料實際極值
  • IncrementAxis:固定 Increment,極值對齊刻距倍數
  • AdaptableIncrementAxis:Increments (預設 1,2,5) + MaximumItems (預設 10) → 自動挑選最佳刻距,不超過最大標籤數
  • ExplicitAxis:開發者直接提供標籤值清單

範例二:雙產品銷售復合圖 (XML 資料 + AdaptableIncrementAxis)

SalesByMonth 雙線圖
  • 資料來源:XmlDataProvider 内嵌 12 月銷售資料
  • 兩個 LineChartGenerator 分別繫結 @Date (X軸) + Widgets/Doodads (Y軸)
  • Y 軸用 AdaptableIncrementAxis IncludeZero="True" 自動適應雙線範圍
  • 資料點區分:三角形 (Path Geometry) vs 方形
  • 連線升級:CanonicalSpline (樣條曲線) 取代直線 Polyline
  • X 軸標籤:StringFormat="MMMM" 顯示完整月份名

範例三:即時溫度監測 — AdaptableDateTimeAxis 自適應時間軸

資料來源:國家氣象服務 API,每小時紐約中央公園溫度。

AdaptableDateTimeAxis 刻距自動選擇邏輯

目標:標籤數 ≤ MaximumItems (預設 10)。

DataTrigger 動態切換日期格式

<DataTemplate>
  <StackPanel HorizontalAlignment="Left">
    <Line Y2="10" Stroke="Black" />
    <TextBlock Name="txtblk" RenderTransform="1 0 0 1 -4 -4">
      <TextBlock.LayoutTransform><RotateTransform Angle="45" /></TextBlock.LayoutTransform>
    </TextBlock>
    <StackPanel.RenderTransform><TranslateTransform X="{Binding Offset}" /></StackPanel.RenderTransform>
  </StackPanel>

  <DataTemplate.Triggers>
    <DataTrigger Binding="{Binding Source={StaticResource generator}, Path=HorizontalAxis.DateTimeInterval}" Value="Second">
      <Setter TargetName="txtblk" Property="Text" Value="{Binding Item, StringFormat='h:mm:ss d MMM yy'}" />
    </DataTrigger>
    <DataTrigger ... Value="Minute"> <Setter ... Value="{Binding Item, StringFormat='h:mm d MMM yy'}" /> </DataTrigger>
    <DataTrigger ... Value="Hour">   <Setter ... Value="{Binding Item, StringFormat='h tt, d MMM yy'}" /> </DataTrigger>
    <DataTrigger ... Value="Day">    <Setter ... Value="{Binding Item, StringFormat='d'}" /> </DataTrigger>
    <DataTrigger ... Value="Month">  <Setter ... Value="{Binding Item, StringFormat='MMM yy'}" /> </DataTrigger>
    <DataTrigger ... Value="Year">   <Setter ... Value="{Binding Item, StringFormat='MMMM'}" /> </DataTrigger>
  </DataTemplate.Triggers>
</DataTemplate>
TemperatureHistory 數小時
TemperatureHistory 數天

隨時間跨度增大,X 軸自動從「時:分:秒」切換到「月份」,零代碼介入。

原始碼下載

完整 Visual Studio 方案 (含 LineChartLib + 3 Demo):
https://code.msdn.microsoft.com/mag201001Charts

總結:DataTemplate 驅動圖表的優勢

  • 設計師友善:Blend 可視化編輯樣板、觸發器、動畫
  • 關注點分離:資料轉換邏輯封裝在 Generator/Strategy,XAML 只管呈現
  • 極致彈性:換資料點形狀、連線樣條、軸標籤旋轉、Tooltip、動畫 — 全在 DataTemplate 一行行微調
  • 效能:WPF 保留模式圖形,虛擬化支援,大量資料點仍流暢

這不只是折線圖技巧,更是 「對 XAML 進行編碼」 思維的典範:盡可能將命令式邏輯轉化為宣告式標記,讓框架幫你做繁重工作。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
時間範圍嘗試刻距序列
秒級1, 2, 5, 15, 30 秒
分級1, 2, 5, 15, 30 分鐘
時級1, 2, 4, 6, 12 小時
日級1, 2, 5, 10 天
月級1, 2, 4, 6 月
年級1, 2, 5, 10, 20, 50 年...