在进行软件开发时,经常需要将数据以图形的方式展示给用户,这就需要用到数据可视化技术。本文将介绍如何使用DataTemplate和ItemsControl创建一个简单的数据可视化控件。
在设计数据可视化控件时,希望保持项目的简洁性,尽可能少地使用第三方库。因此,选择使用WPF(Windows Presentation Foundation)自带的控件和模板来实现。
控件的核心是DataTemplate,它定义了每一列数据的布局和样式。DataTemplate中使用了MultiBinding和Converter来动态计算布局参数。
以下是DataTemplate的XML代码:
<DataTemplate x:Key="HistogramColumnDataTemplate">
<Grid>
<Grid.Width>
<MultiBinding Converter="{converters:DoubleDivideIntConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
<Binding Path="ItemsSource.Count" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</Grid.Width>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding Value, Converter={converters:LengthPercentageConverter}, ConverterParameter=reverse}" />
<RowDefinition Height="{Binding Value, Converter={converters:LengthPercentageConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="1" Grid.Column="1" Background="Blue" BorderBrush="Black" BorderThickness="1,1,1,0" />
<Border Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Height="1" HorizontalAlignment="Stretch" Background="Black" />
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" FontSize="24" Text="{Binding Key}" TextAlignment="Center" />
</Grid>
</DataTemplate>
这个DataTemplate假设绑定的数据是一个KeyValuePair对象。在DataTemplate的末尾,TextBlock用于显示与列数据关联的值。
控件使用ItemsControl来展示数据,ItemsControl的ItemsPanelTemplate是一个StackPanel,用于水平排列数据项。
以下是ItemsControl的XML代码:
<ItemsControl Grid.Row="1" ItemTemplate="{StaticResource HistogramColumnDataTemplate}" ItemsSource="{Binding HistogramData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
ItemsControl通过绑定HistogramData来展示数据,HistogramData是一个KeyValuePair集合。
为了支持控件的功能,定义了两个转换器:LengthPercentageConverter和DoubleDivideIntConverter。
LengthPercentageConverter是一个IValueConverter,它根据传入的值返回一个GridLength对象。如果参数包含"reverse",则返回1减去传入值的GridLength。
以下是LengthPercentageConverter的C#代码:
public sealed class LengthPercentageConverter : MarkupExtension, IValueConverter
{
public LengthPercentageConverter() { }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool reverse = parameter?.ToString() == "reverse";
if (string.IsNullOrWhiteSpace(value?.ToString()))
return new GridLength(0.5, GridUnitType.Star);
double convertedValue = System.Convert.ToDouble(value);
if (convertedValue >= 1)
return new GridLength(reverse ? 0 : 1, GridUnitType.Star);
if (convertedValue <= 0)
return new GridLength(reverse ? 1 : 0, GridUnitType.Star);
var gridLength = new GridLength(reverse ? 1 - convertedValue : convertedValue, GridUnitType.Star);
return gridLength;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
DoubleDivideIntConverter是一个IMultiValueConverter,用于计算列的宽度。它是控件的ActualWidth除以绑定到ItemsSource的KeyValuePair实例的数量。
以下是DoubleDivideIntConverter的C#代码:
internal class DoubleDivideIntConverter : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (!(values[0] is double) || !(values[1] is int))
return null;
var size = (double)values[0];
var count = (int)values[1];
return size / count;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
ViewModel非常简单,它包含一个HistogramData集合和一个Maximum属性。HistogramData是一个KeyValuePair集合,Maximum用于定义数据的最大值。
以下是ViewModel的C#代码:
public class ViewModel
{
public ViewModel()
{
HistogramData = new[]
{
new KeyValuePair<string, double>("25.0", 0.15),
new KeyValuePair<string, double>("25.5", 0.20),
new KeyValuePair<string, double>("26.0", 0.40),
new KeyValuePair<string, double>("26.5", 0.15),
new KeyValuePair<string, double>("27.0", 0.10)
};
}
public IEnumerable<KeyValuePair<string, double>> HistogramData { get; set; }
public double Maximum => 0.6;
}
当前的实现有一个限制,即数据的尺度始终是0到1.0。为了解决这个问题,可以添加一个尺度Maximum和Minimum。这需要将LengthPercentageConverter转换为IMultiValueConverter,并添加一个新的DataTemplate。
以下是改进后的LengthPercentageConverter的C#代码:
public sealed class LengthPercentageConverter : MarkupExtension, IMultiValueConverter
{
public LengthPercentageConverter() { }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
bool reverse = parameter?.ToString() == "reverse";
if (string.IsNullOrWhiteSpace(values[0].ToString()))
return new GridLength(0.5, GridUnitType.Star);
if (values.Length == 1 || values[1] == DependencyProperty.UnsetValue)
return Convert(values[0], targetType, parameter, culture);
if (string.IsNullOrWhiteSpace(values[1].ToString()))
return new GridLength(0.5, GridUnitType.Star);
double convertedMin = 0;
double convertedMax = System.Convert.ToDouble(values[1]);
if (values.Length > 2 && values[2] != DependencyProperty.UnsetValue)
{
convertedMin = convertedMax;
convertedMax = System.Convert.ToSingle(values[2]);
}
Single convertedValue = System.Convert.ToSingle(values[0]);
if (convertedValue >= convertedMax)
return new GridLength(reverse ? 0 : convertedMax - convertedMin, GridUnitType.Star);
if (convertedValue <= convertedMin)
return new GridLength(reverse ? convertedMax - convertedMin : 0, GridUnitType.Star);
var gridLength = new GridLength(reverse ? convertedMax - convertedMin - convertedValue : convertedValue - convertedMin, GridUnitType.Star);
return gridLength;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
<DataTemplate x:Key="HistogramColumnDataTemplateWithMaximum">
<Grid>
<Grid.Width>
<MultiBinding Converter="{local:DoubleDivideIntConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
<Binding Path="ItemsSource.Count" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</Grid.Width>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition>
<RowDefinition.Height>
<MultiBinding Converter="{local:LengthPercentageConverter}" ConverterParameter="reverse">
<Binding Path="Value" />
<Binding Path="DataContext.Maximum" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</RowDefinition.Height>
</RowDefinition>
<RowDefinition Height="{Binding Value, Converter={local:LengthPercentageConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="1" Grid.Column="1" Background="Blue" BorderBrush="Black" BorderThickness="1,1,1,0" />
<Border Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Height="1" HorizontalAlignment="Stretch" Background="Black" />
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" FontSize="24" Text="{Binding Key}" TextAlignment="Center" />
</Grid>
</DataTemplate>