【问题标题】:Handle navigation keys in TextBox inside DataGridView处理 DataGridView 内 TextBox 中的导航键
【发布时间】:2017-03-28 13:37:08
【问题描述】:

我们有一个DataGridView,其中包含表单中的数据。为了实现快速搜索,我们将 TextBox 添加到 DataGridView.Controls 并突出显示包含 TextBox 中的文本的单元格。

但是,有一个问题。 DataGridView 使用左箭头 ←、右箭头 →、Home 和 End(带或不带 Shift ) 键即使光标在 TextBox 中,用户也无法更改插入符号的位置或从键盘选择文本。

TextBox 生成一个PreviewKeyDown 事件,然后没有任何反应。

简化代码:

public partial class TestForm : Form
{
    public TestForm()
    {
        InitializeComponent();
        Width = 400;
        Height = 400;

        var txt = new TextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };
        var dgv = new DataGridView
        {
            Dock = DockStyle.Fill,
            ColumnCount = 3,
            RowCount = 5
        };
        dgv.Controls.Add(txt);
        Controls.Add(dgv);

        dgv.PreviewKeyDown += DgvOnPreviewKeyDown;
        dgv.KeyDown += DgvOnKeyDown;

        txt.PreviewKeyDown += TxtOnPreviewKeyDown;
        txt.KeyDown += TxtOnKeyDown;
    }

    private void DgvOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
    {
        Debug.WriteLine(String.Format("Dgv Key Preview {0}", e.KeyCode));
        e.IsInputKey = true;
    }

    private void DgvOnKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine(String.Format("Dgv Key {0}", e.KeyCode));
    }

    private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
    {
        Debug.WriteLine(String.Format("Txt Key Preview {0}", e.KeyCode));
    }

    private void TxtOnKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine(String.Format("Txt Key {0}", e.KeyCode));
    }
}

在 TextBox 中键入 123,然后尝试右箭头、左箭头、End 或 Home。 DataGridView 更改选定的单元格,但 TextBox 插入符号没有移动。

TextBox 如果不​​在 DataGridView 内也可以正常工作(例如,使用相同的方法将其添加到 TreeView 时完全没有问题)。 TextBox 的作用类似于浏览器中的快速搜索面板,并且必须位于 DataGridView 之上。将 TextBox 添加到 Form(或者更具体地说,添加到 DataGridView 父级)会创建自己的一组问题(跟踪位置、大小、可见性......)并且是不可接受的。

如何确保 TextBox 接收这些键并更改插入符号位置或选择文本?

【问题讨论】:

  • 如果文本框有焦点并且用户点击右键,是否应该移动插入符号位置并移动 DGV 中的选定列?
  • @David,主要关注的是 TextBox 的正确行为。当 TextBox 有焦点时,DGV 不必处理导航键(实际上,在这种情况下,DGV 不应该做任何事情)

标签: c# .net winforms datagridview


【解决方案1】:

TextBox 如果不​​在 DataGridView 内也可以正常工作(例如,使用相同的方法将其添加到 TreeView 时完全没有问题)

显然问题出在DataGridView。这是因为DataGridView 覆盖了Control.ProcessKeyPreview 方法:

当子控件收到键盘消息时,子控件调用此方法。子控件在为消息生成任何键盘事件之前调用此方法。如果此方法返回 true,则子控件认为消息已处理并且不生成任何键盘事件。

DataGridView 实现就是这样做的 - 它在内部维护零个或一个子控件 (EditingControl),当没有此类控件处于活动状态时,它会处理许多键(导航、制表符、回车、转义等)。 ) 通过返回true,从而防止子TextBox 键盘事件的产生。返回值由ProcessDataGridViewKey方法控制。

因为方法是virtual,你可以用一个自定义派生类替换DataGridView .

类似这样的:

public class CustomDataGridView : DataGridView
{
    bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
        (EditingControl == null || !EditingControl.ContainsFocus);

    protected override bool ProcessDataGridViewKey(KeyEventArgs e)
    {
        if (SuppressDataGridViewKeyProcessing) return false;
        return base.ProcessDataGridViewKey(e);
    }
}

以上只是故事的一半,解决了光标导航和选择键问题。然而,DataGridView 拦截了另一个关键消息预处理基础结构方法 - Control.ProcessDialogKey 并在那里处理 Tab、Esc、Return 等键。因此,为了防止这种情况,该方法也必须被覆盖并重定向到数据网格视图的父级。后者需要一点反射技巧来调用protected 方法,但使用一次编译委托至少可以避免性能损失。

添加后,最终的自定义类将如下所示:

public class CustomDataGridView : DataGridView
{
    bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
        (EditingControl == null || !EditingControl.ContainsFocus);

    protected override bool ProcessDataGridViewKey(KeyEventArgs e)
    {
        if (SuppressDataGridViewKeyProcessing) return false;
        return base.ProcessDataGridViewKey(e);
    }

    protected override bool ProcessDialogKey(Keys keyData)
    {
        if (SuppressDataGridViewKeyProcessing)
        {
            if (Parent != null) return DefaultProcessDialogKey(Parent, keyData);
            return false;
        }
        return base.ProcessDialogKey(keyData);
    }

    static readonly Func<Control, Keys, bool> DefaultProcessDialogKey =
        (Func<Control, Keys, bool>)Delegate.CreateDelegate(typeof(Func<Control, Keys, bool>),
        typeof(Control).GetMethod(nameof(ProcessDialogKey), BindingFlags.NonPublic | BindingFlags.Instance));
}

