我正在编写一个 WPF 组件,它显示一个看起来像帕累托图的图表。它工作正常,但我觉得这很垃圾,原因如下:
它使用了这么多容器,对于一个简单的图表,可能有大约 50 个容器构成它;
我使用边距(存储在 ViewModel 中)将矩形放在好的位置,我觉得这真的很难看,但我还没有想到更好的方法;
我需要知道 ViewModel 中图形组件的大小才能将组件放置在正确的位置并根据它进行缩放;
我正在使用两层来渲染图表,一层用于图表,另一层用于显示图表的比例,我认为这根本不好
它看起来如何
http://hpics.li/fd2b0bd (无法显示图像,因为我是新人)
视图模型
顶部对象是ParetoChartVM,包含 SerieVM 的 ObservableCollection 和 AxisVM 的另一个,标题和图表的当前大小。
SerieVM由 ValuePointVM 的 OservableCollection 组成(在图表中表示一个矩形)。
ValuePointVM包含一个画笔、一个数值、一个宽度和高度以及边距(厚度对象)。
AxisVM包含 ScaleVM的MinimumValue、MaximumValue、NumberOfScales 和 ObservableCollection。
ScaleVM包含一个 Value、一个ValuePercentage(在顶部显示值,在底部显示最大值的百分比)、一个 TopMargin 和一个 BottomMargin(两个厚度对象)。
看法
View 层仅包含一个 ParetoChartV WPF 组件。这个组件只包含一个ParetoChartVM,他的DataContext设置为这个ParetoChartVM。
这个怎么运作
每次调整图表容器的大小时,我都会通知 ParetoChartVM,它会重新计算每个位置/宽度/高度,并使用这些属性上的绑定来更新界面。
现在这里是 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 以将每个组件放置在我想要的边距位置。这些项目也在 ObservableCollection 中,因此我需要将它们也放在 ItemsControl 中,并将 Canvas 作为 ItemsPanel。
你觉得我的代码结构有问题吗?如果您确实看到了一些,请告诉我,尽可能多地解释它们,因为我开始使用 WPF 和 MVVM 模式。
(我使用的是 dotnet 框架 3.5 版,所以我不能对容器的 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>
所以我命名它们并将多个值传递给转换器。
所以它可以工作,只是当受到压力时,这个转换器会使组件变得疯狂,然后转换器每秒被调用大约 10 000 次,似乎处于无限循环中。图表将不再调整大小(但窗口中的其他组件仍在响应调整大小)。请注意,我提供的屏幕截图是我使用这个转换器的地方,因为这个问题我停止使用它。
你知道为什么会这样吗?
编辑n°2(关于侧面问题)
我做了一些测试,转换器问题似乎与转换器的 ActualWidth 参数有关。Textblock 似乎有浮点问题。事实上,我没有改变的宽度突然从〜8.08变为〜28.449。以下屏幕截图显示了此值:
(左边的值是调用转换器的次数,右边的是作为参数传递的实际宽度)
ActualWidth 值在 28.44999999... 和 28.45 之间变化,每次都会触发转换器,让图表变得疯狂。
知道如何解决吗?(我试图理解为什么宽度突然跳跃,因为我从来没有碰过它(我改变了 Textblock 的左边距和上边距,而不是它的宽度))
编辑n°3(关于侧面问题)
我检查了边距是否可以改变文本块的宽度,但只有左边距和上边距会改变,下边距和右边不会改变。我在 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>
然后错误消失了,文本块的宽度不再改变,这导致了这个错误。所以问题解决了,但我仍然不明白为什么。