XAML WPF DataGrid:在滚动时减小列宽度以适应其内容,除了一个

Posted

技术标签:

【中文标题】XAML WPF DataGrid:在滚动时减小列宽度以适应其内容,除了一个【英文标题】:XAML WPF DataGrid: reduce columns width to fit its content while scrolling except one 【发布时间】:2022-01-03 12:25:04 【问题描述】:

Here 是在滚动期间自动减小 DataGrid 列宽度的解决方案。 我需要一个稍微修改的版本,其中最后一列填充其他列之后留下的所有行宽。

旧的解决方案:

private void OnLoadingRow(object sender, DataGridRowEventArgs e)

    if (sender is not DataGrid dg) return;
    foreach (var c in dg.Columns) c.Width = 0;
    e.Row.UpdateLayout();
    foreach (var c in dg.Columns) c.Width = DataGridLength.Auto;

如果dg.Columns.Last().Width = new DataGridLength(1, DataGridLengthUnitType.Star); 在方法最后一列的末尾添加不尊重其他列Auto 的大小并强制其大小为 20px(参见图片)。

更新:

我需要描述行为的原因是当鼠标单击行的任何位置时选择行。当我单击该行的右侧时,该行没有选择 - 没有单元格。我通过添加MouseLeftButtonDown 描述的here 事件处理程序实现了这一点。

【问题讨论】:

【参考方案1】:

似乎DataGrid 无法一次性处理所有更改,但您可以“错开”更新并通过将最后一部分放在事件循环上来执行“中间”布局,例如,像这个:

private void OnLoadingRow(object sender, DataGridRowEventArgs e)

    if (sender is not DataGrid dg) return;
    foreach (var c in dg.Columns) c.Width = 0;
    foreach (var c in dg.Columns.SkipLast(1)) c.Width = DataGridLength.Auto;            
    Dispatcher.BeginInvoke((DataGridColumn lc) => lc.Width = new DataGridLength(1, DataGridLengthUnitType.Star), dg.Columns.Last());

这不是一个很好的解决方案,并且由于列宽的可见变化,网格会有点闪烁。也许可以改进它,例如,通过检测何时真正需要“重置”宽度。你决定它是否足够好。

编辑 - 通过在中间布局期间将最后一列设置为相同宽度,这是一个稍微不那么闪烁的版本:

private void OnLoadingRow(object sender, DataGridRowEventArgs e)

    if (sender is not DataGrid dg) return;
    foreach (var c in dg.Columns.SkipLast(1)) c.Width = 0;
    var lastColumn = dg.Columns.Last();
    lastColumn.Width = lastColumn.ActualWidth;
    foreach (var c in dg.Columns.SkipLast(1)) c.Width = DataGridLength.Auto;            
    Dispatcher.BeginInvoke((DataGridColumn lc) => 
        lc.Width = new DataGridLength(1, DataGridLengthUnitType.Star), lastColumn);

EDIT 2 - 重构使其更清晰:

private void OnLoadingRow(object sender, DataGridRowEventArgs e)

    if (sender is not DataGrid dg) return;
    foreach (var c in dg.Columns.SkipLast(1))
    
        c.Width = 0;
        c.Width = DataGridLength.Auto;
    
    var lastColumn = dg.Columns.Last();
    lastColumn.Width = lastColumn.ActualWidth;            
    Dispatcher.BeginInvoke(static (DataGridColumn lc) => 
        lc.Width = new DataGridLength(1, DataGridLengthUnitType.Star), lastColumn);

【讨论】:

【参考方案2】:

UpdateLayout 调用完全是多余的。修改Width 将自动触发布局通道。此外,为了提高性能,应将两个迭代合并为一个迭代。 完整的实现在用户体验方面非常值得怀疑:如果用户调整列的宽度,您将覆盖并重置他的自定义,这会提供糟糕的体验。此外,网格可能会不断调整自身大小,这看起来和感觉也很奇怪。您至少应该将DataGrid.CanUserResizeColumns 设置为false。 我相信添加一个允许用户隐藏特定列以压缩视图的列过滤器具有更高的价值。

从您发布的代码中,无法准确判断发生了什么或可能导致问题的原因。 DataGrid 控件的实际布局配置及其上下文显然未公开。

