为啥嵌套网格中的星号大小不起作用?

Posted

技术标签:

【中文标题】为啥嵌套网格中的星号大小不起作用?【英文标题】:Why does star sizing in a nested grid not work?为什么嵌套网格中的星号大小不起作用? 【发布时间】:2015-08-23 12:40:18 【问题描述】:

考虑以下 XAML:

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <Button Content="Button" HorizontalAlignment="Left"/>
  <Grid Grid.Column="1">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Button Content="B" Margin="5"/>
    <Button Content="Button" Grid.Column="1" Margin="5"/>
  </Grid>
</Grid>

在上面,除一个之外的所有ColumnDefinition 值都使用Width 的默认值,即"*",即“星号大小”。一个例外是包含嵌套Grid 控件的列,该控件设置为"Auto"

我希望它的工作方式如下:

外层Grid根据其内容的需要调整第二列的大小,然后将控件剩余的宽度分配给第一列。 内部Grid 将其可用空间均匀分配到两列。毕竟,它们都被设置为使用星号大小,并且星号大小应该将GridLength 属性(在本例中为宽度)设置为可用空间的加权分布。这个内部Grid 的最小布局大小(外部Grid 需要计算其第二列的宽度)是均匀分布宽度、星型列的总和(即在这种情况下,两倍于内容最宽的列的宽度)。

但是,嵌套网格的列宽是根据每个按钮的计算最小尺寸设置的,两个星形大小的列之间没有明显的加权关系(为清楚起见,显示了网格线):

如果我没有外部网格,它会按预期工作,即只需使内部网格成为窗口中唯一的网格:

两列被强制为相同大小,然后当然左侧按钮被拉伸以适应其包含单元格的大小(这就是我想要的......最终目标是让这两个按钮拥有相同的宽度,网格列提供了实现这一点的布局)。

在这个特定示例中,我可以使用UniformGrid 作为解决方法,以强制均匀分布列宽。这就是我希望它实际的样子(UniformGrid 没有 ShowGridLines 属性,所以你只需要想象两个最右边的按钮之间的假想线):

但我真的很想更全面地了解如何实现这一点,以便在更复杂的场景中我能够在嵌套的Grid 控件中使用星号大小。

似乎不知何故,包含在另一个Grid 控件的单元格中正在改变为内部Grid 控件计算星号大小的方式(或完全阻止星号大小产生任何影响)。但是为什么会这样呢?我是否(再次)错过了一些将其解释为“设计使然”的行为的 WPF 的深奥布局规则?或者这只是框架中的一个错误?

更新:

我理解 Ben 的回答是指星号大小应该只分配剩余空间每列的最小尺寸已被考虑在内。但这不是人们在其他情况下看到的。

例如,如果包含内部网格的列已明确调整大小,则对内部网格的列使用星号大小会导致列的大小均匀,正如我所期望的那样。

即这个 XAML:

<Grid ShowGridLines="True">
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <!--
    <ColumnDefinition Width="Auto"/>
    -->
    <ColumnDefinition Width="60"/>
  </Grid.ColumnDefinitions>
  <Button Content="Button" HorizontalAlignment="Left"/>
  <Grid Grid.Column="1" ShowGridLines="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Button Content="B" Margin="5"/>
    <Button Content="Button" Grid.Column="1" Margin="5"/>
  </Grid>
</Grid>

产生这个输出:

换句话说,在最坏的情况下,我希望 WPF 首先计算内部网格的最小尺寸而不考虑星号大小(例如,如果短按钮占用 10 个像素,长按钮占用 70 个像素,那么总宽度将是 80),然后 still 均匀分布列宽(即在 10/70 示例中,每列将以 40 像素结束,截断较长的按钮,类似于上图) .

为什么星号有时应在列之间均匀分布宽度,有时则不能?

更新 #2:

这是一个简单的示例,它清楚而显着地展示了 WPF 如何根据是计算 Grid 宽度的人还是您来区别对待星号大小:

<Window x:Class="TestGridLayout2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        SizeToContent="WidthAndHeight"
        Title="MainWindow">
  <Window.Resources>
    <Style TargetType="Border">
      <Setter Property="BorderBrush" Value="Black"/>
      <Setter Property="BorderThickness" Value="1"/>
    </Style>
    <Style TargetType="TextBlock">
      <Setter Property="FontSize" Value="24"/>
    </Style>
  </Window.Resources>
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="3*"/>
    </Grid.ColumnDefinitions>

    <Border Grid.Column="0"/>
    <Border Grid.Column="1"/>

    <StackPanel>
      <TextBlock Text="Some text -- one"/>
      <TextBlock Text="Some text -- two"/>
      <TextBlock Text="Some text -- three"/>
    </StackPanel>
    <StackPanel Grid.Column="1">
      <TextBlock Text="one"/>
      <TextBlock Text="two"/>
      <TextBlock Text="three"/>
    </StackPanel>
  </Grid>
</Window>

当你运行程序时,你会看到:

WPF 忽略了星号大小,将每个列的宽度设置为最小。如果您只是单击窗口边框,就像调整窗口大小一样(您甚至不必实际将边框拖动到任何地方),Grid 布局就会重做,您会得到:

此时,应用了星号大小(正如我所期望的那样),并且根据 XAML 声明对列进行了比例调整。

【问题讨论】:

非常有趣!如果为嵌套网格显式设置 会怎样?亲切的问候, “最小布局大小”不是所有列中最大最小布局大小的两倍。它是每个列的最小布局大小的总和。星型大小不会影响最小值,它会影响额外空间的分布。 @AlexBell:明确设置宽度不会改变结果。 :( @dubstylee:我认为当明确的Width="*" 不会改变行为时,这已被排除? @dubstylee:当你为两个内部列设置Width="*"时,它们不能从外部网格继承Width="Auto",对吧? 【参考方案1】:

我同意 Ben 的回答强烈暗示了这里可能发生的事情:

正如 Ben 指出的那样,WPF 忽略了星型大小,以计算内部 Grid 对象的最小宽度(也许是合理的……我认为有诚实辩论的空间,但显然这是一种可能且合法的设计)。

尚不清楚(Ben 没有回答)为什么这应该暗示在计算内部Grid 内的列宽时忽略星号大小。由于在外部施加宽度时,如果需要保持这些比例,成比例的宽度会导致内容被截断,为什么当根据内容的最小所需大小自动计算宽度时不会发生同样的事情。

即我仍在寻找我的问题的答案。

同时,恕我直言,有用的答案包括解决该问题的方法。虽然不是我的问题本身的实际答案,但它们显然对任何可能遇到该问题的人都有帮助。所以我写这个答案是为了整合所有已知的解决方法(无论好坏,WPF 的一个大“特性”是似乎总是至少有几种不同的方法可以实现相同的结果:))。

解决方法 #1:

使用UniformGrid 而不是Grid 作为内部网格对象。该对象不具有与Grid 相同的所有功能,当然也不允许任何列具有不同的宽度。因此,它可能并非在所有情况下都有用。但它确实很容易解决这里的简单问题:

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <Button Content="Button" HorizontalAlignment="Left"/>
  <UniformGrid Rows="1" Columns="2" Grid.Column="1">
    <Button Content="B" Margin="5"/>
    <Button Content="Button" Grid.Column="1" Margin="5"/>
  </UniformGrid>
</Grid>

解决方法 #2:

将较小内容对象的MinWidth 属性(例如,此处为网格中的第一个Button)绑定到较大内容对象的ActualWidth 属性。 这当然需要知道哪个对象的宽度最大。在本地化场景中,这可能会出现问题,因为除了文本资源之外,XAML 还必须进行本地化。但这有时是必要的,所以……:)

这看起来像这样(基本上是回答者dub stylee 提供为an answer here):

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <Button Content="Button" HorizontalAlignment="Left"/>
  <Grid Grid.Column="1">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Button Content="B" Margin="5"
            MinWidth="Binding ElementName=button2, Path=ActualWidth"/>
    <Button x:Name="button2" Content="Button" Grid.Column="1" Margin="5"/>
  </Grid>
</Grid>

一种变体(为简洁起见,我不会在这里包括)是使用MultiBinding,它将所有相关控件作为输入并返回集合中最大的MinWidth。当然,那么这个绑定将用于每个ColumnDefinitionWidth,这样所有的列都显式设置为最大的MinWidth

绑定方案还有其他变体,具体取决于您要使用和/或设置的宽度。这些都不是理想的,不仅因为潜在的本地化问题,还因为它将更明确的关系嵌入到 XAML 中。但在很多场景下,它都能完美运行。

解决方法 #3:

通过使用ColumnDefinition 值的SharedSizeGroup 属性,可以显式强制一组列具有相同的宽度。在这种方法中,内部Grid 对象的最小宽度然后在此基础上计算,当然宽度也一样。

例如:

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <Button Content="Button" HorizontalAlignment="Left"/>
  <Grid Grid.Column="1" IsSharedSizeScope="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition SharedSizeGroup="buttonWidthGroup"/>
      <ColumnDefinition SharedSizeGroup="buttonWidthGroup"/>
    </Grid.ColumnDefinitions>
    <Button Content="B" Margin="5"/>
    <Button Content="Button" Grid.Column="1" Margin="5"/>
  </Grid>
</Grid>

这种方法允许使用Grid,从而获得该对象的所有正常功能,同时解决关注的特定行为。我希望它不会干扰SharedSizeGroup 的任何其他合法使用。

但是,它确实暗示了"Auto" 的大小调整,并排除了"*"(星号)大小的影响。在这种特殊情况下,这不是问题,但与UniformGrid 解决方法一样,当尝试将其与其他尺寸组合时,它确实限制了一个人的选择。例如。让第三列使用"Auto" 并希望SharedSizeGroup 列占用Grid 的剩余空间。

不过,这在许多情况下都可以正常工作。

解决方法 #4:

我最终重新审视了这个问题,因为我遇到了主题的变体。在这种情况下,我正在处理一种情况,我希望对正在调整大小的列具有不同的比例。上述所有解决方法都假定列的大小相同。但我希望(例如)一列拥有 25% 的空间,另一列拥有 75% 的空间。和以前一样,我希望 Grid 的总大小能够容纳 所有 列所需的最小宽度。

此解决方法只是让我自己明确地执行我认为 WPF 应该执行的计算。 IE。取每列内容的最小宽度以及指定的比例大小,计算Grid 控件的实际宽度。

这是一个可以做到这一点的方法:

private static double ComputeGridSizeForStarWidths(Grid grid)

    double maxTargetWidth = double.MinValue, otherWidth = 0;
    double starTotal = grid.ColumnDefinitions
        .Where(d => d.Width.IsStar).Sum(d => d.Width.Value);

    foreach (ColumnDefinition definition in grid.ColumnDefinitions)
    
        if (!definition.Width.IsStar)
        
            otherWidth += definition.ActualWidth;
            continue;
        

        double targetWidth = definition.ActualWidth / (definition.Width.Value / starTotal);

        if (maxTargetWidth < targetWidth)
        
            maxTargetWidth = targetWidth;
        
    

    return otherWidth + maxTargetWidth;

此代码在列的最小宽度和比例大小中找到仍然可以容纳每个星形列的最小宽度,以及未使用星形大小的其余列。

您可以在适当的时间(例如在Grid.Loaded 事件处理程序中)调用此方法,然后将其返回值分配给Grid.Width 属性以将宽度强制为正确的大小以适应所需的最小宽度所有列,同时保持指定的比例。

类似的方法会对行高做同样的事情。

解决方法 #5:

我想需要指出:可以简单地明确指定Grid 大小。这仅适用于预先知道大小的内容,但同样,在许多情况下这会很好。 (在其他情况下,它会截断内容,因为即使指定的尺寸太小,当它显式时,WPF 会继续并应用星大小比例)。

我鼓励其他人在这个答案中添加额外的解决方法,如果他们知道与已经显示的那些有很大不同的好的解决方法。或者,您可以随意发布另一个答案与您的解决方法,我会(在我方便的时候尽早:))自己在此处添加。

【讨论】:

【参考方案2】:

这是一种解决方法,并没有解释您所描述行为的原因,但它实现了您所寻找的:

<Grid ShowGridLines="True">
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Button HorizontalAlignment="Left" Content="Button" />
    <Grid Grid.Column="1"
          ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Button Grid.Column="0"
                Margin="5"
                MinWidth="Binding ElementName=Button2, Path=ActualWidth"
                Content="B"
                x:Name="Button1" />
        <Button Grid.Column="1"
                Margin="5"
                Content="Button"
                x:Name="Button2" />
    </Grid>
</Grid>

我必须假设Auto 列宽是根据子控件的最小宽度计算的。我无法通过任何文档确认或反驳这一点,但这可能是预期的行为。为了强制Button 控件占据相同的大小,您只需将MinWidth 属性绑定到另一个ActualWidthActualWidth。在此示例中,我仅将 Button1.MinWidth 绑定到 Button2.ActualWidth 以说明您想要的行为。

另外,请忽略Button.Height,我没有费心将它们设置为不同于默认值。

【讨论】:

谢谢。虽然我知道这种解决方法(也有一个使用SharedSizeGroup),但它确实产生了我正在寻找的效果,因此绝对是一个有用的答案。 (不用担心高度...这是我的错,因为我没有包含我使用的默认 Button 样式,该样式设置了 VerticalAlignment="Center", to override the default value of "Stretch"`)。【参考方案3】:

来自文档(Modern apps)(WPF):

starSizing 一种约定,您可以根据该约定调整行或列的大小以占用 Grid 中剩余的可用空间。

它不会导致请求更多的最小空间,它会影响超过最小值的空间分配。

【讨论】:

除了如果我显式调整外列宽度,星号大小会均匀分布列的宽度,即使这意味着截断其中一个元素。我会更新我的问题以更清楚地说明我的意思。 @PeterDuniho:您提供的宽度小于最小值。如果在您的第​​四个布局中,您给第二个外部列的宽度仅比内部网格内容最小值稍大(即仅比分配给第二个列的布局 1 稍大)会发生什么? 请在我的问题更新中查看我的 cmets。是的,我知道我提供的宽度小于最小值。这是有意的,以模拟如果 WPF 在计算内部网格对象的最小宽度(即,仅添加每列内容的最小值)时根本不考虑星型大小时会发生什么应该。如果 WPF 这样做,我会发现这是合乎逻辑的;它不会做我想要的,但至少这种行为是有意义的。实际上,行为是在 outer 网格的列宽为 "Auto" 的情况下完全忽略了星号大小。 直接回答你的问题——“如果...你给第二个外列的宽度比内部网格内容的最小值稍微大一点会发生什么”——如只要外部网格的列宽是明确的,星号大小就可以按预期工作:内部网格的两列总是彼此具有相同的宽度。如果我将第二个外部列的宽度显式设置为刚好大于 WPF 在第一个示例中给我的宽度,则两个内部列仍然具有相同的宽度,并且第二个内部按钮文本被截断(因为第一个内部按钮吃光了它需要一些空间)。

以上是关于为啥嵌套网格中的星号大小不起作用?的主要内容,如果未能解决你的问题,请参考以下文章

extjs 嵌套数据网格过滤器和重新加载在 viewModel 上不起作用

为啥嵌套循环在 laravel 中不起作用

Dojo EnhancedGrid 嵌套排序不起作用

为啥嵌套的 z-index 不起作用[重复]

为啥这个嵌套的 SVG 在 Firefox 中不起作用?

ES6/React:为啥我的三重嵌套 setState 更新不起作用?