星期五, 7月 08, 2016

[C#] DataGridView - CheckBox 全選

實務上有該需求,希望 DataGridViewCheckBoxColumn 的 Header,能有個 CheckBox 來做到全部選擇、全部取消的功能,找了些文章來參考,在這整理記錄一下

[C#] DataGridView - CheckBox 全選-1

MyDGVCheckBoxColumnHeaderCell.cs
  1. 建立 CheckBoxAll Event
  2. 在 Paint 內繪出 CheckBox 並把相關資訊紀錄在 Property
  3. OnMouseClick 中根據 Property 資訊,判斷使用者是否有點擊到 CheckBox 並觸發 CheckBoxAll Event
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace DGVCheckBoxAll
{
	public class MyDGVCheckBoxColumnHeaderCell : DataGridViewColumnHeaderCell
	{
		private Point _headerCellCheckBoxLocation;
		private Size _headerCellCheckBoxSize;
		private Point _headerCellLocation;

		public bool CheckBoxAllCheckedState { get; set; }

		public MyDGVCheckBoxColumnHeaderCell(bool defaultValue)
		{
			CheckBoxAllCheckedState = defaultValue;
		}

		protected override void Paint(
			Graphics graphics,
			Rectangle clipBounds,
			Rectangle cellBounds,
			int rowIndex,
			DataGridViewElementStates dataGridViewElementState,
			object value,
			object formattedValue,
			string errorText,
			DataGridViewCellStyle cellStyle,
			DataGridViewAdvancedBorderStyle advancedBorderStyle,
			DataGridViewPaintParts paintParts)
		{
			base.Paint(graphics, clipBounds, cellBounds, rowIndex, dataGridViewElementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);

			// HeaderCellCheckBox 大小
			Size checkBoxSize = CheckBoxRenderer.GetGlyphSize(graphics, CheckBoxState.UncheckedNormal);

			// HeaderCellCheckBox 位置
			Point checkBoxLocation = new Point();
			checkBoxLocation.X = cellBounds.Location.X + (cellBounds.Width / 2) - (checkBoxSize.Width / 2);
			checkBoxLocation.Y = cellBounds.Location.Y + (cellBounds.Height / 2) - (checkBoxSize.Height / 2);

			// 把相關資訊記錄到 Property 上,方便後續使用
			_headerCellLocation = cellBounds.Location;
			_headerCellCheckBoxLocation = checkBoxLocation;
			_headerCellCheckBoxSize = checkBoxSize;

			CheckBoxState checkBoxState = CheckBoxAllCheckedState ? CheckBoxState.CheckedNormal : CheckBoxState.UncheckedNormal;
			CheckBoxRenderer.DrawCheckBox(graphics, _headerCellCheckBoxLocation, checkBoxState);
		}

		protected override void OnMouseClick(DataGridViewCellMouseEventArgs e)
		{
			// 變數 p 是指 Click 位置
			Point p = new Point(e.X + _headerCellLocation.X, e.Y + _headerCellLocation.Y);
			if (p.X >= _headerCellCheckBoxLocation.X &&
				p.X <= _headerCellCheckBoxLocation.X + _headerCellCheckBoxSize.Width &&
				p.Y >= _headerCellCheckBoxLocation.Y &&
				p.Y <= _headerCellCheckBoxLocation.Y + _headerCellCheckBoxSize.Height)
			{
				CheckBoxAllCheckedState = !CheckBoxAllCheckedState;
				OnCheckBoxAll(new CheckBoxAllEventArgs(CheckBoxAllCheckedState));
			}

			base.OnMouseClick(e);
		}

		public event CheckBoxAllEventHandler CheckBoxAll;
		protected virtual void OnCheckBoxAll(CheckBoxAllEventArgs e)
		{
			if (CheckBoxAll != null)
				CheckBoxAll(this, e);
		}
	}

	public class CheckBoxAllEventArgs : EventArgs
	{
		public bool CheckedState { get; set; }

		public CheckBoxAllEventArgs(bool checkedState = false)
		{
			CheckedState = checkedState;
		}
	}

	public delegate void CheckBoxAllEventHandler(object sender, CheckBoxAllEventArgs e);
}

MyDGVCheckBoxColumn.cs
  1. 註冊 MyDGVCheckBoxColumnHeaderCell 的 CheckBoxAll Event 並對外開放
  2. 資料筆數和 ColumnCheckBox 為 true 資料比數是否相等,相等必須把 CheckBoxAll 設為 true
using System.Linq;
using System.Windows.Forms;

namespace DGVCheckBoxAll
{
	public class MyDGVCheckBoxColumn : DataGridViewCheckBoxColumn
	{
		private MyDGVCheckBoxColumnHeaderCell _headerCell;

		// 把 MyDGVCheckBoxColumnHeaderCell.CheckBBoxAll 事件對外開放
		public event CheckBoxAllEventHandler CheckBoxAll
		{
			add
			{
				_headerCell.CheckBoxAll += value;
			}
			remove
			{
				_headerCell.CheckBoxAll -= value;
			}
		}

		public MyDGVCheckBoxColumn(bool defaultValue = false)
		{
			_headerCell = new MyDGVCheckBoxColumnHeaderCell(defaultValue);
			_headerCell.CheckBoxAll += HeaderCell_CheckBoxAll;

			HeaderCell = _headerCell;

			// 不可能在 MyDGVCheckBoxColumn 建構子內去設定 ColumnCheckBox 內的值,
			// MyDGVCheckBoxColumn 產生時,DataGridView 還是 null,牽扯到控件生命週期
			// CheckBoxAllSetValue(DefaultValue);
		}

		private void HeaderCell_CheckBoxAll(object sender, CheckBoxAllEventArgs e)
		{
			CheckBoxAllSetColumnCheckedState(e.CheckedState);
		}

		/// <summary>
		/// 計算欄位 CheckBox 為 true 的資料筆數
		/// </summary>
		/// <returns>true 資料筆數</returns>
		private int CheckedStateCount()
		{
			int result = 0;
			if (DataGridView == null)
				return result;

			result = DataGridView.Rows
				.OfType<DataGridViewRow>()
				.Where(r => (bool)r.Cells[Index].EditedFormattedValue == true)
				.Count();

			return result;
		}

		/// <summary>
		/// 判斷 DataGridView 資料筆數 和 Column CheckBox 為 true 資料筆數,是否一致
		/// </summary>
		/// <returns>資料筆數是否一致</returns>
		private bool IsCheckState()
		{
			bool result = false;
			if (DataGridView == null)
				return result;

			if (DataGridView.Rows.Count == CheckedStateCount())
				result = true;

			return result;
		}

		/// <summary>
		/// 設定 CheckBoxAll 狀態
		/// </summary>
		public void SetCheckedState()
		{
			if ((HeaderCell is MyDGVCheckBoxColumnHeaderCell headerCell) == false)
				return;

			headerCell.CheckBoxAllCheckedState = IsCheckState();
            
			// 以前是使用 DataGridView.Refresh() 來達到重繪目的,
			// 後來發現有 DataGridView.InvalidateCell(),
			// DataGridView 只需要重繪 HeaderCell 就行
			DataGridView.InvalidateCell(_headerCell);
		}

		/// <summary>
		/// 依據 CheckBoxAll 狀態,設定 Column CheckBox 狀態
		/// <param name="checkedState">true 或 false</param>
		/// </summary>
		public void CheckBoxAllSetColumnCheckedState(bool checkedState)
		{
			// 避免 Focus 的資料,因為楚於 EditMode 狀態,而在 UI 沒有正確顯示是否勾選
			DataGridView.EndEdit();

			foreach (DataGridViewRow row in DataGridView.Rows)
				row.Cells[Index].Value = checkedState;
		}
	}
}
MyDGV.cs
using System.Windows.Forms;

namespace DGVCheckBoxAll
{
	public class MyDGV : DataGridView
	{
		public MyDGV()
		{
			AllowUserToAddRows = false;
			AutoGenerateColumns = false;
			CellContentClick += MyDGV_CellContentClick;
		}

		private void MyDGV_CellContentClick(object sender, DataGridViewCellEventArgs e)
		{
			if ((Columns[e.ColumnIndex] is MyDGVCheckBoxColumn col) == false)
				return;

			col.SetCheckedState();
		}
	}
}
測試 Code,在 MyDGV 上顯示自訂的 MyDGVCheckBoxColumn
using System;
using System.Data;
using System.Drawing;
using System.Windows.Forms;

namespace DGVCheckBoxAll
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        
        private bool _checkBoxAll1 = false;
        private bool _checkBoxAll2 = true;

        private void Form1_Load(object sender, EventArgs e)
        {
            // DataGridView
            MyDGV dgv = new MyDGV();
            dgv.Size = new Size(362, 176);
            Controls.Add(dgv);

            dgv.Columns.Add(new DataGridViewTextBoxColumn
            {
                DataPropertyName = "ID",
                Name = "ColID",
                HeaderText = "編號",
                Width = 100,
                Visible = true
            });

            // 自訂欄位設定
            MyDGVCheckBoxColumn Col1 = new MyDGVCheckBoxColumn(_checkBoxAll1)
            {
                DataPropertyName = "",
                Name = "ColChecked1",
                HeaderText = "",
                Width = 100,
                Visible = true
            };

            MyDGVCheckBoxColumn Col2 = new MyDGVCheckBoxColumn(_checkBoxAll2)
            {
                DataPropertyName = "",
                Name = "ColChecked2",
                HeaderText = "",
                Width = 100,
                Visible = true
            };

            // 建立自訂欄位,並指定欄位位置
            dgv.Columns.Insert(0, Col1);
            dgv.Columns.Insert(2, Col2);

            // 資料來源
            DataTable dt = new DataTable();
            dt.Columns.Add("ID", typeof(int));

            dt.Rows.Add(1);
            dt.Rows.Add(2);
            dt.Rows.Add(3);
            dt.Rows.Add(4);
            dt.Rows.Add(5);
            dgv.DataSource = dt;

            Col1.CheckBoxAll += Col1_CheckBoxAll;

            // 設定初始值
            Col2.CheckBoxAllSetColumnCheckedState(_checkBoxAll2);
        }

        private void Col1_CheckBoxAll(object sender, CheckBoxAllEventArgs e)
        {
            // DO Something
        }
    }
}
執行結果

[C#] DataGridView - CheckBox 全選-2

MyDGVCheckBoxColumn.CheckBoxAllSetColumnCheckedState() 沒有 EndEdit() 的話,點選 CheckBoxAll 會變成下圖,Focus 所在 cell 在 UI 上不會顯示

[C#] DataGridView - CheckBox 全選-3

沒有留言:

張貼留言