但是,问题可能与DataGrid 如何计算每列的宽度有关,以防您为DataGrid 提供一个固定的Width,并且此值会导致内容超出可用视口宽度(在这种情况下会出现ScrollViewer):

a) 如果每列宽度设置为 AutoDataGrid 将简单地为每列提供所需的最小宽度。

b) 如果列的宽度设置为星号* 大小,则分配的宽度是相对于可用剩余空间计算的。 为了计算一个合理的值,算法会使用DataGrid的有效大小。 关键是,当使用ScrollViewer 时,虚拟宽度将是无限的。现在,如果算法将最后一列的宽度设置为无限,你会得到一个非常小的滚动条。因此,infinite 不是一个合理的值。实际上,无法计算最终宽度。这就是为什么算法回退到使用DataGrid 的有效宽度,即ScrollViewer.ViewportWidth。结果是所有列都必须压缩到可用的视口空间中,导致列剪裁其标题和单元格内容。

我可以提供两种解决方案。

第一个解决方案是为最后一列使用固定的Width,例如1000 DIP。 该解决方案的扩展性非常差。最后一列的宽度要么太小,要么太极端,没有多大意义(这就是为什么无法计算最后一列的合理宽度的原因——在这种情况下什么是合理的?)。

第二个更好的解决方案将尝试检测ScrollViewer 是否可以滚动:在这种情况下,最后一列将至少部分看不见。这会为Auto 的最后一列生成合理的Width:所需的最小空间。 另一种情况,当ScrollViewer 无法滚动时,所有列都在视口中可见。这导致*Width 合理,以便让最后一列填充剩余空间:


private void OnLoadingRow(object sender, DataGridRowEventArgs e)

  var dataGrid = sender as DataGrid;

  foreach (var dataGridColumn in dataGrid.Columns)
  
    dataGridColumn.Width = 0;
    dataGridColumn.Width = DataGridLength.Auto;
  

  if (TryFindVisualChildElementByName(dataGrid, string.Empty, out ScrollViewer scrollViewer) 
    && scrollViewer.ScrollableWidth == 0)
  
    dataGrid.Columns.Last().Width = new DataGridLength(1, DataGridLengthUnitType.Star);
  


public static bool TryFindVisualChildElementByName<TChild>(
  DependencyObject parent,
  string childElementName,
  out TChild resultElement) where TChild : FrameworkElement

  resultElement = null;

  if (parent is Popup popup)
  
    parent = popup.Child;
    if (parent == null)
    
      return false;
    
  

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is TChild frameworkElement)
    
      if (string.IsNullOrWhiteSpace(childElementName) 
        || frameworkElement.Name.Equals(childElementName, StringComparison.OrdinalIgnoreCase))
      
        resultElement = frameworkElement;
        return true;
      
    

    if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
    
      return true;
    
  

  return false;


【讨论】:

ty 详细解释。不幸的是,您的解决方案没有解决问题,在我的情况下结果是一样的。 它完全符合您的要求。也许您可以在一个空白项目中对其进行测试,然后找出它在您的应用程序中不起作用的原因。正如我之前所说,您应该提供更多上下文。 该解决方案适用于空项目。但是我更喜欢@Magnus 的代码更少的解决方案。提请您注意。 公认解决方案的问题是使用 Dispatcher 推迟执行是一种不确定的行为。这个解决方案应该是最后的手段。 TryFindVisualChildElementByName 方法应该是您私有库中的扩展方法。所以行数不应该是这里的论点。无论如何,我很高兴我能做出贡献。 是的,建议的解决方案是控制/wpf 更新的通用模式,很难或不可能在一个步骤中完成,但在并发性和幂等性方面必须谨慎使用。在这种情况下,新的 LoadingRow 事件的执行可能与前一个事件的调度执行交错,但最后一次更新将始终处于您想要的状态(除非您在用户滚动时添加/删除列,但下一个 LoadingRow事件修复)。

以上是关于XAML WPF DataGrid:在滚动时减小列宽度以适应其内容,除了一个的主要内容,如果未能解决你的问题,请参考以下文章

WPF xaml:如何使用自动生成的列验证 DataGrid 中的单元格

wpf datagrid 滚动条如何设置宽度和颜色

WPF拖动DataGrid滚动条时内容混乱的解决方法

如何根据属性隐藏wpf datagrid列

wpf datagrid 的单元格内容超出列宽度

WPF DataGrid 同步列宽