WPF数据表格:在WPF中创建一个数字列的数据表格。

11

我有一个需求,需要制作一个数据网格列(datagridcolumn),只允许输入数字值(整数),当用户输入非数字时要处理文本框。 我尝试了很多网页,已经感到疲倦,非常感谢任何愿意提供帮助的人。


1
您可以创建一个继承自DataGridColumn的类,并在其中执行数字验证... - Omri Btian
1
@Omribitan 如果你有任何例子,能否发布链接,这对我来说将是一个很大的帮助。 - Mussammil
8个回答

16

根据 @nit 的建议,您可以创建一个派生自 DataGridTextColumn 的自定义类,如下:

基于@nit的建议,你可以创建一个派生自DataGridTextColumn的自定义类,像这样:

public class DataGridNumericColumn : DataGridTextColumn
{
    protected override object PrepareCellForEdit(System.Windows.FrameworkElement editingElement, System.Windows.RoutedEventArgs editingEventArgs)
    {
        TextBox edit = editingElement as TextBox;
        edit.PreviewTextInput += OnPreviewTextInput;

        return base.PrepareCellForEdit(editingElement, editingEventArgs);
    }

    void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        try
        {
            Convert.ToInt32(e.Text);
        }
        catch
        {
            // Show some kind of error message if you want

            // Set handled to true
            e.Handled = true;
        }
    }
}

PrepareCellForEdit方法中,您需要将OnPreviewTextInput方法注册到编辑TextBoxPreviewTextInput事件,从而验证数字值。

在XAML中,您只需使用它:

    <DataGrid ItemsSource="{Binding SomeCollection}">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding NonNumericProperty}"/>
            <local:DataGridNumericColumn Binding="{Binding NumericProperty}"/>
        </DataGrid.Columns>
    </DataGrid>
希望这有所帮助。

4

请使用 TryParse,这有助于将输入值限制为仅为整数数字。

    /// <summary>
    /// This class help to create data grid cell which only support interger numbers.
    /// </summary>
    public class DataGridNumericColumn : DataGridTextColumn
    {
        protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
        {
            TextBox edit = editingElement as TextBox;

            if (edit != null) edit.PreviewTextInput += OnPreviewTextInput;

            return base.PrepareCellForEdit(editingElement, editingEventArgs);
        }

        private void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
        {
            int value;

            if (!int.TryParse(e.Text, out value))
                e.Handled = true;
        }
    }

2
这是一个更好的解决方案,避免引发异常。只需从@Yoav添加“粘贴”功能即可。 - user12805184

3
如果您不想显示任何验证错误,只想阻止任何非数字值,则可以创建`DataGridTemplateColumn`并在`CellEditingTemplate`中使用`TextBox`。
                <DataGridTemplateColumn Width="100*">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Path=NumericProperty}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox PreviewTextInput="TextBox_PreviewTextInput" Text="{Binding Path=NumericProperty}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>

在TextBox的PreviewTextInput事件中,如果输入的值不是整数,则设置e.Handled = true。
       private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            try
            {
                Convert.ToInt32(e.Text);
            }
            catch
            {
                e.Handled = true;
            }
        }

谢谢,但是实际上我无法在我的需求中使用DataGridTemplateColumn。 - Mussammil

3
我遇到了与您相同的问题,希望在DataGrid中限制输入为数字。但是已接受的答案对我没有用。以下是可行的解决方案:
  1. 为DataGrid添加PreparingForCellEdit事件处理程序。
  2. 在该事件处理程序中,将EditingElement强制转换为TextBox,并为TextBox添加PreviewTextInput事件处理程序。
  3. 在PreviewTextInput事件处理程序中,如果不允许输入,则将e.Handled设置为true。
上述步骤仅适用于用户点击单元格以编辑的情况。然而,如果单元格不处于编辑模式,则不会调用PreparingForCellEdit事件。为了在这种情况下执行验证,请按照以下步骤进行操作:
  1. 为DataGrid添加PreviewTextInput事件处理程序。
  2. 在该事件处理程序中,将e.OriginalSource安全地强制转换为DataGridCell(如果不是DataGridCell则退出),检查DataGridCell的IsEditing属性,如果单元格未处于编辑状态,则将e.Handled设置为true。
上述方法的效果是,用户必须单击单元格才能编辑其内容,因此,对单元格内容的所有更改都将调用上述PreparingForCellEdit / PreviewTextInput组合。