【讨论】:

  • 感谢您的调查和解决方案。修复ProcessDataGridViewKey 就足够了。处理选项卡触发选项卡导航到下一个控件,这是不可取的。非常不幸的是,该解决方案需要更换每个视图中的每个 DGV。我希望我能在你的回答和 vito 的回答之间分红
  • 不客气 :) 我猜 Tab 处理是任意的,但是如果没有 ProcessDialogKey 钩子,网格视图将执行转到下一个/上一个单元格并最终从网格视图中跳出它在第一个/最后一个单元格上。所以 IMO 它仍然必须被拦截,但最终以不同的方式处理。更换每个 DGV 肯定是不幸的,但从另一方面来说,它可以让您在一个地方播放和调整所需的行为,这应该值得付出努力。
  • 我认为,这是一个简单且最好的解决方案。但是很好,可以从多种解决方案中进行选择,这会更合适。
【解决方案2】:

你可以试试这个。

我创建了自己的文本框并覆盖了方法ProcessKeyMessage。

public class MyTextBox : TextBox
{
    private const int WM_KEYDOWN = 0x0100;
    private const int WM_SYSKEYDOWN = 0x0104;

    protected override bool ProcessKeyMessage(ref Message m)
    {
        if (m.Msg != WM_SYSKEYDOWN && m.Msg != WM_KEYDOWN)
        {
            return base.ProcessKeyMessage(ref m);
        }

        Keys keyData = (Keys)((int)m.WParam);
        switch (keyData)
        {
            case Keys.Left:
            case Keys.Right:
            case Keys.Home:
            case Keys.End:
            case Keys.ShiftKey:
                return base.ProcessKeyEventArgs(ref m);
            default:
                return base.ProcessKeyMessage(ref m);
        }
    }
}

然后你可以调用:

var txt = new MyTextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };

【讨论】:

  • 好的,它似乎有效。并且用其派生类替换 TextBox 是我们快速搜索功能中的本地更改,通过封装隐藏,无需外部更改。但我不清楚 - 为什么它有效? ProcessKeyMessage 的文档说“控件应该返回 true 以表明它已经处理了密钥”。但是这里 MyTextBox 返回false (未处理)然后文本选择以某种方式起作用?!它会触发 TextBox 处理吗?
  • 我仔细研究了 DataGridView 和 TextBox 如何处理键。当 datagridview 为空(没有列)时,文本框具有预期的行为。但是当 DGW 有列和一些行时,文本框不会响应一些键,如 home、end、left... 我的事情,这是因为 DGW 行有焦点。
  • 按键后,DGW先处理key,textbox再处理。当我将一些文本写入文本框时,DGW 不处理密钥。 DGW的'ProcessKeyPreview'的返回值为false。但是当我按下 'home' 或 'left arrow' 键时,DGW 通过 'ProcessDataGridViewKey' 处理该键并返回 true。
  • 但你是对的。我的代码中有错误。我不应该简单的返回值false。阅读文章Notes to Inheritors 后,我将代码更改为从调用“ProcessKeyEventArgs”返回值。
【解决方案3】:

尝试只将 TextBox 添加到主窗体而不是 DataGridView:

Controls.Add(txt);
Controls.Add(dgv);

txt.PreviewKeyDown += DgvOnPreviewKeyDown;
txt.KeyDown += DgvOnKeyDown;

txt.PreviewKeyDown += TxtOnPreviewKeyDown;
txt.KeyDown += TxtOnKeyDown;

【讨论】:

  • 感谢您的宝贵时间。如果不在 DataGridView 内,TextBox 工作得很好(例如,将它添加到 TreeView 时完全没有问题)。 TextBox 的作用类似于浏览器中的快速搜索面板,并且必须位于 DGV 之上。将 TextBox 添加到 Form(或者更具体地说,添加到 DGV 父级)会创建自己的一组问题(跟踪位置、大小、可见性......)
【解决方案4】:

这听起来有点像徒劳的练习。

通过将 TextBox 和 DataGridView 控件的行为封装到 UserControl 中以及一些处理事件的代码可能会更容易。

【讨论】:

  • 这样您将失去 DataGridView 的所有设计时功能。
【解决方案5】:

这是该问题的部分解决方案。 TextBox 仍然不接收本机的导航键输入,但我重现了正常的插入符号和选择行为。

PreviewKeyDownEventArgs 包含有关按下的键和修饰符 (Shift) 的信息。对于每个组合键,我为 TextBox 设置了一个新的 SelectionStart 和 SelectionLength。

private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
    TextBox txt = (TextBox)sender;
    if (e.KeyCode == Keys.Home)
    {
        int idx = txt.SelectionStart;

        txt.SelectionStart = 0;
        txt.SelectionLength = e.Shift ? idx : 0;
    }
    else if (e.KeyCode == Keys.End)
    {
        int idx = txt.SelectionStart;

        if (e.Shift)
            txt.SelectionLength = txt.TextLength - idx;
        else
        {
            txt.SelectionStart = txt.TextLength;
            txt.SelectionLength = 0;
        }
    }
    else if (e.KeyCode == Keys.Left)
    {
        if (e.Shift)
        {
            if (txt.SelectionStart > 0)
            {
                txt.SelectionStart--;
                txt.SelectionLength++;
            }
        }
        else
        {
            txt.SelectionStart = Math.Max(0, txt.SelectionStart - 1);
            txt.SelectionLength = 0;
        }
    }
    else if (e.KeyCode == Keys.Right)
    {
        if (e.Shift)
            txt.SelectionLength++;
        else
        {
            txt.SelectionStart = Math.Min(txt.TextLength, txt.SelectionStart + 1);
            txt.SelectionLength = 0;
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多