WPF DataGrid,在 header 和行之间添加一个标签

WPF DataGrid, add a lable between header and rows

当数据网格中有一些关于数据的通知时,我想如下图所示。
这在 windows 中作为通知很常见,但在控件内部并不常见。我认为 WPF 应该能够通过使用控件模板来执行此操作,但我对这个主题还不是很有经验。我需要直接跳入这些内容,所以我需要 Datagrid 模板的基础知识。

此处 post 的解决方案假装只是最终解决方案的起点,而不是完整的解决方案。

要实现您的目标,您必须开始研究 DataGrid 风格,正如您已经指出的那样。 如 this post, you might get the Style by using Expression Blend 中所述或查看默认的 Microsoft 样式。

我这里选了第二个,为了有一个清晰的例子。因此,我将以下代码基于代码 posted Microsoft Docs。请注意,此处的代码可能与原始 Microsoft Controls 代码略有不同,如上文 post 中所述。

在post输入完整代码之前,让我们看看结果如何:

这里的 NOTIFICATION BAR 只是一个带有 hard-coded 内容的 Label -> 这是自定义通知栏(颜色、绑定、可见性等)的起点。 .) 根据您的要求,您的第一个 post 不是很清楚。 如您所见,在默认 Datagrid 中有一个不可见的渐变,正如我解释的那样,Microsoft 网站上的样式 posted 可能与默认的略有不同。

完整代码很长,所以我post编辑了它here。 为了帮助浏览代码,请注意我刚刚在主 Grid 中添加了一个 Row(请在第 225 行找到一个战利品)和上面提到的第 251 行中的 Label

您不能单独使用控件模板来完成此操作。控件模板定义控件的外观和状态。在您的情况下,您添加了需要一些代码的功能。您可以使用附加属性、触发器和改编的控件模板来解决此问题,但编写自定义控件似乎更合适。

编写自定义控件可能会非常复杂,因此我将重点关注您问题的简单解决方案。我假设您希望一次显示一个通知。但是,以下解决方案也很容易扩展以支持 collection 通知。

首先,我创建了一个派生自 DataGrid 的自定义控件。它为通知的可见性、其文本和关闭按钮文本提供依赖属性。它还公开了一个 RoutedUICommand 并注册了一个绑定到它的命令。它将用于关闭通知。

[TemplatePart(Name = NotificationBorderPart, Type = typeof(Border))]
[TemplatePart(Name = NotificationTextBlockPart, Type = typeof(TextBlock))]
[TemplatePart(Name = NotificationButtonPart, Type = typeof(Button))]
public class NotifyingDataGrid : DataGrid
{
   private const string NotificationBorderPart = "PART_NotificationBorder";
   private const string NotificationTextBlockPart = "PART_NotificationTextBlock";
   private const string NotificationButtonPart = "PART_NotificationButton";

   static NotifyingDataGrid()
   {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(NotifyingDataGrid), new FrameworkPropertyMetadata());
   }

   public static readonly RoutedUICommand Dismiss = new RoutedUICommand("Dismisses a notification", "Dismiss", typeof(NotifyingDataGrid));

   public static readonly DependencyProperty NotificationVisibilityProperty = DependencyProperty.Register(
      nameof(NotificationVisibility), typeof(Visibility), typeof(NotifyingDataGrid), new PropertyMetadata(Visibility.Collapsed));

   public static readonly DependencyProperty NotificationTextProperty = DependencyProperty.Register(
      nameof(NotificationText), typeof(string), typeof(NotifyingDataGrid), new PropertyMetadata(string.Empty, OnNotificationTextChanged));

   public static readonly DependencyProperty DismissTextProperty = DependencyProperty.Register(
      nameof(DismissText), typeof(string), typeof(NotifyingDataGrid), new PropertyMetadata("Dismiss"));

   public NotifyingDataGrid()
   {
      CommandBindings.Add(new CommandBinding(Dismiss, OnDismiss));
   }

   public Visibility NotificationVisibility
   {
      get => (Visibility)GetValue(NotificationVisibilityProperty);
      set => SetValue(NotificationVisibilityProperty, value);
   }

   public string NotificationText
   {
      get => (string)GetValue(NotificationTextProperty);
      set => SetValue(NotificationTextProperty, value);
   }

   public string DismissText
   {
      get => (string)GetValue(DismissTextProperty);
      set => SetValue(DismissTextProperty, value);
   }

   private void OnDismiss(object sender, ExecutedRoutedEventArgs e)
   {
      NotificationVisibility = Visibility.Collapsed;
   }

   private static void OnNotificationTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   {
      var notifyingDataGrid = (NotifyingDataGrid)d;
      notifyingDataGrid.NotificationVisibility = Visibility.Visible;
   }
}

我使用 Blend 创建了原始控件模板的副本。我用代表通知 header 的 Border 控件扩展了它。其中有一个TextBlock和一个ButtonText 属性绑定到我们的 parent 自定义数据网格的依赖属性。按钮的命令使用Dismiss命令,由我们的命令绑定来处理

