数据可视化控件设计

在进行软件开发时,经常需要将数据以图形的方式展示给用户,这就需要用到数据可视化技术。本文将介绍如何使用DataTemplate和ItemsControl创建一个简单的数据可视化控件。

设计思路

在设计数据可视化控件时,希望保持项目的简洁性,尽可能少地使用第三方库。因此,选择使用WPF(Windows Presentation Foundation)自带的控件和模板来实现。

核心组件:DataTemplate

控件的核心是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来展示数据,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的实现

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>
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485