ASP.NET开发组件之DataGridView列标题可编辑组件
本篇中介绍的DataGridView列标题可编辑组件在对DataGridView控件的事件进行处理的同时,加入了更多的技巧。
首先介绍本示例要实现的效果。WinForm中的DataGridView控件只能对单元格进行编辑,但有时候需要对列标题进行编辑,即自定义列标题。本组件就是实现列标题编辑的功能,双击列标题即可进行编辑,支持键盘左右键移动编辑单元格。编辑效果如下图。(注:双击列标题对某些数据源会执行排序操作,如果需要避免,可以自行修改为通过右键菜单选择开始编辑。)
上面介绍了需要实现什么效果,但DataGridView的列标题是不提供编辑的,那如何实现编辑呢?这里用了一个RichTextBox控件去模拟编辑状态,将RichTextBox控件覆盖到需要编辑的列标题上方,看起来就像是对列标题进行编辑一样。这个例子就比上一个稍微复杂一点,不仅仅是处理几个简单的事件了。下面就介绍实现的过程。
首先新建一个项目,选择项目类型为类库,输入项目名称DataGridViewColumnHeaderEditor,然后添加组件DataGridViewColumnHeaderEditor。具体的操作步骤在上一篇已经介绍过了,就不详细阐述。
和上一篇中介绍的组件一样,首先必须给组件指定一个操作目标。这里要操作的是DataGridView,所以添加一个DataGridView类型的属性,另外添加了一个属性指示是否允许编辑,代码如下:上面提到了用一个RichTextBox控件去模拟编辑效果,那么这里就需要添加一个RichTextBox控件。切换到组件的设计视图,从工具箱中拖动一个RichTextBox控件到组件中。设置RichTextBox控件的相关属性,将MultiLine、TabStop和Visible均设置为False。
启用编辑的操作是双击列标题,那么就需要对DataGridView控件的列标题双击事件进行处理。上一篇中介绍了窗体背后的故事,是通过设置属性的时候绑定事件处理程序的,也提到了用另一种方法实现,那就是ISupportInitialize接口。本例就采用这种方法来把控件的事件和对应的事件处理程序绑定。


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方法中有如下代码:


this.dataGridView1 = new System.Windows.Forms.DataGridView();
//省略其他代码

((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
//…
//
//dataGridView1
//
//省略设置dataGridView1属性的代码
//…
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
//…
可以看出这个接口的方法是在窗体初始化的时候被调用的。如果需要对控件或者组件进行初始化,可以在BeginInit中进行,如果需要在初始化完成之后进行其他相关的操作,可以在EndInit中进行。本例把绑定事件与处理方法的操作放在了EndInit中,代码如下:


#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控件的事件绑定到相关的事件处理方法上。以下就是事件处理方法的代码:


#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控件没法直接获取滚动条的位移,所以只好在滚动条事件中记录滚动条的位移了。(注意:在其他带滚动条的控件中确定子控件的位置也需要考虑滚动条。)