我正在编写一个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 . 以下屏幕截图显示了此值:

http://hpics.li/63af790

(左侧值是转换器的调用次数,右侧是作为参数传递的实际宽度)

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的宽度不再变化了造成这个错误 . 所以问题解决了,但我仍然不明白为什么 .