洋洋灑灑幾千言以後,在前一篇文章的最後終於看到一絲曙光了— 至少有一個看起來像直方圖的玩意了。使用ItemsControl來實現直方圖有以下幾個優點:
1. 省去了手工佈局X軸座標上刻度的問題,否則的話,我們必須寫類似下面的代碼來佈局X軸座標的刻度。
double tickMarkWidth = LineChart.ActualWidth / CategoryTickMarks.Count;
double left = 0.0;
foreach (TickMark mark in CategoryTickMarks)
{
Canvas.SetLeft(mark, left);
// 順序排列X座標
left += tickMarkWidth;
}
2. 可以利用DataBinding的機制使用最少的代碼來提供放大、縮小的功能。
3. 使用ItemsControl,可以通過替換顯示座標軸的座標的DataTemplate來達到自定義的座標軸效果。
因爲前一篇文章裏面的直方圖是將數據硬編碼在Xaml文件裏面的,因此這一次我們要使用程序計算出各個矩形的高度,這個計算我們可以通過將前一篇文章DataTemplate裏面的Rectangle的高度綁定到圖表的數據上面,然後通過轉換器(IConverter)來將數據轉換成Rectangle的高度:
internal class ValueToRectangleHeightConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// 獲取傳遞過來的直方圖控件實例的引用
ObjectDataProvider provider = parameter as ObjectDataProvider;
Histogram histogram = provider.ObjectInstance as Histogram;
Debug.Assert(histogram != null);
double number = (double)value;
// 加一個0.9是不想讓直方圖數據的最大值頂到了直方圖的最上層。
return histogram.ActualHeight * 0.9 * number / (histogram.Maximum - histogram.Minimum);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
至於直方圖刻度的寬度,可以使用一個變量來保存,因爲我們需要給用戶一個選擇,他既可以讓我們的直方圖控件自動計算出一個合適的值,也可以設置自定義的值,當然,每次用戶代碼修改刻度的寬度,我們都希望自動將直方圖重繪:
/// <summary>
/// 獲取和設置直方圖橫座標上每一個刻度的寬度。
/// </summary>
public double TickMarkWidth
{
get { return (double)GetValue(TickMarkWidthProperty); }
set { SetValue(TickMarkWidthProperty, value); }
}
public static readonly DependencyProperty TickMarkWidthProperty =
DependencyProperty.Register("TickMarkWidth", typeof(double), typeof(Histogram), new UIPropertyMetadata(0.0d));
我們希望直方圖能夠在可視區域顯示所有的數據,所以需要知道直方圖所表示的數據範圍,然後在顯示數據的時候按比例顯示每一個矩形:
internal double Maximum = 0.0d;
internal double Minimum = 0.0d;
因爲我們希望支持多種格式的數據,因此最好在直方圖控件內部採用一種統一的格式,這樣方便我們編程:
/// <summary>
/// 因爲我們打算支持很多種不同的數據,例如數組,Dictionary以及其他的可以解釋成
/// 鍵值對的數組,所以直方圖內部最好有一個統一格式的數據結構來表示這些不同的數據。
/// 至於數據格式之間的轉換問題,我們可以通過採用Adapter模式來做。
/// </summary>
internal class HistogramDataObject
{
public object Category { get; set; }
public double Value { get; set; }
}
在Xaml中,我們就可以通過重載ItemsControl的ItemsPanel和ItemTemplate屬性來繪製一個直方圖了:
<ItemsControl ItemsSource="{Binding}" Padding="0" Margin="0"
HorizontalContentAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel HorizontalAlignment="Stretch" Orientation="Horizontal" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--
爲了節省計算時間,我們在每次給直方圖控件賦值的時候,計算出直方圖控件的每一個刻度的寬度來。
-->
<Grid Margin="0" Width="{Binding ElementName=HistogramControl, Path=TickMarkWidth}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" VerticalAlignment="Bottom">
<Rectangle Fill="Gray" ToolTip="{Binding Path=Value}">
<Rectangle.Height>
<Binding Path="Value"
Converter="{StaticResource valueToRectangleHeightConverter}"
ConverterParameter="{StaticResource histogramSelfInstance}" />
</Rectangle.Height>
</Rectangle>
</StackPanel>
<Label Margin="0" Padding="2 0 2 0" Grid.Row="1" BorderBrush="Gray" HorizontalContentAlignment="Center"
BorderThickness="0 0 1 0" Content="{Binding Path=Category}" Foreground="Gray" VerticalAlignment="Top" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
下面的就是效果截圖:
下面是完整的源代碼:
Window1.xaml:
<Window x:Class="CodeLibrary.Charts.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CodeLibrary.Charts"
Loaded="Window_Loaded"
Unloaded="Window_Unloaded"
Title="Window1" Height="600" Width="800">
<local:Histogram x:Name="TestHistogram"/>
</Window>
Window1.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.ComponentModel;
namespace CodeLibrary.Charts
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
double[] test = new double[256];
double[] test2 = new double[256];
double[] test3 = new double[256];
Random random = new Random(1000);
for (int i = 0; i < test.Length; ++i)
{
test[i] = random.Next();
test2[i] = random.Next();
test3[i] = random.Next();
}
TestHistogram.HistogramData = test;
TestHistogram.TickMarkWidth = 30;
}
}
}
Histogram.xaml:
<UserControl x:Class="CodeLibrary.Charts.Histogram"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CodeLibrary.Charts" x:Name="HistogramControl">
<UserControl.Resources>
<!--
這個Converter用來將直方圖裏面的數據轉換成矩形的高度
-->
<local:ValueToRectangleHeightConverter x:Key="valueToRectangleHeightConverter" />
<!--
在計算直方圖每一項矩形的高度的時候,需要得到當前Histogram控件的高度,以及它所表示的
值得範圍,因此我們需要將當前的Histogram控件傳遞給ValueToRectangleHeightConverter。
可以通過ObjectDataProvider來做,然後在Histogram控件的構造函數裏面將自身的引用保存下來
-->
<ObjectDataProvider x:Key="histogramSelfInstance" />
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding}" Padding="0" Margin="0"
HorizontalContentAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel HorizontalAlignment="Stretch" Orientation="Horizontal" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--
爲了節省計算時間,我們在每次給直方圖控件賦值的時候,計算出直方圖控件的每一個刻度的寬度來。
-->
<Grid Margin="0" Width="{Binding ElementName=HistogramControl, Path=TickMarkWidth}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" VerticalAlignment="Bottom">
<Rectangle Fill="Gray" ToolTip="{Binding Path=Value}" Stroke="White" StrokeThickness="1">
<Rectangle.Height>
<Binding Path="Value"
Converter="{StaticResource valueToRectangleHeightConverter}"
ConverterParameter="{StaticResource histogramSelfInstance}" />
</Rectangle.Height>
</Rectangle>
</StackPanel>
<Label Margin="0" Padding="2 0 2 0" Grid.Row="1" BorderBrush="Gray" HorizontalContentAlignment="Center"
BorderThickness="0 0 1 0" Content="{Binding Path=Category}" Foreground="Gray" VerticalAlignment="Top" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
Histogram.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Collections;
using System.IO;
using System.Threading;
namespace CodeLibrary.Charts
{
public partial class Histogram : UserControl
{
public object HistogramData
{
get { return (object)GetValue(HistogramDataProperty); }
set { SetValue(HistogramDataProperty, value); }
}
public static readonly DependencyProperty HistogramDataProperty =
DependencyProperty.Register("HistogramData", typeof(object), typeof(Histogram), new UIPropertyMetadata(null, new PropertyChangedCallback(HistogramDataChanged)));
/// <summary>
/// 獲取和設置直方圖橫座標上每一個刻度的寬度。
/// </summary>
public double TickMarkWidth
{
get { return (double)GetValue(TickMarkWidthProperty); }
set { SetValue(TickMarkWidthProperty, value); }
}
public static readonly DependencyProperty TickMarkWidthProperty =
DependencyProperty.Register("TickMarkWidth", typeof(double), typeof(Histogram), new UIPropertyMetadata(0.0d));
/// <summary>
/// 獲取和設置直方圖中數據的最大值,因爲我們希望在直方圖的可視區域裏面顯示所有的矩形,
/// 獲取數據的最大值和最小值之後,其他的值我們可以按照比例放置,這樣就能顯示直方圖中所有的數據了。
///
/// 之所以設置成internal,是因爲我們希望在同一個Assembly裏面的Converter可以訪問,但是不期望用戶代碼
/// 能夠訪問到他們--反射除外。
/// </summary>
internal double Maximum = 0.0d;
internal double Minimum = 0.0d;
/// <summary>
/// 因爲我們打算支持很多種不同的數據,例如數組,Dictionary以及其他的可以解釋成
/// 鍵值對的數組,所以直方圖內部最好有一個統一格式的數據結構來表示這些不同的數據。
/// 至於數據格式之間的轉換問題,我們可以通過採用Adapter模式來做。
/// </summary>
internal class HistogramDataObject
{
public object Category { get; set; }
public double Value { get; set; }
}
public Histogram()
{
InitializeComponent();
ObjectDataProvider provider = FindResource("histogramSelfInstance") as ObjectDataProvider;
provider.ObjectInstance = this;
}
private static void HistogramDataChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Histogram histogram = sender as Histogram;
Debug.Assert(histogram != null);
if (e.NewValue is Array)
PopulateArray(histogram, e);
else if (e.NewValue is IDictionary)
PopulateDictionary(histogram, e);
else
throw new NotImplementedException("Supports for objects other than Dictionary and Array are not implemented yet!");
}
private static void PopulateArray(Histogram histogram, DependencyPropertyChangedEventArgs e)
{
Array array = e.NewValue as Array;
Debug.Assert(array != null);
Trace.WriteLineIf(array.Rank > 1, "Warning, multiple dimentional array is treated as 1-D array");
List<HistogramDataObject> histogramDataObjects = new List<HistogramDataObject>();
int index = 0;
foreach (object a in array)
{
if (!(a is IConvertible))
throw new InvalidDataException("Only arrays which host IComparable object are supported");
histogramDataObjects.Add(new HistogramDataObject() { Category = index++, Value = ((IConvertible)a).ToDouble(Thread.CurrentThread.CurrentUICulture) });
}
// 重新計算直方圖數據的範圍
histogram.Maximum = histogramDataObjects.Max(h => h.Value);
histogram.Minimum = histogramDataObjects.Min(h => h.Value);
// 計算每一個刻度的寬度
double tickWidth = histogram.ActualWidth / (histogramDataObjects.Count + 1);
histogram.TickMarkWidth = tickWidth > 1 ? tickWidth : 1;
// 強制直方圖更新視圖
histogram.DataContext = histogramDataObjects;
}
private static void PopulateDictionary(Histogram histogram, DependencyPropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
}
}
Converters.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
namespace CodeLibrary.Charts
{
internal class ValueToRectangleHeightConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// 獲取傳遞過來的直方圖控件實例的引用
ObjectDataProvider provider = parameter as ObjectDataProvider;
Histogram histogram = provider.ObjectInstance as Histogram;
Debug.Assert(histogram != null);
double number = (double)value;
// 加一個0.9是不想讓直方圖數據的最大值頂到了直方圖的最上層。
return histogram.ActualHeight * 0.9 * number / (histogram.Maximum - histogram.Minimum);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}