程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET組件控件實例編程系列——3.DataGridView列標題可編輯組件

.NET組件控件實例編程系列——3.DataGridView列標題可編輯組件

編輯:關於.NET

在上一篇中介紹了用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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved