XAML WPF DataGrid:滚动时减小列宽以适合其内容,只有一个除外
XAML WPF DataGrid: reduce columns width to fit its content while scrolling except one
是在滚动期间自动减小 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(参见图片)。
更新:
我要求描述行为的原因是 select 当鼠标单击行的任何位置时编辑该行。当我单击该行的右侧时,该行没有 select - 没有单元格。我通过添加 MouseLeftButtonDown
描述的事件处理程序实现了这一点 .
UpdateLayout
调用完全是多余的。修改 Width
将自动触发布局过程。此外,为了提高性能,应该将两个迭代合并为一个迭代。
就用户体验而言,完整的实现是非常值得怀疑的:如果用户调整列的宽度,您将覆盖并重置他的自定义,这提供了糟糕的体验。此外,网格可能会不断调整自身大小,这看起来和感觉起来也很奇怪。您至少应该将 DataGrid.CanUserResizeColumns
设置为 false
。
我相信添加一个列过滤器,允许用户隐藏特定列以压缩视图,具有更高的价值。
根据您发布的代码,无法准确判断发生了什么或可能导致问题的原因。 DataGrid
控件及其上下文的实际布局配置显然未公开。
但是,问题可能与 DataGrid
如何计算每列的宽度有关,以防您给 DataGrid
一个固定的 Width
而这个值会导致超出可用视口宽度的内容(在这种情况下会出现 ScrollViewer
):
a) 如果每列宽度设置为 Auto
,DataGrid
将简单地为每列提供所需的最小宽度。
b) 如果列的宽度设置为星号 *
大小,则分配的宽度是相对于可用的剩余 space.
计算的
为了计算出合理的值,该算法将使用 DataGrid
.
的有效大小
关键是,当使用 ScrollViewer
时,虚拟宽度将是无限的。现在,如果算法将最后一列的宽度设置为无限大,您将得到一个非常小的滚动条。因此,无限不是一个合理的值。实际上,无法计算最终宽度。这就是为什么算法回退使用 DataGrid
的有效宽度,即 ScrollViewer.ViewportWidth
。结果是所有列都必须压缩到可用视口 space 中,导致列剪切其 headers 和单元格内容。
我可以提供两种解决方案。
第一个解决方案是对最后一列使用固定的 Width
,例如 1000
DIPs。
该解决方案的扩展性非常差。最后一列的宽度要么太小,要么宽度太大,这没有多大意义(这就是为什么无法计算出最后一列的合理宽度的原因 - 在这种情况下什么是合理的?)。
第二个更好的解决方案是尝试检测 ScrollViewer
是否可以滚动:在这种情况下,最后一列至少会部分不在视图中。这导致 Auto
最后一列的合理 Width
:所需的最小值 space.
另一种情况,当 ScrollViewer
无法滚动时,所有列都在视口中可见。这导致 *
的合理 Width
以允许最后一列填充剩余的 space:
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;
}
似乎 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);
}
编辑 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);
}
旧的解决方案:
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(参见图片)。
更新:
我要求描述行为的原因是 select 当鼠标单击行的任何位置时编辑该行。当我单击该行的右侧时,该行没有 select - 没有单元格。我通过添加 MouseLeftButtonDown
描述的事件处理程序实现了这一点
UpdateLayout
调用完全是多余的。修改 Width
将自动触发布局过程。此外,为了提高性能,应该将两个迭代合并为一个迭代。
就用户体验而言,完整的实现是非常值得怀疑的:如果用户调整列的宽度,您将覆盖并重置他的自定义,这提供了糟糕的体验。此外,网格可能会不断调整自身大小,这看起来和感觉起来也很奇怪。您至少应该将 DataGrid.CanUserResizeColumns
设置为 false
。
我相信添加一个列过滤器,允许用户隐藏特定列以压缩视图,具有更高的价值。
根据您发布的代码,无法准确判断发生了什么或可能导致问题的原因。 DataGrid
控件及其上下文的实际布局配置显然未公开。
但是,问题可能与 DataGrid
如何计算每列的宽度有关,以防您给 DataGrid
一个固定的 Width
而这个值会导致超出可用视口宽度的内容(在这种情况下会出现 ScrollViewer
):
a) 如果每列宽度设置为 Auto
,DataGrid
将简单地为每列提供所需的最小宽度。
b) 如果列的宽度设置为星号 *
大小,则分配的宽度是相对于可用的剩余 space.
计算的
为了计算出合理的值,该算法将使用 DataGrid
.
的有效大小
关键是,当使用 ScrollViewer
时,虚拟宽度将是无限的。现在,如果算法将最后一列的宽度设置为无限大,您将得到一个非常小的滚动条。因此,无限不是一个合理的值。实际上,无法计算最终宽度。这就是为什么算法回退使用 DataGrid
的有效宽度,即 ScrollViewer.ViewportWidth
。结果是所有列都必须压缩到可用视口 space 中,导致列剪切其 headers 和单元格内容。
我可以提供两种解决方案。
第一个解决方案是对最后一列使用固定的 Width
,例如 1000
DIPs。
该解决方案的扩展性非常差。最后一列的宽度要么太小,要么宽度太大,这没有多大意义(这就是为什么无法计算出最后一列的合理宽度的原因 - 在这种情况下什么是合理的?)。
第二个更好的解决方案是尝试检测 ScrollViewer
是否可以滚动:在这种情况下,最后一列至少会部分不在视图中。这导致 Auto
最后一列的合理 Width
:所需的最小值 space.
另一种情况,当 ScrollViewer
无法滚动时,所有列都在视口中可见。这导致 *
的合理 Width
以允许最后一列填充剩余的 space:
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;
}
似乎 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);
}
编辑 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);
}