我们有一个表格中有数据的DataGridView
。为了实现快速搜索,我们将TextBox
添加到DataGridView.Controls
并突出显示包含TextBox文本的单元格。
但是有一个问题。 DataGridView消耗Left
arrow←,Right
arrow→,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
,然后尝试Right
箭头,Left
箭头,End
或Home
。 DataGridView更改选定的单元格但TextBox插入符号不移动。
如果没有DataGridView内部的TextBox工作得很好(例如,使用相同的方法将其添加到TreeView中时没有任何问题)。 TextBox与浏览器中的快速搜索面板类似,必须位于DGV之上。将TextBox添加到表单(或者更具体地说,添加到DGV父级)会创建自己的一组问题(跟踪位置,大小,可见性......),这是不可接受的。
可以做些什么来确保TextBox接收这些键并更改插入位置或选择文本?
你可以试试这个。我创建了自己的文本框并重写方法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 };
如果没有DataGridView内部的TextBox工作得很好(例如,使用相同的方法将其添加到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));
}
您是否尝试过将TextBox添加到主窗体而不是DGV?
Controls.Add(txt);
Controls.Add(dgv);
txt.PreviewKeyDown += DgvOnPreviewKeyDown;
txt.KeyDown += DgvOnKeyDown;
txt.PreviewKeyDown += TxtOnPreviewKeyDown;
txt.KeyDown += TxtOnKeyDown;
这是该问题的部分解决方案。 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;
}
}
}
听起来有点像徒劳无功,通过将TextBox和DataGridView控件放在UserControl中以及处理事件的小代码,可能更容易封装它们的行为。