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

5
我正在制作一个进度向导。我将其定义为基于ItemsControl的样式。我有一个ItemTemplateSelector,其中包含两个DataTemplates,一个用于第一项,另一个用于剩余项。除了一个非常难以修复的小问题之外,它可以正常工作。第一项和第二项之间有一个间隙。enter image description here这是控件应该看起来的样子:enter image description here这个间隙出现是因为我使用了一个均匀的网格,所以所有的列都被同样地调整大小,即使第一列没有线。但是使用均匀的网格非常重要,因为我希望所有的东西都在一行上,并且随着控件的增长而拉伸以填充可用空间。我已经尝试过不使用均匀网格,但最终要么出现边距问题,要么无法填充可用空间。我该如何修复这个间隙?
以下是代码:
<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;
        }
    }

UniformGrid在这里不是一个合适的ItemsPanel,因为它的行为不是你想要的(它没有“除第一个之外”的概念)。但是你可以编写自己的Panel类。请注意,你不需要DataTemplateSelector。相反,添加一个ItemContainerStyle(针对ContentPresenter),将ContentTemplate属性设置为OtherItem,并在ItemsControl.AlternationIndex上设置一个触发器,当Trigger Value为零时将ContentTemplate设置为FirstItem。还要在ItemsControl上设置AlternationCount为一个高值(如2147483647)。 - Clemens
3个回答

1
主要问题是,您设置中的第一项实际上必须与其他项具有不同的宽度。这在 UniformGrid 中是不可能的。
我可以建议以下解决方案。
目标配置将如下所示:
|··O––|––O––|––O––|––O··|

您的控件左右各有半个单元宽的边距(如上方的点所示)。如果需要,您可以通过指定控件的边距来利用它们。
此外,我们可以简化数据模板。实际上,我们不需要为第一个和其他项使用单独的模板。请参见下面的内容。
设置
这里我们将使用三个技巧:
- ItemsControl.AlternationIndex属性获取ItemsControl中的第一个元素 - 一个Canvas,允许在相应的UniformGrid单元格之外绘制连接线 - 一个特殊的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
}

解释

每个椭圆代表向导页面,并位于相应UniformGrid单元格的中心。线条放置在椭圆左侧。线条的宽度设置为UnifiormGrid单个单元格的宽度,并根据公式在Canvas中设置其水平位置:WidthOfEllipse / 2 - WidthOfCell / 2。这样可以确保正确的定位。

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

请注意,您可能希望使用Fill来隐藏椭圆下面的线条。


这个解决方案非常有帮助。我正在考虑做类似的事情。不过,您知道有没有什么方法可以让我去掉或隐藏左侧和右侧的那个半单元宽的边距? - user2481095
@user2481095,正如我在答案中提到的那样,您可以使用“Margin”属性来利用那些不需要的空白空间。将您的“ItemControl”的“Margin”设置为负偏移量左右,例如Margin="-50,0,-50,0"。如果您愿意,甚至可以直接在“Style”中设置此属性并动态计算此边距。 - dymanoid

0
一个快速的解决方案是从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>

那不是我需要创建的正确控件形式。我编辑了描述,以使我的要求更清晰一些。 - user2481095

-1
引用史蒂夫·乔布斯的话:“设计不仅关乎外观和手感,更关乎功能。”。
我们可以通过将前一个节点、中间路径和下一个节点放置在一个网格单元中来可视化进程中的一步。以下是五个单元格和用户必须完成的五个相应阶段。
 ___________________________________________________________
|           |           |           |           |           |
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

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

这是一个提供良好答案的不错尝试,只是针对错误的问题。提问者的问题主要与正确定位有关,而不是如何绘制线条来更改状态。一个更具体的实现可能会得到更高的评分。 - Ryan Leach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接