本篇介绍如何实现DataGridView控件的列标题可编辑效果,相比上一个例子增加了接口实现和自定义事件,也提供了一种间接解决问题的思路。但示例中无法用代码操作DataGridView控件的滚动条,不知哪位大侠知道方法?
在上一篇中介绍了用Label控件模拟网页链接的组件,实现原理只是简单的将Label控件的事件进行了处理。本篇中介绍的DataGridView列标题可编辑组件在对DataGridView控件的事件进行处理的同时,加入了更多的技巧。
首先介绍本示例要实现的效果。WinForm中的DataGridView控件只能对单元格进行编辑,但有时候需要对列标题进行编辑,即自定义列标题。本组件就是实现列标题编辑的功能,双击列标题即可进行编辑,支持键盘左右键移动编辑单元格。编辑效果如下图。(注:双击列标题对某些数据源会执行排序操作,如果需要避免,可以自行修改为通过右键菜单选择开始编辑。)
上面介绍了需要实现什么效果,但DataGridView的列标题是不提供编辑的,那如何实现编辑呢?这里用了一个RichTextBox控件去模拟编辑状态,将RichTextBox控件覆盖到需要编辑的列标题上方,看起来就像是对列标题进行编辑一样。这个例子就比上一个稍微复杂一点,不仅仅是处理几个简单的事件了。下面就介绍实现的过程。
首先新建一个项目,选择项目类型为类库,输入项目名称DataGridViewColumnHeaderEditor,然后添加组件DataGridViewColumnHeaderEditor。具体的操作步骤在上一篇已经介绍过了,就不详细阐述。
和上一篇中介绍的组件一样,首先必须给组件指定一个操作目标。这里要操作的是DataGridView,所以添加一个DataGridView类型的属性,另外添加了一个属性指示是否允许编辑,代码如下:上面提到了用一个RichTextBox控件去模拟编辑效果,那么这里就需要添加一个RichTextBox控件。切换到组件的设计视图,从工具箱中拖动一个RichTextBox控件到组件中。设置RichTextBox控件的相关属性,将MultiLine、TabStop和Visible均设置为False。
启用编辑的操作是双击列标题,那么就需要对DataGridView控件的列标题双击事件进行处理。上一篇中介绍了窗体背后的故事,是通过设置属性的时候绑定事件处理程序的,也提到了用另一种方法实现,那就是ISupportInitialize接口。本例就采用这种方法来把控件的事件和对应的事件处理程序绑定。
Code
private DataGridView m_TargetControl = null;
/// <summary>
/// 要编辑的目标 DataGridView 控件
/// </summary>
[Description("要编辑的目标 DataGridView 控件。")]
public DataGridView TargetControl
{
get { return m_TargetControl; }
set { m_TargetControl = value; }
}
private bool m_EnableEdit = true;
/// <summary>
/// 是否允许编辑
/// </summary>
[Description("是否允许编辑。"), DefaultValue(true)]
public bool EnableEdit
{
get { return m_EnableEdit; }
set { m_EnableEdit = value; }
}
下面介绍一下ISupportInitialize接口。参考MSDN中的介绍,ISupportInitialize接口:指定该对象支持对批初始化的简单的事务处理通知。该接口包含两个方法BeginInit和EndInit,在该接口的备注中有如下说明:
ISupportInitialize 允许控件为多组属性而优化。因此,可以在设计时初始化相互依赖的属性或批设置多个属性。
调用 BeginInit 方法用信号通知对象初始化即将开始。调用 EndInit 方法用信号通知初始化已完成。
下面做个试验,往一个窗体上放置一个DataGridView控件,回到窗体的设计器代码Designer.cs中,可以看到在InitializeComponent方法中有如下代码:
Code
this.dataGridView1 = new System.Windows.Forms.DataGridView();
//省略其他代码
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
//…
//
//dataGridView1
//
//省略设置dataGridView1属性的代码
//…
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
//…
可以看出这个接口的方法是在窗体初始化的时候被调用的。如果需要对控件或者组件进行初始化,可以在BeginInit中进行,如果需要在初始化完成之后进行其他相关的操作,可以在EndInit中进行。本例把绑定事件与处理方法的操作放在了EndInit中,代码如下:
Code
#region ISupportInitialize 成员
public void BeginInit()
{
//无操作
}
public void EndInit()
{
if (m_TargetControl != null)
{
this.m_TargetControl.Parent.Controls.Add(this.rtbTitle);
this.rtbTitle.BringToFront();//将RichTextBox控件前置
this.ReloadSortedColumnList();//重新加载列对象列表
m_TargetControl.ColumnHeaderMouseDoubleClick += new DataGridViewCellMouseEventHandler(TargetControl_ColumnHeaderMouseDoubleClick);
m_TargetControl.ColumnDisplayIndexChanged += new DataGridViewColumnEventHandler(TargetControl_ColumnDisplayIndexChanged);
m_TargetControl.ColumnRemoved += new DataGridViewColumnEventHandler(TargetControl_ColumnRemoved);
m_TargetControl.ColumnAdded += new DataGridViewColumnEventHandler(TargetControl_ColumnAdded);
m_TargetControl.Scroll += new ScrollEventHandler(TargetControl_Scroll);
}
}
#endregion ISupportInitialize 成员
在EndInit方法中,首先判断目标控件是否为空,然后将RichTextBox添加到目标控件的父控件中并前置,这样才能在编辑的时候覆盖在DataGridView控件上。之后是ReloadSortedColumnList方法,该方法获取列对象列表,并且按照显示序号进行排序。因为DataGridViewColumn有两个序号,一个是Index,是在DataGridView控件的Columns中的序号,另一个是DisplayIndex,是实际显示的序号。用户可能调整列的顺序,有些列可能是隐藏的,如果从DataGridView控件的Columns属性中按Index操作可能发生错误。比如在DataGridView控件的Columns中Index为2的列可能DisplayIndex为0。用键盘操作编辑框从Index为3且DisplayIndex为3的列向左移动的时候,跳到序号为2的列上,显示给用户就是从第3列跳到第0列。最后就是将DataGridView控件的事件绑定到相关的事件处理方法上。以下就是事件处理方法的代码:
Code
#region 目标控件的事件处理
void TargetControl_Scroll(object sender, ScrollEventArgs e)
{
//只在操作水平滚动条时进行处理
if (e.ScrollOrientation == ScrollOrientation.HorizontalScroll)
{
this.m_ScrollValue = e.NewValue;//记录滚动条位置
if (this.rtbTitle.Visible)
this.ShowHeaderEdit();//如果当前是编辑状态,则刷新显示编辑框的位置
}
}
void TargetControl_ColumnAdded(object sender, DataGridViewColumnEventArgs e)
{
this.ReloadSortedColumnList();//重新加载列对象列表
}
void TargetControl_ColumnRemoved(object sender, DataGridViewColumnEventArgs e)
{
this.ReloadSortedColumnList();
}
void TargetControl_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
{
this.ReloadSortedColumnList();
}
//双击列标题显示编辑状态
void TargetControl_ColumnHeaderMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)
{
this.m_SelectedColumnIndex = this.m_TargetControl.Columns[e.ColumnIndex].DisplayIndex;
if (this.m_EnableEdit)
this.ShowHeaderEdit();//显示编辑状态
}
#endregion 目标控件的事件处理
从代码里可以看到,列增减以及序号改变都需要重新加载列表排序,双击则显示编辑效果,另一个就是DataGridView控件的滚动条操作。为什么需要对滚动条事件进行处理?因为这里是用一个RichTextBox控件模拟的编辑状态,如果不处理,列标题的位置变了,编辑框却还定在那里,就会错位了。而且列的坐标会随着滚动条操作发生改变,如果不记录滚动条的位置,在双击列标题时就会得到一个列标题的内部相对坐标,但RichTextBox是按照外部绝对坐标显示的,这样也会发生错位。而DataGridView控件没法直接获取滚动条的位移,所以只好在滚动条事件中记录滚动条的位移了。(注意:在其他带滚动条的控件中确定子控件的位置也需要考虑滚动条。)
绑定好DataGridView控件的事件处理方法之后,就是对RichTextBox控件的操作了。编辑框需要处理键盘操作以实现移动和完成编辑的操作,对应方法是rtbTitle_KeyDown。编辑框失去焦点时也要作为编辑完成的动作,对应方法是rtbTtile_Leave方法。ShowHeaderEdit方法是显示编辑效果的,主要是确定编辑框的位置和大小,把对应列的标题显示到编辑框中。这里不允许输入空的标题,如果需要,可以根据实际情况修改代码。另外其中加入了一些事件,用来更加灵活控制编辑操作。关于事件,稍后再详细介绍。
Code
#region 文本框相关方法
/// <summary>
/// 文本框的键盘处理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void rtbTitle_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Enter://回车结束编辑
this.m_TargetControl.Focus();//让编辑框失去焦点而结束编辑并隐藏,下同
e.Handled = true;//必须设置为true,否则会有烦人的系统提示音,下同
break;
case Keys.Right://向右
//判断光标是否移动到当前编辑字符串的末尾,光标移到末尾才移动编辑框
if (this.rtbTitle.SelectionStart >= this.rtbTitle.Text.Length)
{
//判断当前编辑列是否是最后一列
if (this.m_SelectedColumnIndex < this.m_TargetControl.Columns.Count - 1)
{
e.Handled = true;
this.m_TargetControl.Focus();
//获取下一个可见列的序号并设置为当前选中列序号
this.m_SelectedColumnIndex = this.GetNextVisibleColumnIndex(this.m_SelectedColumnIndex);
this.ShowHeaderEdit();//根据选中列显示编辑框
}
}
break;
case Keys.Left://向左
//判断光标是否到达当前编辑字符串的最前,光标移动到最前才移动编辑框
if (this.rtbTitle.SelectionStart == 0)
{
//判断当前编辑列是否是第0列
if (this.m_SelectedColumnIndex > 0)
{
e.Handled = true;
this.m_TargetControl.Focus();
this.m_SelectedColumnIndex = this.GetPreVisibleColumnIndex(this.m_SelectedColumnIndex);
this.ShowHeaderEdit();
}
}
break;
default:
break;
}
}
/// <summary>
/// 文本框失去焦点
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void rtbTitle_Leave(object sender, EventArgs e)
{
DataGridViewColumn myColumn = this.m_SortedColumnList[this.m_SelectedColumnIndex];
//定义事件参数
ColumnHeaderEditEventArgs myArgs = new ColumnHeaderEditEventArgs(myColumn, this.rtbTitle.Text.Trim());
if (this.EndingEdit != null)
{
this.EndingEdit(this, myArgs);//引发事件
if (myArgs.Cancel)//如果取消标志为true
{
this.rtbTitle.Focus();//保持编辑状态
return;
}
}
this.rtbTitle.Visible = false;
if (myArgs.NewHeaderText.Length > 0)//不允许用空字符串作为标题
{
if (myColumn.HeaderText != myArgs.NewHeaderText)
{
//用事件参数里面的新标题,因为在事件处理程序里面可能修改新标题
myColumn.HeaderText = myArgs.NewHeaderText;
}
}
if (this.EndEdit != null)
this.EndEdit(this, myArgs);//引发事件
}
/// <summary>
/// 显示标题编辑效果
/// </summary>
private void ShowHeaderEdit()
{
if (this.BeginEdit != null)
{
ColumnHeaderEditEventArgs myArgs = new ColumnHeaderEditEventArgs(this.m_SortedColumnList[this.m_SelectedColumnIndex], "");
BeginEdit(this, myArgs);
if (myArgs.Cancel)
return;
}
int intColumnRelativeLeft = 0;
//第一列左边距,需要判断是否显示行标题
int intFirstColumnLeft = (this.m_TargetControl.RowHeadersVisible ? this.m_TargetControl.RowHeadersWidth + 1 : 1);
int intTargetX = this.m_TargetControl.Location.X, intTargetY = this.m_TargetControl.Location.Y, intTargetWidth = this.m_TargetControl.Width;
intColumnRelativeLeft = GetColumnRelativeLeft(this.m_SelectedColumnIndex);
if (intColumnRelativeLeft < this.m_ScrollValue)
{
this.rtbTitle.Location = new Point(intTargetX + intFirstColumnLeft, intTargetY + 1);
if (intColumnRelativeLeft + this.m_SortedColumnList[this.m_SelectedColumnIndex].Width > this.m_ScrollValue)
this.rtbTitle.Width = intColumnRelativeLeft + this.m_SortedColumnList[this.m_SelectedColumnIndex].Width - this.m_ScrollValue;
else
this.rtbTitle.Width = 0;
}
else
{
this.rtbTitle.Location = new Point(intColumnRelativeLeft + intTargetX - this.m_ScrollValue + intFirstColumnLeft, intTargetY + 1);
if (this.rtbTitle.Location.X + this.rtbTitle.Width > intTargetX + intTargetWidth)
{
int intWidth = intTargetX + intTargetWidth - this.rtbTitle.Location.X;
this.rtbTitle.Width = (intWidth >= 0 ? intWidth : 0);
}
else
this.rtbTitle.Width = this.m_SortedColumnList[this.m_SelectedColumnIndex].Width;
}
this.rtbTitle.Height = this.m_TargetControl.ColumnHeadersHeight - 1;
this.rtbTitle.Text = this.m_SortedColumnList[this.m_SelectedColumnIndex].HeaderText;
this.rtbTitle.SelectAll();
this.rtbTitle.Visible = true;
this.rtbTitle.Focus();
}
#endregion 文本框相关方法
在上面对编辑框操作的相关方法中,又涉及到了对列对象的一些操作,比如获取相对坐标,左右移动时获取邻近显示的列。下面就是这些方法的代码。
Code
#region DataGridView列相关方法
/// <summary>
/// 重新加载列对象的列表
/// </summary>
private void ReloadSortedColumnList()
{
this.m_SortedColumnList.Clear();
foreach (DataGridViewColumn column in this.m_TargetControl.Columns)
{
this.m_SortedColumnList.Add(column.DisplayIndex, column);
}
}
/// <summary>
/// 获取列的相对左边距
/// </summary>
/// <param name="ColumnIndex">列序号</param>
/// <returns>列的左边距</returns>
private int GetColumnRelativeLeft(int ColumnIndex)
{
int intLeft = 0;
DataGridViewColumn Column = null;
for (int intIndex = 0; intIndex < ColumnIndex; intIndex++)
{
if (this.m_SortedColumnList.ContainsKey(intIndex))
{
Column = this.m_SortedColumnList[intIndex];
if (Column.Visible)
intLeft += Column.Width + Column.DividerWidth;
}
}
return intLeft;
}
/// <summary>
/// 获取上一个可见列的序号
/// </summary>
/// <param name="CurrentIndex">当前列序号</param>
/// <returns></returns>
private int GetPreVisibleColumnIndex(int CurrentIndex)
{
int intPreIndex = 0;
for (int intIndex = CurrentIndex - 1; intIndex >= 0; intIndex--)
{
if (this.m_SortedColumnList.ContainsKey(intIndex) && this.m_SortedColumnList[intIndex].Visible)
{
intPreIndex = intIndex;
break;
}
}
return intPreIndex;
}
/// <summary>
/// 获取下一个可见列的序号
/// </summary>
/// <param name="CurrentIndex">当前列序号</param>
/// <returns></returns>
private int GetNextVisibleColumnIndex(int CurrentIndex)
{
int intNextIndex = CurrentIndex;
for (int intIndex = CurrentIndex + 1; intIndex <= this.m_SortedColumnList.Keys[this.m_SortedColumnList.Count - 1]; intIndex++)
{
if (this.m_SortedColumnList.ContainsKey(intIndex) && this.m_SortedColumnList[intIndex].Visible)
{
intNextIndex = intIndex;
break;
}
}
return intNextIndex;
}
#endregion DataGridView列相关方法
以上方法都比较简单,不再详细解释。下面就介绍事件。在类中声明了三个事件,代码如下:
Code
#region 事件声明
/// <summary>
/// 开始编辑,可取消编辑
/// </summary>
[Description("在开始编辑列标题时发生的事件,可取消编辑。")]
public event ColumnHeaderEditEventHandler BeginEdit;
/// <summary>
/// 准备结束编辑,可取消
/// </summary>
[Description("在即将结束编辑时发生的事件,可取消。")]
public event ColumnHeaderEditEventHandler EndingEdit;
/// <summary>
/// 结束编辑
/// </summary>
[Description("在编辑结束后发生的事件。")]
public event ColumnHeaderEditEventHandler EndEdit;
#endregion 事件声明
BeginEdit事件是在编辑开始的时候发生的,如果有一些列不允许编辑,则可以在该事件处理方法中捕获并取消。
EndingEdition事件是在编辑即将结束的时候发生的,如果用户输入的列标题不合理,可以取消结束编辑,强制用户继续编辑。
EndEdit事件是在编辑结束后发生的,通知外部被编辑的列的相关信息。
这些事件的类型都是ColumnHeaderEditEventHandler,如下是该事件委托的定义以及事件参数的定义。如果对事件和委托不是很了解,请先查阅相关资料,这里不作详细阐述。
小技巧——事件委托和事件参数相关
通常事件委托的名称定义为事件相关名称+EventHandler,比如MouseEventHandler,PaintEventHandler,CancelEventHandler,FormClosedEventHandler。事件委托一般包含两个参数格式,定义格式如public delegate void MyEventHandler(object sender, MyEventArgs e)。而事件参数一般定义为事件相关名称+EventArgs,比如DragEventArgs,ListChangedEventArgs,NavigateEventArgs,MouseEventArgs。事件参数中的属性一般是不可修改的,即没有set段,是通过构造函数指定的。如果需要通过参数影响事件的行为,则会存在set段。
Code
/// <summary>
/// 列标题编辑事件委托
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void ColumnHeaderEditEventHandler(object sender, ColumnHeaderEditEventArgs e);
/// <summary>
/// 列标题编辑事件参数
/// </summary>
public class ColumnHeaderEditEventArgs : EventArgs
{
private bool m_Cancel = false;
/// <summary>
/// 取消编辑
/// </summary>
public bool Cancel
{
get { return m_Cancel; }
set { m_Cancel = value; }
}
private string m_NewHeaderText = "";
/// <summary>
/// 新的列标题
/// </summary>
public string NewHeaderText
{
get { return m_NewHeaderText; }
set
{
if (!(string.IsNullOrEmpty(value) || value.Trim().Length == 0))
m_NewHeaderText = value;
}
}
private DataGridViewColumn m_Column = null;
/// <summary>
/// 目标列
/// </summary>
public DataGridViewColumn Column
{
get { return m_Column; }
}
public ColumnHeaderEditEventArgs(DataGridViewColumn Column, string NewHeaderText)
{
if (Column == null)
throw new ArgumentNullException("Column", "要编辑的列不允许为空。");
this.m_Column = Column;
if (string.IsNullOrEmpty(NewHeaderText) || NewHeaderText.Trim().Length == 0)
NewHeaderText = Column.HeaderText;
this.m_NewHeaderText = NewHeaderText.Trim();
}
}//class ColumnHeaderEditEventArgs
小技巧——引发事件的方法
如果在一个类中存在多个地方引发同一个事件,可以考虑用一个方法代替。因为引发事件之前都必须判断该事件委托是否为空,否则直接引发事件可能出错。示例如下:
Code
//直接引发事件
if(myEvent != null)
myEvent(sender,myEventArgs);
//间接引发事件
//一般sender是类实例本身,所以通常生理sender参数
//如果MyEventArgs的构造参数不多,或者操作比较复杂,可以通过参数传入,在这个方法中再实例化
private void OnSomeEvent(object sender, MyEventArgs e)
{
if(myEvent != null)
myEvent(sender, e);
}
至此,组件的编码就完成了,类图如下。
小技巧——查看类图的方法
对项目添加新项,选择“类关系图”,然后把需要查看的类从解决方案管理器中拖动到类图即可。也可以在类图中直接添加新项,用类图去设计类和其他对象。
编译一下。然后添加测试的Windows应用程序项目,在窗体上放置一个DataGridView控件,对该控件添加几列。然后拖动DataGridViewColumnHeaderEditor组件到窗体上,设置组件的TargetControl属性为之前添加的DataGridView控件。按F5运行,双击列标题即可编辑,回车或者用鼠标点击别处可完成编辑,也可以通过键盘左右方向键移动编辑框。
本例相比上一个例子,稍微复杂一点,添加了接口实现和自定义事件。这里也提供了一种间接解决问题的思路,虽然DataGridView控件本身不支持编辑列标题,但可以用一个RichTextBox去模拟编辑状态。通过这个例子,可以引申出其他解决方案,比如对树节点用下拉框编辑,用ListView或者DataGridView让下拉框显示多列等等。具体的应用就要靠自己实践了,希望这篇文章能给您带来收获。
另外在这个示例中有个问题没解决,那就是滚动条的操作,当编辑框移动到可视范围之外时,需要手动操作滚动条才能让编辑框显示。但是DataGridView不提供操作滚动条的方法,其他带滚动条的控件也不提供操作滚动条的方法。不知有没有哪位大侠知道方法?
代码下载:https://files.cnblogs.com/conexpress/TestDataGridViewColumnHeaderEditor.zip
Author:Alex Leo
Email:conexpress@qq.com
Blog:http://conexpress.cnblogs.com/