2
“不管价值如何,这是我解决它的方法。该解决方案允许您在验证输入时指定各种选项,允许使用字符串格式(例如数据网格中的“$15.00”)等等。”
Binding类本身提供的空值和字符串格式化不足以正确处理可编辑单元格,因此该类对此进行了覆盖。它使用另一个我已经使用了很长时间的类:TextBoxInputBehavior,这对我来说是非常宝贵的资产,最初来自WPF - TextBox Input Behavior博客文章,尽管此处的版本似乎更旧(但经过充分测试)。所以我的做法就是将我已经在TextBoxes上拥有的现有功能转移到了我的自定义列中,因此我在两个地方都有相同的行为。很整洁,不是吗?”
“以下是自定义列的代码:”
public class DataGridNumberColumn : DataGridTextColumn
{
    private TextBoxInputBehavior _behavior;

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        var element = base.GenerateElement(cell, dataItem);

        // A clever workaround the StringFormat issue with the Binding set to the 'Binding' property. If you use StringFormat it
        // will only work in edit mode if you changed the value, otherwise it will retain formatting when you enter editing.
        if (!string.IsNullOrEmpty(StringFormat))
        {
            BindingOperations.ClearBinding(element, TextBlock.TextProperty);
            BindingOperations.SetBinding(element, FrameworkElement.TagProperty, Binding);
            BindingOperations.SetBinding(element,
                TextBlock.TextProperty,
                new Binding
                {
                    Source = element,
                    Path = new PropertyPath("Tag"),
                    StringFormat = StringFormat
                });
        }

        return element;
    }

    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        if (!(editingElement is TextBox textBox))
            return null;

        var originalText = textBox.Text;

        _behavior = new TextBoxInputBehavior
        {
            IsNumeric = true,
            EmptyValue = EmptyValue,
            IsInteger = IsInteger
        };

        _behavior.Attach(textBox);

        textBox.Focus();

        if (editingEventArgs is TextCompositionEventArgs compositionArgs) // User has activated editing by already typing something
        {
            if (compositionArgs.Text == "\b") // Backspace, it should 'clear' the cell
            {
                textBox.Text = EmptyValue;
                textBox.SelectAll();
                return originalText;
            }

            if (_behavior.ValidateText(compositionArgs.Text))
            {
                textBox.Text = compositionArgs.Text;
                textBox.Select(textBox.Text.Length, 0);
                return originalText;
            }
        }

        if (!(editingEventArgs is MouseButtonEventArgs) || !PlaceCaretOnTextBox(textBox, Mouse.GetPosition(textBox)))
            textBox.SelectAll();

        return originalText;
    }

    private static bool PlaceCaretOnTextBox(TextBox textBox, Point position)
    {
        int characterIndexFromPoint = textBox.GetCharacterIndexFromPoint(position, false);
        if (characterIndexFromPoint < 0)
            return false;
        textBox.Select(characterIndexFromPoint, 0);
        return true;
    }

    protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue)
    {
        UnwireTextBox();
        base.CancelCellEdit(editingElement, uneditedValue);
    }

    protected override bool CommitCellEdit(FrameworkElement editingElement)
    {
        UnwireTextBox();
        return base.CommitCellEdit(editingElement);
    }

    private void UnwireTextBox() => _behavior.Detach();

    public static readonly DependencyProperty EmptyValueProperty = DependencyProperty.Register(
        nameof(EmptyValue),
        typeof(string),
        typeof(DataGridNumberColumn));

    public string EmptyValue
    {
        get => (string)GetValue(EmptyValueProperty);
        set => SetValue(EmptyValueProperty, value);
    }

    public static readonly DependencyProperty IsIntegerProperty = DependencyProperty.Register(
        nameof(IsInteger),
        typeof(bool),
        typeof(DataGridNumberColumn));

    public bool IsInteger
    {
        get => (bool)GetValue(IsIntegerProperty);
        set => SetValue(IsIntegerProperty, value);
    }

    public static readonly DependencyProperty StringFormatProperty = DependencyProperty.Register(
        nameof(StringFormat),
        typeof(string),
        typeof(DataGridNumberColumn));

    public string StringFormat
    {
        get => (string) GetValue(StringFormatProperty);
        set => SetValue(StringFormatProperty, value);
    }
}

我所做的是,我窥视了DataGridTextColumn的源代码,并以几乎相同的方式处理了TextBox的创建,此外我还将自定义行为附加到了TextBox上。
以下是我所附加的行为代码(这是一个可以用于任何TextBox的行为):
public class TextBoxInputBehavior : Behavior<TextBox>
{
    #region DependencyProperties

    public static readonly DependencyProperty RegularExpressionProperty = DependencyProperty.Register(
        nameof(RegularExpression), 
        typeof(string), 
        typeof(TextBoxInputBehavior), 
        new FrameworkPropertyMetadata(".*"));

    public string RegularExpression
    {
        get
        {
            if (IsInteger)
                return @"^[0-9\-]+$";
            if (IsNumeric)
                return @"^[0-9.\-]+$";
            return (string)GetValue(RegularExpressionProperty);
        }
        set { SetValue(RegularExpressionProperty, value); }
    }

    public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register(
        nameof(MaxLength), 
        typeof(int), 
        typeof(TextBoxInputBehavior),
        new FrameworkPropertyMetadata(int.MinValue));

    public int MaxLength
    {
        get { return (int)GetValue(MaxLengthProperty); }
        set { SetValue(MaxLengthProperty, value); }
    }

    public static readonly DependencyProperty EmptyValueProperty = DependencyProperty.Register(
        nameof(EmptyValue), 
        typeof(string), 
        typeof(TextBoxInputBehavior));

    public string EmptyValue
    {
        get { return (string)GetValue(EmptyValueProperty); }
        set { SetValue(EmptyValueProperty, value); }
    }

    public static readonly DependencyProperty IsNumericProperty = DependencyProperty.Register(
        nameof(IsNumeric), 
        typeof(bool), 
        typeof(TextBoxInputBehavior));

    public bool IsNumeric
    {
        get { return (bool)GetValue(IsNumericProperty); }
        set { SetValue(IsNumericProperty, value); }
    }

    public static readonly DependencyProperty IsIntegerProperty = DependencyProperty.Register(
        nameof(IsInteger),
        typeof(bool),
        typeof(TextBoxInputBehavior));

    public bool IsInteger
    {
        get { return (bool)GetValue(IsIntegerProperty); }
        set
        {
            if (value)
                SetValue(IsNumericProperty, true);
            SetValue(IsIntegerProperty, value);
        }
    }

    public static readonly DependencyProperty AllowSpaceProperty = DependencyProperty.Register(
        nameof(AllowSpace),
        typeof (bool),
        typeof (TextBoxInputBehavior));

    public bool AllowSpace
    {
        get { return (bool) GetValue(AllowSpaceProperty); }
        set { SetValue(AllowSpaceProperty, value); }
    }

    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewTextInput += PreviewTextInputHandler;
        AssociatedObject.PreviewKeyDown += PreviewKeyDownHandler;
        DataObject.AddPastingHandler(AssociatedObject, PastingHandler);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (AssociatedObject == null)
            return;

        AssociatedObject.PreviewTextInput -= PreviewTextInputHandler;
        AssociatedObject.PreviewKeyDown -= PreviewKeyDownHandler;
        DataObject.RemovePastingHandler(AssociatedObject, PastingHandler);
    }

    private void PreviewTextInputHandler(object sender, TextCompositionEventArgs e)
    {
        string text;
        if (AssociatedObject.Text.Length < AssociatedObject.CaretIndex)
            text = AssociatedObject.Text;
        else
            text = TreatSelectedText(out var remainingTextAfterRemoveSelection)
                ? remainingTextAfterRemoveSelection.Insert(AssociatedObject.SelectionStart, e.Text)
                : AssociatedObject.Text.Insert(AssociatedObject.CaretIndex, e.Text);
        e.Handled = !ValidateText(text);
    }

    private void PreviewKeyDownHandler(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Space)
            e.Handled = !AllowSpace;

        if (string.IsNullOrEmpty(EmptyValue))
            return;

        string text = null;

        // Handle the Backspace key
        if (e.Key == Key.Back)
        {
            if (!TreatSelectedText(out text))
            {
                if (AssociatedObject.SelectionStart > 0)
                    text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart - 1, 1);
            }
        }
        // Handle the Delete key
        else if (e.Key == Key.Delete)
        {
            // If text was selected, delete it
            if (!TreatSelectedText(out text) && AssociatedObject.Text.Length > AssociatedObject.SelectionStart)
            {
                // Otherwise delete next symbol
                text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, 1);
            }
        }

        if (text == string.Empty)
        {
            AssociatedObject.Text = EmptyValue;
            if (e.Key == Key.Back)
                AssociatedObject.SelectionStart++;
            e.Handled = true;
        }
    }

    private void PastingHandler(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(DataFormats.Text))
        {
            var text = Convert.ToString(e.DataObject.GetData(DataFormats.Text));

            if (!ValidateText(text))
                e.CancelCommand();
        }
        else
            e.CancelCommand();
    }

    public bool ValidateText(string text)
    {
        return new Regex(RegularExpression, RegexOptions.IgnoreCase).IsMatch(text) && (MaxLength == int.MinValue || text.Length <= MaxLength);
    }

    /// <summary>
    /// Handle text selection.
    /// </summary>
    /// <returns>true if the character was successfully removed; otherwise, false.</returns>
    private bool TreatSelectedText(out string text)
    {
        text = null;
        if (AssociatedObject.SelectionLength <= 0)
            return false;

        var length = AssociatedObject.Text.Length;
        if (AssociatedObject.SelectionStart >= length)
            return true;

        if (AssociatedObject.SelectionStart + AssociatedObject.SelectionLength >= length)
            AssociatedObject.SelectionLength = length - AssociatedObject.SelectionStart;

        text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, AssociatedObject.SelectionLength);
        return true;
    }
}

