.NET组件控件实例编程系列——3.DataGridView列标题可编辑组件

在上一篇中介绍了用Label控件模拟网页链接的组件,实现原理只是简单的将Label控件的事件进行了处理。本篇中介绍的DataGridView列标题可编辑组件在对DataGridView控件的事件进行处理的同时,加入了更多的技巧。

首先介绍本示例要实现的效果。WinForm中的DataGridView控件只能对单元格进行编辑,但有时候需要对列标题进行编辑,即自定义列标题。本组件就是实现列标题编辑的功能,双击列标题即可进行编辑,支持键盘左右键移动编辑单元格。编辑效果如下图。(注:双击列标题对某些数据源会执行排序操作,如果需要避免,可以自行修改为通过右键菜单选择开始编辑。)

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

 

至此,组件的编码就完成了,类图如下。
DataGridViewColumnHeaderEditor类图
小技巧——查看类图的方法
对项目添加新项,选择“类关系图”,然后把需要查看的类从解决方案管理器中拖动到类图即可。也可以在类图中直接添加新项,用类图去设计类和其他对象。

编译一下。然后添加测试的Windows应用程序项目,在窗体上放置一个DataGridView控件,对该控件添加几列。然后拖动DataGridViewColumnHeaderEditor组件到窗体上,设置组件的TargetControl属性为之前添加的DataGridView控件。按F5运行,双击列标题即可编辑,回车或者用鼠标点击别处可完成编辑,也可以通过键盘左右方向键移动编辑框。

本例相比上一个例子,稍微复杂一点,添加了接口实现和自定义事件。这里也提供了一种间接解决问题的思路,虽然DataGridView控件本身不支持编辑列标题,但可以用一个RichTextBox去模拟编辑状态。通过这个例子,可以引申出其他解决方案,比如对树节点用下拉框编辑,用ListView或者DataGridView让下拉框显示多列等等。具体的应用就要靠自己实践了,希望这篇文章能给您带来收获。

另外在这个示例中有个问题没解决,那就是滚动条的操作,当编辑框移动到可视范围之外时,需要手动操作滚动条才能让编辑框显示。但是DataGridView不提供操作滚动条的方法,其他带滚动条的控件也不提供操作滚动条的方法。不知有没有哪位大侠知道方法?

代码下载:http://files.cnblogs.com/conexpress/TestDataGridViewColumnHeaderEditor.zip

转载于:https://www.cnblogs.com/conexpress/archive/2009/03/06/Component_Control_03.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值