我不知道如何在有限的标题中正确地表达我的问题,所以我会尽力解释它。我制作了用于展示的 gif,但在我没有足够的声誉点后注意到。
编辑:我现在对 gif 有足够的声誉!
我有一个 WPF 应用程序,我在其中使用 DataGrid 来显示 ViewModel 列表。此 DataGrid 填充了文件夹中的数据,您可以使用附加到按钮的FolderBrowserDialog 打开该文件夹。当您之前设置路径时,应用程序会记住它,并在启动时自动填充 DataGrid。
这意味着有两种状态:打开应用程序不填充DataGrid,以及打开应用程序填充DataGrid。稍后请注意这一点。
我试图解决的问题是,每当 DataGrid 填充数据时,我需要所有列自动调整大小以适应其内容的宽度。但是,最后一列的宽度需要填充所有剩余的空白空间,因此 DataGrid 内部的行可以在 DataGrid 的整个宽度内选择。同时,当您将应用程序的大小调整到 DataGrid 的宽度变得比列的总宽度窄时,需要出现水平滚动条,以便您可以滚动列,其宽度仍与其内容相同.
这不仅需要在通过按钮填充数据时发生,而且还需要在应用程序启动时自动填充数据时发生。
我的困难在于找到适用于任何情况的解决方案。
我尝试了很多方法,但我将列出最成功的方法:
代码:
<Grid Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="3"
Background="#f7f5f2"
Margin="10">
<DataGrid x:Name="ModList"
ItemsSource="{Binding Mods}"
Style="{DynamicResource DataGridStyle1}"
CellStyle="{DynamicResource DataGridCellStyle1}"
ColumnHeaderStyle="{DynamicResource DataGridColumnHeaderStyle1}"
RowStyle="{DynamicResource DataGridRowStyle1}"
RowDetailsTemplate="{DynamicResource DataGridRowDetailsTemplate1}"
Margin="10"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.SelectDroppedItems="True"
dd:DragDrop.DropHandler="{Binding}"
dd:DragDrop.DropTargetAdornerBrush="#484D54">
<DataGrid.Columns>
<DataGridTemplateColumn x:Name="DataGridColumnEnabled"
Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="CheckBoxIsEnabled"
IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
Command="{Binding DataContext.ToggleCheckBoxCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn x:Name="DataGridColumnLoadorder"
Header="Loadorder"
IsReadOnly="True"
Width="Auto"
Binding="{Binding LoadOrder, UpdateSourceTrigger=PropertyChanged}"
CanUserSort="False"/>
<DataGridTextColumn x:Name="DataGridColumnMod"
Header="Mod"
IsReadOnly="True"
Width="Auto"
Binding="{Binding DisplayName}"
CanUserSort="False"
VirtualizingPanel.VirtualizationMode="Standard"/>
<DataGridTemplateColumn x:Name="DataGridColumnNotification"
IsReadOnly="True"
Width="Auto"
CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="/WarningIcon.png"
Height="16"
Width="16"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Cursor="Help"
Visibility="{Binding HasConflicts}">
<ToolTipService.ToolTip>
<ToolTip Content="Mod(s) detected altering the same asset(s). 
This may be intentional, please check before committing loadorder."/>
</ToolTipService.ToolTip>
</Image>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn x:Name="DataGridColumnAuthor"
Header="Author"
IsReadOnly="True"
Width="Auto"
Binding="{Binding Author}"
CanUserSort="False"/>
<DataGridTextColumn x:Name="DataGridColumnVersion"
Header="Version"
IsReadOnly="True"
Width="Auto"
Binding="{Binding Version}"
CanUserSort="False"/>
<DataGridTextColumn x:Name="DataGridColumnSource"
Header="Source"
IsReadOnly="True"
Width="*"
Binding="{Binding Source}"
CanUserSort="False"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
专业人士:
无论宽度如何,整个 DataGrid 中的行都可以选择。
缺点:
调整大小时列会混在一起,并且没有水平滚动条(调整大小时它会闪烁,但我不知道为什么会这样)。
用数据填充空 DataGrid 会导致列无法根据内容自动调整大小。
代码:
在我的 XAML 中向 DataGrid 添加了事件:
SizeChanged="ModList_SizeChanged"
后台代码:
public partial class MainWindow : Window
{
// Other code
private void ModList_SizeChanged(object sender, EventArgs e)
{
DataGridColumnEnabled.MinWidth = DataGridColumnEnabled.ActualWidth;
DataGridColumnLoadorder.MinWidth = DataGridColumnLoadorder.ActualWidth;
DataGridColumnMod.MinWidth = DataGridColumnMod.ActualWidth;
DataGridColumnNotification.MinWidth = DataGridColumnNotification.ActualWidth;
DataGridColumnAuthor.MinWidth = DataGridColumnAuthor.ActualWidth;
DataGridColumnVersion.MinWidth = DataGridColumnVersion.ActualWidth;
DataGridColumnSource.MinWidth = 200;
}
}
亲:
调整窗口大小时现在会出现水平滚动条
Con
用数据填充空 DataGrid 仍然会导致列无法根据内容自动调整大小。
最后一种方法是我现在一直在使用的方法。我需要找到一个解决方案,该解决方案基本上可以实现相同的优点,但在填充 DataGrid 时也会自动调整所有列的大小。
我开始觉得我的想法是错误的,这就是为什么我决定通过这里寻求帮助。
我真的希望有人能为我提供一个合适的解决方案,让 DataGrid 自动调整大小,在窗口太小时使用水平滚动条。
感谢您阅读这篇文章,我希望我已经以合适的方式构建了它。
期待您的建议!
您不得将列的宽度设置为
*
宽度,因为这会改变排列行为,因为它将强制最大化 *
列的空间。因此,自动列将会缩小(基于 DataGrid
的布局算法。
相反,您必须显式计算最后一列的宽度以使其填充。要保留默认的列大小(例如单元格大小或标题大小),您必须允许
DataGid
完成原始布局算法。然后最后调整最后一列。
对于一个优雅的解决方案,您可能需要考虑扩展
DataGrid
,以便您可以覆盖 ArrangeOverride
以在最后一列前面添加自定义计算。
以下示例扩展
DataGrid
以在内部实现逻辑。或者,您可以将调整逻辑移至附加行为或 DataGrid.SizeChanged
事件的事件处理程序。从设计角度来看,扩展 DataGrid
是最简洁的解决方案,因为布局逻辑已由元素本身正确封装。
<!--
Setting the DataGrid.ColumnWidth is not required.
This example does this because one requirement was
to size the columns based on the cell content.
-->
<ExtendedDataGrid LastChildFill="True"
ColumnWidth="{x:Static DataGridLength.SizeToCells}" />
public class ExtendedDataGrid : DataGrid
{
public bool LastChildFill
{
get => (bool)GetValue(LastChildFillProperty);
set => SetValue(LastChildFillProperty, value);
}
public static readonly DependencyProperty LastChildFillProperty = DependencyProperty.Register(
"LastChildFill",
typeof(bool),
typeof(ExtendedDataGrid),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsArrange, OnLastChildFillChanged));
private static void OnLastChildFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((ExtendedDataGrid)d).OnLastChildFillChanged((bool)e.OldValue, (bool)e.NewValue);
private const string VerticalScrollBarPartName = "PART_VerticalScrollBar";
private ScrollBar PART_VerticalScrollBar { get; set; }
private ScrollBarVisibility HorizontalScrollBarVisibilityInternal { get; set; }
public ExtendedDataGrid()
{
this.HorizontalScrollBarVisibilityInternal = this.HorizontalScrollBarVisibility;
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
this.Loaded -= OnLoaded;
if (TryFindVisualChildElementByName(this, VerticalScrollBarPartName, out ScrollBar scrollBar))
{
this.PART_VerticalScrollBar = scrollBar;
}
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
arrangeBounds = base.ArrangeOverride(arrangeBounds);
HandleLastColumnFill(arrangeBounds.Width);
return arrangeBounds;
}
protected virtual void OnLastChildFillChanged(bool oldValue, bool newValue)
{
if (newValue)
{
// Backup original horizontalScrollBarVisibility
this.HorizontalScrollBarVisibilityInternal = this.HorizontalScrollBarVisibility;
}
else
{
// Restore original horizontalScrollBarVisibility
SetCurrentValue(HorizontalScrollBarVisibilityProperty, this.HorizontalScrollBarVisibilityInternal);
}
}
private void HandleLastColumnFill(double availableWidth)
{
if (!this.LastChildFill
|| !this.IsLoaded
|| !this.HasItems)
{
return;
}
ScrollBarVisibility horizontalScrollBarVisibility = TryMakeLastColumnFill(availableWidth)
? ScrollBarVisibility.Disabled
: ScrollBarVisibility.Visible;
SetCurrentValue(HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility);
}
private bool TryMakeLastColumnFill(double availableWidth)
{
double totalPrecedingColumnWidth = this.Columns.SkipLast(1).Sum(column => column.ActualWidth);
double verticalScrollBarWidth = this.PART_VerticalScrollBar?.Width ?? 0;
DataGridColumn lastColumn = this.Columns.Last();
double maxCellContentWidth = this.Items
.Cast<object>()
.Max(item => lastColumn.GetCellContent(item).DesiredSize.Width);
double desiredLastColumnWidth = maxCellContentWidth + verticalScrollBarWidth;
double lastColumnAvailableWidth = availableWidth - (totalPrecedingColumnWidth);
bool canLastColumnFill = lastColumnAvailableWidth > desiredLastColumnWidth;
DataGridLength defaultColumnWidth = this.ColumnWidth;
if (canLastColumnFill)
{
lastColumn.Width = new DataGridLength(lastColumnAvailableWidth, DataGridLengthUnitType.Pixel);
}
else
{
lastColumn.Width = defaultColumnWidth;
}
return canLastColumnFill;
}
private 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 (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is FrameworkElement frameworkElement
&& frameworkElement.Name.Equals(childElementName, StringComparison.OrdinalIgnoreCase))
{
resultElement = frameworkElement as TChild;
return true;
}
if (child.TryFindVisualChildElementByName<TChild>(childElementName, out resultElement))
{
return true;
}
}
return false;
}
}