Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle navigation keys in TextBox inside DataGridView

We have a DataGridView with data in a form. To enable quick search, we added TextBox to DataGridView.Controls and highlight cells which contain text from TextBox.

However, there is an issue. DataGridView consumes the Left arrow , Right arrow , Home and End (with or without Shift) keys even if the cursor is in TextBox, and the user cannot change the caret position or select text from the keyboard.

TextBox generates a PreviewKeyDown event and nothing more happens.

Simplified code:

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));
    }
}

Type 123 in TextBox and then try the Right arrow, Left arrow, End, or Home. DataGridView change the selected cell, but the TextBox caret doesn't move.

TextBox works just fine if not inside a DataGridView (no problem at all when using the same method adding it into TreeView for example). TextBox acts similar to the Quick search Panel in the browser and has to be on top of the DataGridView. Adding a TextBox to a Form (or to be more specific, to a DataGridView parent) creates its own set of issues (tracking Location, Size, Visibility, ...) and is not acceptable.

What can be done to make sure that TextBox receive those keys and change the caret position or select text?

like image 712
ASh Avatar asked Mar 28 '17 13:03

ASh


2 Answers

TextBox works just fine if not inside DataGridView (no problem at all when using the same method adding it into TreeView for example)

Apparently the problem is in DataGridView. It's because DataGridView overrides the Control.ProcessKeyPreview method:

This method is called by a child control when the child control receives a keyboard message. The child control calls this method before generating any keyboard events for the message. If this method returns true, the child control considers the message processed and does not generate any keyboard events.

The DataGridView implementation does just that - it maintains zero or one child controls internally (EditingControl), and when there is no such control active, it handles many keys (navigation, tab, enter, escape, etc.) by returning true, thus preventing the child TextBox keyboard events generation. The return value is controlled by the ProcessDataGridViewKey method.

Since the method is virtual, you can replace the DataGridView with a custom derived class which overrides the aforementioned method and prevents the undesired behavior when neither the view nor the view active editor (if any) has the keyboard focus.

Something like this:

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);
    }
}

The above is just the half of the story and solves the cursor navigation and selection keys issue. However DataGridView intercepts another key message preprocessing infrastructure method - Control.ProcessDialogKey and handles Tab, Esc, Return, etc. keys there. So in order to prevent that, the method has to be overridden as well and redirected to the parent of the data grid view. The later needs a little reflection trickery to call a protected method, but using one time compiled delegate at least avoids the performance hit.

With that addition, the final custom class would be like this:

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));
}
like image 106
Ivan Stoev Avatar answered Sep 28 '22 01:09

Ivan Stoev


You can try this.

I created my own textbox and overrode method 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);
        }
    }
}

And then you can call:

var txt = new MyTextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };
like image 20
vito Avatar answered Sep 28 '22 01:09

vito