以上行为类的全部好评归功于blindmeis,我只是随着时间的推移进行了微调。在检查他的博客之后,我发现他有一个更新版本,你们可以去看看。很高兴发现我可以在DataGrid上使用他的行为!
这个解决方案非常好用,你可以通过鼠标/键盘正确地编辑单元格,正确地粘贴内容,使用任何绑定源更新触发器,使用任何字符串格式等——它只是有效。
这里是一个如何使用它的示例:
    <local:DataGridNumberColumn Header="Nullable Int Currency" IsInteger="True" Binding="{Binding IntegerNullable, TargetNullValue=''}" StringFormat="{}{0:C}" />

希望这对某人有所帮助。

2

为了扩展 @Omribitan 的答案,这里加入了一个数据 Paste 保护的解决方案:

public class NumericTextColumn : DataGridTextColumn
{
    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        var edit = editingElement as TextBox;
        edit.PreviewTextInput += Edit_PreviewTextInput;
        DataObject.AddPastingHandler(edit, OnPaste);
        return base.PrepareCellForEdit(editingElement, editingEventArgs);
    }

    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        var data = e.SourceDataObject.GetData(DataFormats.Text);
        if (!IsDataValid(data)) e.CancelCommand();
    }

    private void Edit_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        e.Handled = !IsDataValid(e.Text);
    }

    bool IsDataValid(object data)
    {
        try
        {
            Convert.ToInt32(data);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

0
我继承了Omris的方法,但希望在输入后能够删除单元格的值以便清除。
我的做法是覆盖CommitCellEdit方法,并将字符串设为null而非空白。在我的情况下,我还使用了decimal?。
public class DataGridNumericColumn : DataGridTextColumn
{
    protected override object PrepareCellForEdit(System.Windows.FrameworkElement editingElement, System.Windows.RoutedEventArgs editingEventArgs)
    
    {
        TextBox edit = editingElement as TextBox;
        edit.PreviewTextInput += OnPreviewTextInput;

        return base.PrepareCellForEdit(editingElement, editingEventArgs);
    }

    protected override bool CommitCellEdit(System.Windows.FrameworkElement editingElement)
    {
        TextBox tb = editingElement as TextBox;
        if (string.IsNullOrEmpty(tb.Text))
            tb.Text = null;
        
        return base.CommitCellEdit(editingElement);
    }
    void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    
    {
        try
        {
            Convert.ToDecimal(e.Text);
        }
        catch
        {
            // Show some kind of error message if you want

            // Set handled to true
            e.Handled = true;
        }
    }
}

0
这个类扩展了之前答案的功能,而不会像一些后来的答案那样变得过于复杂。
主要增加点如下:
  • TextBox_Loaded 处理用户直接在单元格上开始输入而不是先进入编辑模式的情况。
  • TextBox_LostFocus 清理用户的输入。 处理小数点前导0,小数点后的尾随0等。
其余部分与以前的答案非常相似,除了此处使用正则表达式检查数字输入是否有效(允许负数和小数)。
internal class CustomDataGridNumericColumn : DataGridTextColumn
{
    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        var textBox = editingElement as TextBox;
        textBox.PreviewTextInput += OnPreviewTextInput;
        DataObject.AddPastingHandler(textBox, OnPaste);
        textBox.LostFocus += TextBox_LostFocus;
        textBox.Loaded += TextBox_Loaded; // Add Loaded event handler to handle initial text
        return base.PrepareCellForEdit(editingElement, editingEventArgs);
    }

    private void TextBox_Loaded(object sender, RoutedEventArgs e)
    {
        var textBox = (TextBox)sender;
        textBox.Loaded -= TextBox_Loaded; // Remove the event handler after it's executed once
        if (!IsValidNumericInput(textBox.Text))
        {
            textBox.Text = string.Empty; // Clear the text if it's not valid initially
        }
    }

    private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        var textBox = (TextBox)sender;
        var proposedText = textBox.Text.Insert(textBox.CaretIndex, e.Text);
        if (!IsValidNumericInput(proposedText))
        {
            e.Handled = true;
        }
    }

    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        var data = e.SourceDataObject.GetData(DataFormats.Text);
        if (!IsValidNumericInput(data.ToString()))
        {
            e.CancelCommand();
        }
    }

    private void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        var textBox = (TextBox)sender;
        var text = textBox.Text;
        if (string.IsNullOrEmpty(text))
        {
            textBox.Text = null;
            return;
        }

        if (decimal.TryParse(text, out decimal value))
        {
            textBox.Text = value.ToString("G29"); // Removes trailing zeros for a decimal
        }
    }

    private static bool IsValidNumericInput(string input)
    {
        const string numericPattern = @"^-?[0-9]*(?:\.[0-9]*)?$";
        return Regex.IsMatch(input, numericPattern);
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接