【发布时间】:2011-03-26 12:01:49
【问题描述】:
我希望用户能够将单元格置于编辑模式并通过单击突出显示单元格所在的行。默认情况下,这是双击。
我如何覆盖或实现它?
【问题讨论】:
-
您在使用 WPF 工具包中的 DataGrid 吗?
-
您是否可以向我们提供更多信息,说明您尝试过什么以及它是如何不起作用的?
我希望用户能够将单元格置于编辑模式并通过单击突出显示单元格所在的行。默认情况下,这是双击。
我如何覆盖或实现它?
【问题讨论】:
其中几个答案以及this blog post 启发了我,但每个答案都留下了一些不足之处。我结合了它们中最好的部分,并提出了这个相当优雅的解决方案,它似乎可以完全正确地获得用户体验。
这使用了一些 C# 9 语法,尽管您可能需要在项目文件中进行设置,但它在任何地方都可以正常工作:
<LangVersion>9</LangVersion>
将此类添加到您的项目中:
public static class WpfHelpers
{
internal static void DataGridPreviewMouseLeftButtonDownEvent(object sender, RoutedEventArgs e)
{
// The original source for this was inspired by https://softwaremechanik.wordpress.com/2013/10/02/how-to-make-all-wpf-datagrid-cells-have-a-single-click-to-edit/
DataGridCell? cell = e is MouseButtonEventArgs { OriginalSource: UIElement clickTarget } ? FindVisualParent<DataGridCell>(clickTarget) : null;
if (cell is { IsEditing: false, IsReadOnly: false })
{
if (!cell.IsFocused)
{
cell.Focus();
}
if (FindVisualParent<DataGrid>(cell) is DataGrid dataGrid)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
{
cell.IsSelected = true;
}
}
else
{
if (FindVisualParent<DataGridRow>(cell) is DataGridRow { IsSelected: false } row)
{
row.IsSelected = true;
}
}
}
}
}
internal static T? GetFirstChildByType<T>(DependencyObject prop)
where T : DependencyObject
{
int count = VisualTreeHelper.GetChildrenCount(prop);
for (int i = 0; i < count; i++)
{
if (VisualTreeHelper.GetChild(prop, i) is DependencyObject child)
{
T? typedChild = child as T ?? GetFirstChildByType<T>(child);
if (typedChild is object)
{
return typedChild;
}
}
}
return null;
}
private static T? FindVisualParent<T>(UIElement element)
where T : UIElement
{
UIElement? parent = element;
while (parent is object)
{
if (parent is T correctlyTyped)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
}
将此添加到您的 App.xaml.cs 文件中:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
EventManager.RegisterClassHandler(
typeof(DataGrid),
DataGrid.PreviewMouseLeftButtonDownEvent,
new RoutedEventHandler(WpfHelpers.DataGridPreviewMouseLeftButtonDownEvent));
}
将此添加到包含DataGrid的页面后面的代码中:
private void TransactionDataGrid_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
{
WpfHelpers.GetFirstChildByType<Control>(e.EditingElement)?.Focus();
}
并将其连接起来(在 XAML 中):PreparingCellForEdit="TransactionDataGrid_PreparingCellForEdit"
【讨论】:
我稍微修改了 Dušan Knežević 的解决方案
<DataGrid.Resources>
<Style x:Key="ddlStyle" TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
并将样式应用到我的愿望列
<DataGridComboBoxColumn CellStyle="{StaticResource ddlStyle}">
【讨论】:
如果您对单元格保持文本框感到满意,这是一个简单的解决方案(不区分编辑模式和非编辑模式)。这种方式单击编辑可以开箱即用。这也适用于组合框和按钮等其他元素。否则使用更新下方的解决方案。
<DataGridTemplateColumn Header="My Column header">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding MyProperty } />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
我尝试了在这里和谷歌上找到的所有内容,甚至尝试创建自己的版本。但是每个答案/解决方案主要适用于文本框列,但不适用于所有其他元素(复选框、组合框、按钮列),甚至破坏了其他元素列或有一些其他副作用。感谢微软让 datagrid 表现得如此丑陋,并迫使我们创建这些黑客。因此,我决定制作一个可以直接将样式应用于文本框列而不影响其他列的版本。
我使用了这个解决方案和@m-y 的答案并将它们修改为附加行为。 http://wpf-tutorial-net.blogspot.com/2016/05/wpf-datagrid-edit-cell-on-single-click.html
添加此样式。
当您将一些 fancy styles 用于您的数据网格并且您不想丢失它们时,BasedOn 很重要。
<Window.Resources>
<Style x:Key="SingleClickEditStyle" TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter Property="local:DataGridTextBoxSingleClickEditBehavior.Enable" Value="True" />
</Style>
</Window.Resources>
将带有CellStyle 的样式应用于您的每个DataGridTextColumns,如下所示:
<DataGrid ItemsSource="{Binding MyData}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="My Header" Binding="{Binding Comment}" CellStyle="{StaticResource SingleClickEditStyle}" />
</DataGrid.Columns>
</DataGrid>
现在将这个类添加到与您的 MainViewModel 相同的命名空间(或不同的命名空间。但是您将需要使用除 local 之外的其他命名空间前缀)。欢迎来到附加行为的丑陋样板代码世界。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace YourMainViewModelNameSpace
{
public static class DataGridTextBoxSingleClickEditBehavior
{
public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(DataGridTextBoxSingleClickEditBehavior),
new FrameworkPropertyMetadata(false, OnEnableChanged));
public static bool GetEnable(FrameworkElement frameworkElement)
{
return (bool) frameworkElement.GetValue(EnableProperty);
}
public static void SetEnable(FrameworkElement frameworkElement, bool value)
{
frameworkElement.SetValue(EnableProperty, value);
}
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DataGridCell dataGridCell)
dataGridCell.PreviewMouseLeftButtonDown += DataGridCell_PreviewMouseLeftButtonDown;
}
private static void DataGridCell_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
EditCell(sender as DataGridCell, e);
}
private static void EditCell(DataGridCell dataGridCell, RoutedEventArgs e)
{
if (dataGridCell == null || dataGridCell.IsEditing || dataGridCell.IsReadOnly)
return;
if (dataGridCell.IsFocused == false)
dataGridCell.Focus();
var dataGrid = FindVisualParent<DataGrid>(dataGridCell);
dataGrid?.BeginEdit(e);
}
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
var parent = VisualTreeHelper.GetParent(element) as UIElement;
while (parent != null)
{
if (parent is T parentWithCorrectType)
return parentWithCorrectType;
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
}
}
【讨论】:
我是这样解决这个问题的:
<DataGrid DataGridCell.Selected="DataGridCell_Selected"
ItemsSource="{Binding Source={StaticResource itemView}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Nom" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"/>
</DataGrid.Columns>
</DataGrid>
此 DataGrid 绑定到 CollectionViewSource(包含虚拟 Person 对象)。
神奇的地方发生了:DataGridCell.Selected="DataGridCell_Selected"。
我只是简单地挂钩 DataGrid 单元格的 Selected 事件,然后在 DataGrid 上调用 BeginEdit()。
这是事件处理程序的代码:
private void DataGridCell_Selected(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
【讨论】:
SelectionUnit 属性设置为 Cell 来解决已经选择的行问题。
grd.BeginEdit(e) 之后,我希望该单元格中的 TextBox 具有焦点。我怎样才能做到这一点?我尝试在 DataGridCell 和 DataGrid 上调用 FindName("txtBox"),但它为我返回 null。
http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing 的解决方案对我来说效果很好,但我使用 ResourceDictionary 中定义的样式为每个 DataGrid 启用了它。要在资源字典中使用处理程序,您需要向其中添加代码隐藏文件。以下是你的做法:
这是一个 DataGridStyles.xaml 资源字典:
<ResourceDictionary x:Class="YourNamespace.DataGridStyles"
x:ClassModifier="public"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="DataGrid">
<!-- Your DataGrid style definition goes here -->
<!-- Cell style -->
<Setter Property="CellStyle">
<Setter.Value>
<Style TargetType="DataGridCell">
<!-- Your DataGrid Cell style definition goes here -->
<!-- Single Click Editing -->
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="DataGridCell_PreviewMouseLeftButtonDown" />
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
注意根元素中的 x:Class 属性。 创建一个类文件。在本例中,它是 DataGridStyles.xaml.cs。把这段代码放进去:
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;
namespace YourNamespace
{
partial class DataGridStyles : ResourceDictionary
{
public DataGridStyles()
{
InitializeComponent();
}
// The code from the myermian's answer goes here.
}
【讨论】:
我知道我参加聚会有点晚了,但我遇到了同样的问题并想出了一个不同的解决方案:
public class DataGridTextBoxColumn : DataGridBoundColumn
{
public DataGridTextBoxColumn():base()
{
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
throw new NotImplementedException("Should not be used.");
}
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var control = new TextBox();
control.Style = (Style)Application.Current.TryFindResource("textBoxStyle");
control.FontSize = 14;
control.VerticalContentAlignment = VerticalAlignment.Center;
BindingOperations.SetBinding(control, TextBox.TextProperty, Binding);
control.IsReadOnly = IsReadOnly;
return control;
}
}
<DataGrid Grid.Row="1" x:Name="exportData" Margin="15" VerticalAlignment="Stretch" ItemsSource="{Binding CSVExportData}" Style="{StaticResource dataGridStyle}">
<DataGrid.Columns >
<local:DataGridTextBoxColumn Header="Sample ID" Binding="{Binding SampleID}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Analysis Date" Binding="{Binding Date}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Test" Binding="{Binding Test}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Comment" Binding="{Binding Comment}"></local:DataGridTextBoxColumn>
</DataGrid.Columns>
</DataGrid>
如您所见,我编写了自己的 DataGridTextColumn,继承了 DataGridBoundColumn 的所有内容。通过覆盖 GenerateElement 方法并在此处返回一个文本框控件,永远不会调用用于生成编辑元素的方法。 在另一个项目中,我使用它来实现 Datepicker 列,因此这也适用于复选框和组合框。
这似乎不会影响数据网格的其他行为。至少到目前为止我没有注意到任何副作用,也没有收到任何负面反馈。
【讨论】:
我在 MVVM 中寻找单击编辑单元格,这是另一种方法。
在 xaml 中添加行为
<UserControl xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:myBehavior="clr-namespace:My.Namespace.To.Behavior">
<DataGrid>
<i:Interaction.Behaviors>
<myBehavior:EditCellOnSingleClickBehavior/>
</i:Interaction.Behaviors>
</DataGrid>
</UserControl>
EditCellOnSingleClickBehavior 类扩展 System.Windows.Interactivity.Behavior;
public class EditCellOnSingleClick : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.LoadingRow += this.OnLoadingRow;
this.AssociatedObject.UnloadingRow += this.OnUnloading;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.LoadingRow -= this.OnLoadingRow;
this.AssociatedObject.UnloadingRow -= this.OnUnloading;
}
private void OnLoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.GotFocus += this.OnGotFocus;
}
private void OnUnloading(object sender, DataGridRowEventArgs e)
{
e.Row.GotFocus -= this.OnGotFocus;
}
private void OnGotFocus(object sender, RoutedEventArgs e)
{
this.AssociatedObject.BeginEdit(e);
}
}
瞧!
【讨论】:
OnGotFocus,否则,单击单元格上的 Enter 将再次触发此方法并且不提交编辑:var row = sender as Row; if (!row.IsEditing) this.AssociatedObject.BeginEdit(e);
根据 Dušan Knežević 的建议,我更喜欢这种方式。你点击就可以了))
<DataGrid.Resources>
<Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver"
Value="True" />
<Condition Property="IsReadOnly"
Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="IsEditing"
Value="True" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
【讨论】:
<DataGridComboBoxColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="cal:Message.Attach"
Value="[Event MouseLeftButtonUp] = [Action ReachThisMethod($source)]"/>
</Style>
</DataGridComboBoxColumn.CellStyle>
public void ReachThisMethod(object sender)
{
((System.Windows.Controls.DataGridCell)(sender)).IsEditing = true;
}
【讨论】:
我通过添加一个触发器解决了这个问题,当鼠标悬停在 DataGridCell 上时,它将 IsEditing 属性设置为 True。它解决了我的大部分问题。它也适用于组合框。
<Style TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
【讨论】:
user2134678 的回答有两个问题。一种是非常轻微的,没有功能作用。另一个相当重要。
第一个问题是 GotFocus 实际上是针对 DataGrid 调用的,而不是实际中的 DataGridCell。 XAML 中的 DataGridCell 限定符是多余的。
我发现答案的主要问题是 Enter 键的行为被破坏了。 Enter 应该将您移动到正常 DataGrid 行为中当前单元格下方的下一个单元格。然而,在幕后实际发生的是 GotFocus 事件将被调用两次。一次是当前单元格失去焦点,一次是新单元格获得焦点。但只要在第一个单元格上调用 BeginEdit,就永远不会激活下一个单元格。结果是您可以一键编辑,但任何不按字面单击网格的人都会感到不便,并且用户界面设计人员不应假设所有用户都在使用鼠标。 (键盘用户可以通过使用 Tab 来绕过它,但这仍然意味着他们正在跳过他们不应该这样做的障碍。)
那么这个问题的解决方案呢?处理单元格的 KeyDown 事件,如果 Key 是 Enter 键,则设置一个标志以阻止 BeginEdit 在第一个单元格上触发。现在 Enter 键的行为正常。
首先,将以下样式添加到您的 DataGrid:
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}" x:Key="SingleClickEditingCellStyle">
<EventSetter Event="KeyDown" Handler="DataGridCell_KeyDown" />
</Style>
</DataGrid.Resources>
将该样式应用于要启用一键单击的列的“CellStyle”属性。
然后在后面的代码中,您的 GotFocus 处理程序中有以下内容(请注意,我在这里使用 VB,因为这是我们的“一键式数据网格请求”客户端想要的开发语言):
Private _endEditing As Boolean = False
Private Sub DataGrid_GotFocus(ByVal sender As Object, ByVal e As RoutedEventArgs)
If Me._endEditing Then
Me._endEditing = False
Return
End If
Dim cell = TryCast(e.OriginalSource, DataGridCell)
If cell Is Nothing Then
Return
End If
If cell.IsReadOnly Then
Return
End If
DirectCast(sender, DataGrid).BeginEdit(e)
.
.
.
然后为 KeyDown 事件添加处理程序:
Private Sub DataGridCell_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
If e.Key = Key.Enter Then
Me._endEditing = True
End If
End Sub
现在您有了一个未更改开箱即用实现的任何基本行为并且支持单击编辑的 DataGrid。
【讨论】:
Micael Bergeron 的回答是我找到适合我的解决方案的良好开端。为了允许对已经处于编辑模式的同一行中的单元格进行单击编辑,我不得不对其进行一些调整。使用 SelectionUnit 单元格对我来说是没有选择的。
我使用 DataGridCell.GotFocus 事件,而不是使用仅在第一次单击行的单元格时触发的 DataGridCell.Selected 事件。
<DataGrid DataGridCell.GotFocus="DataGrid_CellGotFocus" />
如果这样做,您将始终保持正确的单元格聚焦并处于编辑模式,但单元格中的控件不会聚焦,我这样解决了
private void DataGrid_CellGotFocus(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
Control control = GetFirstChildByType<Control>(e.OriginalSource as DataGridCell);
if (control != null)
{
control.Focus();
}
}
}
private T GetFirstChildByType<T>(DependencyObject prop) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(prop); i++)
{
DependencyObject child = VisualTreeHelper.GetChild((prop), i) as DependencyObject;
if (child == null)
continue;
T castedProp = child as T;
if (castedProp != null)
return castedProp;
castedProp = GetFirstChildByType<T>(child);
if (castedProp != null)
return castedProp;
}
return null;
}
【讨论】:
发件人:http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing
XAML:
<!-- SINGLE CLICK EDITING -->
<Style TargetType="{x:Type dg:DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
</Style>
代码隐藏:
//
// SINGLE CLICK EDITING
//
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
{
if (!cell.IsFocused)
{
cell.Focus();
}
DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
if (dataGrid != null)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
cell.IsSelected = true;
}
else
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null && !row.IsSelected)
{
row.IsSelected = true;
}
}
}
}
}
static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
【讨论】: