我正在编写一个WPF组件,它显示一个看起来像帕累托图的图表 . 它运作正常,但我觉得它几乎是废话,这就是为什么:
-
它使用SO MANY容器,对于一个简单的图表,可能有大约50个容器构造它;
-
我使用边距(存储在ViewModel中)将矩形放在好位置,我觉得这很难看,但我还没有想到更好的方法;
-
我需要知道ViewModel中图形组件的大小,以便将组件放在正确的位置并根据它进行缩放;
-
我使用两个图层来渲染图表,一个用于图表,另一个用于显示图表的比例,我认为这根本不好
它看起来如何
http://hpics.li/fd2b0bd(可以't display the image because I'新)
ViewModel
顶部对象是 ParetoChartVM ,包含一个SerieVM的ObservableCollection和另一个AxisVM,一个Title和图表的当前大小 .
SerieVM 由ValuePointVM的OservableCollection组成(表示图表中的矩形) .
ValuePointVM 包含画笔,数值,宽度和高度以及边距(厚度对象) .
AxisVM 包含MinimumValue,MaximumValue,NumberOfScales和ScaleVM的ObservableCollection .
ScaleVM 包含一个Value,一个ValuePercentage(在顶部我显示值,在底部显示最大值的百分比),TopMargin和BottomMargin(两个Thickness对象) .
查看
View层仅包含ParetoChartV WPF组件 . 该组件仅包含一个ParetoChartVM,他的DataContext设置为此ParetoChartVM .
它是如何工作的
每次调整图表容器的大小时,我都会通知它的ParetoChartVM,它会重新计算每个位置/宽度/高度,使用这些属性上的Bindings更新界面 .
现在这里是XAML(它非常大):
<UserControl x:Class="ParetoChart.View.ParetoChartV"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:ParetoChart.ViewModel"
xmlns:converter="clr-namespace:ParetoChart.ViewModel.Converter"
DataContext="{Binding RelativeSource={RelativeSource self}, Path=ParetoChart}">
<Grid>
<Grid.Resources>
<Style x:Key="TitleTextStyle" TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="20"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Grid.Resources>
<!--
Fours parts: Title, Scales, Chart, Caption
Scales & Chart have the same location, the Scales layer is an overlay on the chart layer
-->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*"/>
<ColumnDefinition Width=".5*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<!--Title-->
<TextBlock Style="{StaticResource TitleTextStyle}" Text="{Binding Title, Mode=OneWay}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="0" Margin="0,20,0,0"> <!--Chart layer-->
<Grid.Background>
<ImageBrush ImageSource="../Resources/Images/GlassBlock.png"/>
</Grid.Background>
<Grid SizeChanged="FrameworkElement_OnSizeChanged" Margin="10, 0">
<ItemsControl ItemsSource="{Binding Series, Mode=OneWay}"> <!-- Container for SerieVM -> for each "line" on the chart -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SerieVM}">
<ItemsControl ItemsSource="{Binding Values, Mode=OneWay}"> <!--Container for ValuePoints -> each rectangle -->
<ItemsControl.Resources>
<converter:SolidColorToGradientColor x:Key="SolidColorToGradientColor"/>
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:IValuePoint}">
<Rectangle Width="{Binding RectangleWidth, Mode=OneWay}"
Height="{Binding RectangleHeight, Mode=OneWay}"
Fill="{Binding BrushColor, Converter={StaticResource SolidColorToGradientColor}, Mode=OneWay}"
Margin="{Binding Margins, Mode=OneWay}">
<Rectangle.Effect>
<DropShadowEffect Color="Gray"/>
</Rectangle.Effect>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Grid>
<Grid Grid.Row="1" Grid.Column="0" Margin="0,0,0,0"> <!--Scales layer-->
<ItemsControl ItemsSource="{Binding Axes, Mode=OneWay}"> <!-- Container containing axes -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:AxisVM}">
<ItemsControl ItemsSource="{Binding Scales, Mode=OneWay}"> <!-- Container containing scales -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:ScaleVM}">
<Canvas>
<Canvas.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="#0C077D"/>
</Style>
</Canvas.Resources>
<TextBlock x:Name="MyTB2" Text="{Binding Value, StringFormat={}{0:N0}}"
Margin="{Binding TopCaptionMargins, Mode=OneWay}"/> <!--Scale point value-->
<Line X2="{Binding TopCaptionMargins.Left, Mode=OneWay}"
Y1="20"
Y2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}, Path=ActualHeight, Mode=OneWay}"
X1="{Binding TopCaptionMargins.Left, Mode=OneWay}"
StrokeDashArray="1 2" Stroke="Gray"/> <!-- vertical dashed line at the same X location of the scale -->
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
Margin="{Binding BottomCaptionMargins, Mode=OneWay}"/><!--Scale point percentage of maximum-->
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
<!-- This part is probably ok -->
<Grid Grid.Row="1" Grid.Column="1" Margin="10,20,0,0"> <!--Caption-->
<Grid.Background>
<ImageBrush ImageSource="../Resources/Images/GlassBlock.png"/>
</Grid.Background>
<ItemsControl ItemsSource="{Binding Series, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SerieVM}">
<ItemsControl ItemsSource="{Binding Values, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:IValuePoint}">
<Grid Margin="20, 20, 10, 20">
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="#0C077D"/>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width=".8*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Width="30" Height="30" Fill="{Binding BrushColor, Mode=OneWay}"/>
<TextBlock Margin="20,0,0,0" Grid.Column="1" Text="{Binding ValueOfXAxis, StringFormat={}{0:N0}, Mode=OneWay}"
HorizontalAlignment="Stretch" TextAlignment="Right"/>
<TextBlock Margin="20,0,0,0" Grid.Column="2" Text="{Binding ValueDescription, Mode=OneWay}" TextWrapping="WrapWithOverflow"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Grid>
</UserControl>
因此,对于每个ObservableCollection,我创建一个ItemsControl来存储和显示它们,我还需要在ItemsControl.ItemsPanel中放置一个Canvas,用Margins将每个组件放到我想要的位置 . 这些项目也在ObservableCollection中,所以我需要将它们放在ItemsControl中,Canvas作为ItemsPanel .
您认为我的代码结构存在问题吗?请告诉我你是否看到了一些,尽可能地解释它们因为我正在开始使用WPF和MVVM模式 .
(我使用的是dotnet框架版本3.5,所以我无法使用Interactivity来处理容器的SizeChanged事件)
感谢您的帮助和时间
编辑(相关问题)
我遇到的一个问题是我做了一个转换器,它将Textblocks放在一个特定的点上(使用图表中的垂直虚线显示比例值的文本中心) .
我是这样做的:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace ParetoChart.ViewModel.Converter {
public class CenterTextblockTextConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue) {
return DependencyProperty.UnsetValue;
}
if (values[0] is Thickness) {
Thickness margins = (Thickness) values[0];
double width = (double) values[1];
margins.Left = margins.Left - (width / 2.0);
return margins;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}
在XAML中,我更改了Textblocks,如下所示:
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
x:Name="MyTB">
<TextBlock.Margin>
<MultiBinding Converter="{StaticResource CenterTextblockTextConverter}">
<Binding Path="BottomCaptionMargins"/>
<Binding ElementName="MyTB" Path="ActualWidth"/>
</MultiBinding>
</TextBlock.Margin>
</TextBlock>
所以我命名它们并将多个值传递给转换器 .
所以它的工作除了强调时,这个转换器使组件变得疯狂,然后转换器每秒被调用大约10000次,看似无限循环 . 该图表将不再调整大小(但窗口中的其他组件仍然响应调整大小) . 请注意,我提供的屏幕截图是我正在使用此转换器的屏幕截图,因为此问题我停止使用它 .
你知道为什么会这样吗?
编辑n°2(关于旁边问题)
我做了一些测试,转换器问题似乎与转换器的ActualWidth参数一起发生 . Textblock似乎有浮点问题 . 实际上,我没有改变的宽度突然从~8.08变为~28.449 . 以下屏幕截图显示了此值:
(左侧值是转换器的调用次数,右侧是作为参数传递的实际宽度)
ActualWidth值在28.44999999 ...和28.45之间变化,每次触发转换器并使图表变得疯狂 .
知道怎么解决吗? (我试图理解为什么宽度会突然跳起,因为我没有碰到它(我改变了Textblock左边和上边距,从不改变它的宽度))
编辑n°3(关于问题)
我检查边距是否可以改变Textblock的宽度,但只有Left和Top边距改变,Bottom和Right不改变 . 我在xaml中更改了从Margin到Canvas.Left和Canvas.Top的绑定,如下所示:
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
x:Name="MyTB" MaxWidth="40">
<Canvas.Left>
<MultiBinding Converter="{StaticResource CenterTextblockTextConverter}" Mode="OneWay">
<Binding Path="BottomCaptionMargins.Left"/>
<Binding ElementName="MyTB" Path="ActualWidth" Mode="OneWay"/>
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<Binding Path="BottomCaptionMargins.Top"/>
</Canvas.Top>
</TextBlock>
然后bug就消失了,Textblock的宽度不再变化了造成这个错误 . 所以问题解决了,但我仍然不明白为什么 .