<Style x:Key="{ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}" TargetType="{x:Type Button}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type Button}">
            <Grid>
               <Rectangle x:Name="Border" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" SnapsToDevicePixels="True"/>
               <Polygon x:Name="Arrow" Fill="Black" HorizontalAlignment="Right" Margin="8,8,3,3" Opacity="0.15" Points="0,10 10,10 10,0" Stretch="Uniform" VerticalAlignment="Bottom"/>
            </Grid>
            <ControlTemplate.Triggers>
               <Trigger Property="IsMouseOver" Value="True">
                  <Setter Property="Stroke" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
               </Trigger>
               <Trigger Property="IsPressed" Value="True">
                  <Setter Property="Fill" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
               </Trigger>
               <Trigger Property="IsEnabled" Value="False">
                  <Setter Property="Visibility" TargetName="Arrow" Value="Collapsed"/>
               </Trigger>
            </ControlTemplate.Triggers>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>
<Style TargetType="{x:Type local:NotifyingDataGrid}">
   <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
   <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
   <Setter Property="BorderBrush" Value="#FF688CAF"/>
   <Setter Property="BorderThickness" Value="1"/>
   <Setter Property="RowDetailsVisibilityMode" Value="VisibleWhenSelected"/>
   <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
   <Setter Property="ScrollViewer.PanningMode" Value="Both"/>
   <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type local:NotifyingDataGrid}">
            <Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
               <ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
                  <ScrollViewer.Template>
                     <ControlTemplate TargetType="{x:Type ScrollViewer}">
                        <Grid>
                           <Grid.ColumnDefinitions>
                              <ColumnDefinition Width="Auto"/>
                              <ColumnDefinition Width="*"/>
                              <ColumnDefinition Width="Auto"/>
                           </Grid.ColumnDefinitions>
                           <Grid.RowDefinitions>
                              <RowDefinition Height="Auto"/>
                              <RowDefinition Height="Auto"/>
                              <RowDefinition Height="*"/>
                              <RowDefinition Height="Auto"/>
                           </Grid.RowDefinitions>
                           <Button Command="{x:Static DataGrid.SelectAllCommand}" Focusable="false" Style="{DynamicResource {ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}}" Visibility="{Binding HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.All}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
                           <DataGridColumnHeadersPresenter x:Name="PART_ColumnHeadersPresenter" Grid.Column="1" Visibility="{Binding HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
                           <Border x:Name="PART_NotificationBorder" Grid.Row="1" Grid.Column="1" BorderBrush="Black" BorderThickness="1" Background="Yellow" Visibility="{Binding NotificationVisibility, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}">
                              <Grid>
                                 <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition Width="Auto"/>
                                 </Grid.ColumnDefinitions>
                                 <TextBlock x:Name="PART_NotificationTextBlock" Grid.Column="0" Text="{Binding NotificationText, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}" FontStyle="Italic" Foreground="Gray" TextTrimming="CharacterEllipsis"/>
                                 <Button x:Name="PART_NotificationButton" Grid.Column="1" BorderThickness="0" Background="Yellow" TextBlock.FontStyle="Italic" TextBlock.FontWeight="Bold" TextBlock.Foreground="Gray" Content="{Binding DismissText, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}" Command="local:NotifyingDataGrid.Dismiss"/>
                              </Grid>
                           </Border>
                           <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" Grid.ColumnSpan="2" CanContentScroll="{TemplateBinding CanContentScroll}" Grid.Row="2"/>
                           <ScrollBar x:Name="PART_VerticalScrollBar" Grid.Column="2" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" Grid.Row="2" ViewportSize="{TemplateBinding ViewportHeight}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
                           <Grid Grid.Column="1" Grid.Row="3">
                              <Grid.ColumnDefinitions>
                                 <ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
                                 <ColumnDefinition Width="*"/>
                              </Grid.ColumnDefinitions>
                              <ScrollBar x:Name="PART_HorizontalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
                           </Grid>
                        </Grid>
                     </ControlTemplate>
                  </ScrollViewer.Template>
                  <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
               </ScrollViewer>
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsGrouping" Value="true"/>
            <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
         </MultiTrigger.Conditions>
         <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
      </MultiTrigger>
   </Style.Triggers>
</Style>

你现在可以绑定通知文本和关闭按钮文本,以及通知可见性,当你想从视图模型中隐藏它时,它会派上用场。

<local:NotifyingDataGrid ItemsSource="{Binding MyItems}"
                         NotificationText="{Binding MyNotificationText}"
                         DismissText="{Binding MyDismissText}"/>

请注意,我在这里定义了模板部件,尽管它们并没有被直接使用。这是因为我想专注于使用绑定的简单解决方案。正如您在控件的控件模板中看到的那样,通知 header 控件嵌套在 ScrollViewer 的另一个控件模板中,它们不能像通常在自定义中那样被 GetTemplateChild 访问控制。因此,将此解决方案作为起点。

如果你想显示多个通知,你可以很容易地调整这个控件。您将公开包含所有通知文本的 collection 类型的依赖项 属性。在控件模板中,您可以放置​​一个 ItemsControl 作为绑定到此 collection 的通知 header。要像以前一样显示每个项目,您可以将 DataTemplate 添加为 ItemTemplate,其中包含 BorderTextBlockButton。将当前项目作为 CommandParameter 传递,以便您可以访问和关闭焦点通知。