2

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

http://hpics.li/63af790

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

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>

然后错误消失了,文本块的宽度不再改变,这导致了这个错误。所以问题解决了,但我仍然不明白为什么。

4

0 回答 0