在上一篇中介紹了用Label控件模擬網頁鏈接的組件,實現原理只是簡單的將Label控件的事件進行了 處理。本篇中介紹的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控件沒法直接獲 取滾動條的位移,所以只好在滾動條事件中記錄滾動條的位移了。(注意:在其他帶滾動條的控件中確定 子控件的位置也需要考慮滾動條。)
綁定好DataGridView控件的事件處理方法之後,就是對RichTextBox控件的操作了。編輯框需要處理鍵 盤操作以實現移動和完成編輯的操作,對應方法是rtbTitle_KeyDown。編輯框失去焦點時也要作為編輯完 成的動作,對應方法是rtbTtile_Leave方法。ShowHeaderEdit方法是顯示編輯效果的,主要是確定編輯框 的位置和大小,把對應列的標題顯示到編輯框中。這裡不允許輸入空的標題,如果需要,可以根據實際情 況修改代碼。另外其中加入了一些事件,用來更加靈活控制編輯操作。關於事件,稍後再詳細介紹。
#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 文本框相關方法
在上面對編輯框操作的相關方法中,又涉及到了對列對象的一些操作,比如獲取相對坐標,左右移動 時獲取鄰近顯示的列。下面就是這些方法的代碼。
#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列相關方法
以上方法都比較簡單,不再詳細解釋。下面就介紹事件。在類中聲明了三個事件,代碼如下:
#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段。
/// <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
小技巧——引發事件的方法
如果在一個類中存在多個地方引發同一個事件,可以考慮用一個方法代替。因為引發事件之前都必須 判斷該事件委托是否為空,否則直接引發事件可能出錯。示例如下:
//直接引發事件
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不提供操作滾動條的方法,其他帶滾動條的控件也不 提供操作滾動條的方法。不知有沒有哪位大俠知道方法?
本文配套源碼:http://www.bianceng.net/dotnet/201212/729.htm