實現 DataGridView 中的「日期選取器 (DateTimePicker)」

實現 DataGridView 中的「日期選取器 (DateTimePicker)」

在 Window Form 中,DataGridView 控制項是一個還算蠻好用的工具,可以快速的顯示井然有序的資料。欄位可以使用按鍵、核取方塊、下拉選單、圖檔、超連結與文字方塊,足以應付很多的狀態了。

但總還是有些不足的地方,比如說某一個欄位是日期的格式,一般來說都是使用文字方塊來處理,若只是單純顯示那還沒問題,要是可以讓使用者編輯的時後,就有點麻煩了,要是使用者輸入了不正確的日期,有可能造成無法預期的錯誤產生。所以最好的方式就是透過 DateTimePicker 來讓使用者選取日期,如此一來使用者就不會輸入不正確的日期了。

但問題來了,在 DataGridView 的 Column 型態中並沒有 DateTimePicker 這個類型,這時可以想到的解決方式就是透過 CellValueChanged 的事件來判斷,但也要當 Cell 失去焦點後才會觸發。而且必需要寫一堆的判斷式來處理,比如輸入的格式有 yyyy/mm/dd, yyyy-mm-dd, yyyymmdd 或是月和日有1碼的情況,如:1/9/2014,這樣算是2014年1月9日,還是2014年9月1日呢。

因此基於種種無法判定的因素,只有朝 DateTimePicker 來處理才是又快又適當的方式。

 

以下就以範例來解說:

首先在 Form 上新增一個 DateGridView,在此就不做資料庫的連結,僅以 DataGridView 的新增資料功能來處裡。DataGridView 中安置了 7 個 Column,其中 Column1 與 Column6 是唯讀不允許變更的,Column2 是下拉選單填值,Column4 為一般的文字輸入欄位,Column5 為勾選方塊,Column3 與 Column7 則為日期欄位,但這邊須設定為文字輸入欄位。

而 Column3 的日期顯示格式為 yyyy.MM.dd,Column7 的日期顯示格式為 yyyy-MM-dd,這會設定在另一個地方,並不是去設定 Column 中 DefaultCellStyle 的 Format 喔!

 

 

再來將 DataGridView 的 EditMode 變更為 EditProgrammatically。改由程式控制是否要進編輯模式,當然這個部份也可以使用預設值,但是如果不允許修改的欄位,就必需要設定為 ReadOnly 囉。

執行狀態如下圖:

這時後點選 Column1 或 Column6 都不會進入編輯狀態。點選了其他的欄位時,就會開啟編輯模式,這是透過 Cell Click 事件來處理的。


private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
    if ((e.RowIndex < 0) || (e.ColumnIndex < 0)) return;
    switch (dataGridView1.Columns[e.ColumnIndex].Name)
    {
        case "Column2":
        case "Column3":
        case "Column4":
        case "Column5":
        case "Column7":
            dataGridView1.BeginEdit(true);
            break;
        default:
            dataGridView1.EndEdit();
            break;
    }
}

當點選到行首或列首時,不做任何處理直接跳出。然後就依照觸發的 ColumnIndex 來判斷是在哪一個 Column 下,如果是 2,3,4,5,7 就啟用編輯,其他的結束編輯。到這邊都還是平淡無奇,還看不出來 DataTimePicker 在哪產生的。

 

接下來就是重頭戲了,透過 EditingControlShowing 事件,在 DataGridView 進入編輯模式要產生編輯的欄位後,將 DataTimePicker 以五鬼搬運加入到控制項中,並且將原本的文字方塊控制項給隱藏起來。

 


private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    if (dataGridView1.CurrentCell.ColumnIndex < 0) return;
    Control parentCTL = e.Control.Parent;

    if ((dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column3") || (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column7"))
    {
        DateTimePicker dtPicker = new DateTimePicker();
        dtPicker.Name = "dateTimePicker1";
        dtPicker.Size = dataGridView1.CurrentCell.Size;
        if (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column3")
        {
            dtPicker.CustomFormat = "yyyy.MM.dd";
        }
        else if (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column7")
        {
            dtPicker.CustomFormat = "yyyy-MM-dd";
        }
        
        dtPicker.Format = DateTimePickerFormat.Custom;
        dtPicker.Location = new Point(e.Control.Location.X - e.Control.Margin.Left < 0 ? 0 :e.Control.Location.X - e.Control.Margin.Left, e.Control.Location.Y - e.Control.Margin.Top < 0 ? 0:e.Control.Location.Y - e.Control.Margin.Top);

        if (e.Control.Text != "")
        {
            dtPicker.Value = DateTime.ParseExact(e.Control.Text, dtPicker.CustomFormat, null);
        }
        e.Control.Visible = false;

        foreach (Control tmpCTL in parentCTL.Controls)
        {
            if (tmpCTL.Name == dtPicker.Name) parentCTL.Controls.Remove(tmpCTL);
        }
        parentCTL.Controls.Add(dtPicker);

        dtPicker.ValueChanged += new EventHandler(dateTimePicker1_ValueChanged);
    }
    else
    {
        foreach (Control tmpCTL in parentCTL.Controls)
        {
            if (tmpCTL.Name == "dateTimePicker1") parentCTL.Controls.Remove(tmpCTL);
        }
    }
}

首先取出父系控制項到 parentCTL,接下來判斷目前的 Column 是否在所指定的 3 與 7 中。然後新增一個 DateTimePicker 物件為 dtPicker 並且命名為 dateTimePicker1,然後設定大小與目前的欄位大小一樣。

至於格式設定則依各人喜好,在這個例子中 Column3 是 yyyy.MM.dd、Column7 是 yyyy-MM-dd。然後指訂 DateTimePicker 的 Format 為自定模式(Custom)。

再來就調整 DateTimePicker 的顯示位置,透過原控制項來取得設定的位置。

然後將原資料代回到 DateTimePicker 中,總要記得設定前的資料吧。

接下來就是在父系控制項中尋找是否有同名的子控制項,(當然如果要再多判斷型別也是可以的!) 找到後將已存在的子控制項刪除。然後再將新的 DateTimePicker 控制項加入到父系控制項中。並且指定 ValueChanged 的事件。

 

如果目前點選的 Column 並沒有要做這樣客製行為,那麼就在父系控制項中將存在的 DateTimePicker 控制項全部刪除。

 

ValueChanged 事件


private void dateTimePicker1_ValueChanged(object sender, EventArgs e)
{
    if (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column3")
    {
        dataGridView1.CurrentCell.Value = ((DateTimePicker)sender).Value.ToString("yyyy.MM.dd");
    }
    else if (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Column7")
    {
        dataGridView1.CurrentCell.Value = ((DateTimePicker)sender).Value.ToString("yyyy-MM-dd");
    }
}

這個地方就只是判斷是哪一個欄位,要以什麼樣的格式傳回到原始觸發的欄位中。

 

執行畫面

 

如此一來,省去了一堆的判斷,都委由 DateTimePicker 來處理,同時還可以限定選取的最小日期與最大日期,這樣不是很好嗎!


程式是運氣與直覺堆砌而成的奇蹟。
若不具備這兩者,不可能以這樣的工時實現這樣的規格。
修改規格是對奇蹟吐槽的褻瀆行為。
而追加修改則是相信奇蹟還會重現的魯莽行動。