统一网格作为 ItemsControl 中所有项目的面板模板,第一个除外

Uniform Grid as Panel Template for All Items in ItemsControl Except the First

我正在制作进度向导。我将其定义为基于 ItemsControl 的样式。我有一个带有两个数据模板的 ItemTemplateSelector,一个用于第一个项目,一个用于其余项目。除了一个非常难以修复的非常小的问题外,我让它正常工作。第一项和第二项之间有一个间隙。 控件应如下所示: 出现间隙是因为我使用的是统一的网格,所以所有列的大小都相同,即使第一列没有线。使用统一的网格很重要,因为我希望所有内容都在一行中,并且我希望控件随着它的增长而伸展以填充可用的 space。我试过不使用统一网格,但我最终要么遇到边距问题,要么没有填充可用 space。我该如何解决这个差距?

代码如下:

<Style x:Key="WizardProgressBar" TargetType="{x:Type ItemsControl}">
        <Style.Resources>
            <DataTemplate x:Key="FirstItem">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Ellipse Name="ellipse"  HorizontalAlignment="Left" Height="32" Width="32"  />
                </Grid>
                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding Completed}" Value="False">
                        <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding InProgress}" Value="True">
                        <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>


            <DataTemplate x:Key="OtherItem">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Ellipse Name="ellipse"  Grid.Column="1" HorizontalAlignment="Left" Height="32" Width="32" />
                    <Line Name="leftPath" Grid.Column="0" X1="0" Y1="16" 
                              X2="{Binding ActualWidth, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}" Y2="16" />
                </Grid>
                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding Completed}" Value="False">
                        <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
                        <Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource DisabledBrush}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding InProgress}" Value="True">
                        <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
                        <Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </Style.Resources>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <UniformGrid Rows="1"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemTemplateSelector">
            <Setter.Value>
                <wpf:ItemsDataTemplateSelector  FirstItem="{StaticResource FirstItem}" OtherItem="{StaticResource OtherItem}" />
            </Setter.Value>
        </Setter>
    </Style>


public class ItemsDataTemplateSelector : DataTemplateSelector
    {
        public DataTemplate FirstItem { get; set; }
        public DataTemplate OtherItem { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
            var returnTemplate = (itemsControl.ItemContainerGenerator.IndexFromContainer(container) == 0) ? FirstItem : OtherItem;
            return returnTemplate;
        }
    }

一个快速的解决方案是从 ItemsControl 中删除第一个。

    <Grid VerticalAlignment="Top">
    <Grid.Resources>
        <Style x:Key="WizardProgressBar" TargetType="{x:Type ItemsControl}">
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Ellipse Name="ellipse"  Grid.Column="1" HorizontalAlignment="Left" Height="32" Width="32" />
                            <Line Name="leftPath" Grid.Column="0" X1="0" Y1="16" 
                          X2="{Binding ActualWidth, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}" Y2="16" />
                        </Grid>
                        <DataTemplate.Triggers>
                            <DataTrigger Binding="{Binding Completed}" Value="False">
                                <Setter TargetName="ellipse" Property="Stroke" Value="Gray" />
                                <Setter TargetName="leftPath" Property="Stroke" Value="Gray"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding InProgress}" Value="True">
                                <Setter TargetName="ellipse" Property="Stroke" Value="Black"/>
                                <Setter TargetName="leftPath" Property="Stroke" Value="Black"/>
                            </DataTrigger>
                        </DataTemplate.Triggers>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <UniformGrid Rows="1"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Ellipse Name="ellipse"  HorizontalAlignment="Left" Height="32" Width="32" Stroke="Black"/>
    <ItemsControl Grid.Column="1" Style="{StaticResource WizardProgressBar}"
                  x:Name="otherItemsGrid">
    </ItemsControl>
</Grid>

主要问题是您设置中的第一项实际上必须与其他项相比具有不同的宽度。 UniformGrid.

这是不可能的

我可以向您推荐以下解决方案。

目标配置如下所示:

|··O––|––O––|––O––|––O··|

您将在左侧和右侧(由上面的点表示)有半个单元宽的边距。如果需要,您可以通过指定控件的边距来利用它们。

此外,我们可以简化您的数据模板。实际上,我们不需要为第一个项目和其他项目使用单独的模板。见下文。

设置

我们将在这里使用三个技巧:

  • ItemsControl.AlternationIndex属性获取第一个元素ItemsControl
  • a Canvas 允许在相应 UniforGrid 的单元格外绘制连接线
  • 一个特殊的IValueConverter将帮助我们计算所需的行位置

现在,这是您 ItemsControl 的样式:

<Style x:Key="WizardProgressBar" TargetType="{x:Type ItemsControl}">
  <Style.Resources>
    <local:LinearConverter x:Key="Multiplier" Scale="-0.5" Offset="16"/>

    <DataTemplate DataType="{x:Type local:YourItemType}">
      <Grid>            
        <Canvas>
          <Rectangle x:Name="leftPath" Height="2" Stroke="Blue" Canvas.Top="16"
                     Canvas.Left="{Binding Width, RelativeSource={RelativeSource Self}, Converter={StaticResource Multiplier}}"
                     Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentPresenter}}}"/>
        </Canvas>
        <Ellipse Name="ellipse" HorizontalAlignment="Center" Height="32" Width="32" Stroke="Blue"/>
      </Grid>
      <DataTemplate.Triggers>
        <Trigger Property="ItemsControl.AlternationIndex" Value="0">
          <Setter TargetName="leftPath" Property="Visibility" Value="Collapsed"/>
        </Trigger>
        <DataTrigger Binding="{Binding Completed}" Value="False">
          <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
          <Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding InProgress}" Value="True">
          <Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
          <Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}" />
        </DataTrigger>
      </DataTemplate.Triggers>
    </DataTemplate>
  </Style.Resources>
  <Setter Property="ItemsPanel">
    <Setter.Value>
      <ItemsPanelTemplate>
        <UniformGrid Rows="1"/>
      </ItemsPanelTemplate>
    </Setter.Value>
  </Setter>
  <Setter Property="AlternationCount" Value="100"/>
</Style>

注意以下变化:

  • 只有一个DataTemplate,不需要两个不同的
  • 不再需要模板选择器
  • 我们需要一个符合我们风格的特殊转换器(代码如下)
  • Ellipse 和线(作为 Rectangle)被放置在同一个单元格中的 1x1 Grid
  • 该行本身位于 Canvas
  • Ellipse水平居中
  • 还有一个附加的 Trigger 捕获 AlternationIndex 值 0 并隐藏该行 - 它用于第一个元素
  • 样式将 AlternationCount 设置为 100,假设您最多有 100 个向导页面

这是转换器:

class LinearConverter : IValueConverter
{
    public double Scale { get; set; }

    public double Offset { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // TODO: exception handling
        return System.Convert.ToDouble(value) * Scale + Offset;
    }

    // ConvertBack just throws a NotImplementedException
}

说明

每个 Ellipse 代表一个向导页面并放置在相应 UniformGrid 的中心 's cell. The lines are placed to the left of the ellipses. The lines' 宽度设置为 UnifiormGrid 的单个单元格的宽度,它们在Canvas中的水平位置根据公式设置:WidthOfEllipse / 2 - WidthOfCell / 2。这确保了正确的放置。

对于第一个向导页面,该行将被隐藏。

请注意,您可能希望对省略号使用 Fill 来隐藏下面的线条。

引用史蒂夫乔布斯的话:"It’s not just what it looks like and feels like. Design is how it works."

我们可以通过将前一个节点、中间路径和下一个节点放在一个网格单元上来可视化进度中的一个步骤。下面五个单元格,以及用户必须完成的五个相应阶段。

 ___________________________________________________________
|           |           |           |           |           |
O - - - - - O - - - - - O - - - - - O - - - - - O - - - - - O
|___________|___________|___________|___________|___________|

一旦中间路径在进行中,前一个节点就已经完成(完全绘制)。并且完成后,路径和下一个节点也可以完全绘制出来。

这种方法的美妙之处在于它的简单性以及没有特殊情况或例外的事实。每个阶段的处理方式完全相同,无论是在视图中还是在其背后的模型中。

第一个节点是我们开始的地方(前进到第二个)。想一想,你不想在锡空气中开始,你从链中的第一个节点开始,移动到下一个。如果有五个级数,就会有六个节点。

就您所引用的笔刷而言:

                    Default                 In progress             Completed

Left node           DisabledBrush           PrimaryTextBrush        PrimaryTextBrush

Intermediate path   DisabledBrush           DisabledBrush           PrimaryTextBrush

Right node          DisabledBrush           DisabledBrush           PrimaryTextBrush

唯一的绘制后面是您将绘制每个节点两次,除了第一个和最后一个。