# 多单元格拖动填充DataGridView功能实现总结 本文档围绕WinForm的DataGridView控件,实现了一套贴近Excel体验的多单元格拖动填充功能,核心涵盖高亮反馈、内容预览、循环填充三大核心特性,兼顾实用性与用户体验。 功能实现以自定义控件为载体,继承原生DataGridView并扩展核心逻辑。通过监听鼠标按下、移动、抬起事件,精准判断填充柄操作:仅当鼠标点击选中区域右下角5x5填充柄时,触发拖动状态,否则保留原生控件操作,避免功能冲突。拖动过程中,实时计算目标区域行列范围,绘制半透明浅蓝色高亮背景与深蓝色边框,清晰标记填充范围。 同时新增内容预览功能,基于循环取模算法,同步计算目标单元格对应起始区域的数据,以深灰色半透明文字居中绘制预览内容,确保预览与最终填充结果一致,且仅预览待填充区域,不重复绘制起始数据。填充逻辑支持多单元格矩形区域起始,通过取模运算实现数据循环复制,跳过只读单元格,兼顾安全性与兼容性。 优化层面,启用双缓冲减少闪烁,通过缩小高亮绘制区域避免遮挡原生内容,增加行列范围容错判断,防止索引越界。整体功能既保留了DataGridView原生操作体验,又通过Excel风格的视觉反馈与交互逻辑,提升了数据填充的便捷性,可直接应用于各类WinForm数据管理场景。
using System; using System.Drawing; using System.Windows.Forms; namespace DataGridViewFillHandleDemo { /// <summary> /// 支持多单元格拖动填充+高亮+内容实时预览的DataGridView(兼容Excel风格) /// </summary> public class DataGridViewWithMultiCellFill : DataGridView { #region 升级后的状态变量定义 // 填充柄大小(单元格右下角小方块,默认5x5像素) private readonly Size _fillHandleSize = new Size(8, 8); // 是否处于拖动填充状态 private bool _isDragging; // 是否鼠标悬停在填充柄上 private bool _isHoverFillHandle; // 起始选中区域(多单元格):记录行列范围 private int _startMinCol, _startMaxCol, _startMinRow, _startMaxRow; // 起始选中区域的原始数据(二维集合,保存多单元格数据) private object[,] _startCellData; // 鼠标起始位置 private Point _startMousePos; // 填充目标区域的结束单元格 private DataGridViewCell _endCell; // 高亮颜色(半透明浅蓝,不遮挡内容) private readonly Color _highlightColor = Color.FromArgb(150, 210, 255); // 预览文字颜色(深灰色,区别于原生内容) private readonly Color _previewTextColor = Color.FromArgb(100, 0, 0, 0); #endregion #region 构造函数 public DataGridViewWithMultiCellFill() { // 启用双缓冲,减少闪烁 this.DoubleBuffered = true; // 允许单元格多选(支持多单元格选中) this.SelectionMode = DataGridViewSelectionMode.CellSelect; this.MultiSelect = true; // 启用鼠标移动监听 this.MouseMove += DataGridViewWithMultiCellFill_MouseMove; this.MouseUp += DataGridViewWithMultiCellFill_MouseUp; } #endregion #region 核心鼠标事件处理(升级多单元格支持) /// <summary> /// 鼠标按下:判断填充柄(基于选中区域右下角),初始化多单元格拖动状态 /// </summary> protected override void OnMouseDown(MouseEventArgs e) { // 仅处理左键按下,且必须有选中单元格 if (e.Button != MouseButtons.Left || this.SelectedCells.Count == 0) return; // 1. 获取当前选中区域的行列范围(多单元格) GetSelectedCellRange(out _startMinCol, out _startMaxCol, out _startMinRow, out _startMaxRow); // 2. 选中区域的右下角单元格(填充柄所在位置,与Excel一致) var bottomRightCell = this[_startMaxCol, _startMaxRow]; if (bottomRightCell == null) return; // 3. 判断鼠标是否在右下角单元格的填充柄内 if (IsPointInFillHandle(bottomRightCell, e.Location)) { // 4. 初始化拖动状态,保存起始区域原始数据 _isDragging = true; _startMousePos = e.Location; _endCell = bottomRightCell; _startCellData = GetSelectedCellData(); // 保存多单元格数据 this.Cursor = Cursors.Cross; // 切换鼠标样式为十字光标 } else { base.OnMouseDown(e); } } /// <summary> /// 鼠标移动:更新填充区域,支持多单元格拖动预览+高亮+内容预览 /// </summary> private void DataGridViewWithMultiCellFill_MouseMove(object sender, MouseEventArgs e) { // 1. 非拖动状态:判断是否悬停在填充柄上,更新鼠标样式 if (!_isDragging) { _isHoverFillHandle = false; if (this.SelectedCells.Count > 0) { // 获取选中区域右下角单元格 GetSelectedCellRange(out int minCol, out int maxCol, out int minRow, out int maxRow); var bottomRightCell = this[maxCol, maxRow]; if (bottomRightCell != null) { _isHoverFillHandle = IsPointInFillHandle(bottomRightCell, e.Location); } } // 切换填充柄悬停鼠标样式(黑色十字) this.Cursor = _isHoverFillHandle ? Cursors.Cross : Cursors.Default; this.Invalidate(); // 重绘控件,显示填充柄 return; } // 2. 拖动状态:更新目标单元格,触发重绘实现高亮+内容预览 if (e.Button != MouseButtons.Left) return; var hitTestInfoDrag = this.HitTest(e.X, e.Y); if (hitTestInfoDrag.Type == DataGridViewHitTestType.Cell) { _endCell = this[hitTestInfoDrag.ColumnIndex, hitTestInfoDrag.RowIndex]; this.Invalidate(); // 关键:重绘控件,触发OnPaint绘制高亮和内容 } } /// <summary> /// 鼠标抬起:结束拖动,执行多单元格数据批量填充逻辑 /// </summary> private void DataGridViewWithMultiCellFill_MouseUp(object sender, MouseEventArgs e) { if (!_isDragging || _startCellData == null || _endCell == null) { ResetDragState(); // 重置状态 return; } // 执行多单元格数据填充(核心:循环复制起始区域数据到目标区域) FillMultiCellData(); // 重置所有拖动状态 ResetDragState(); } #endregion #region 辅助方法(新增/升级:支持多单元格) /// <summary> /// 获取当前选中单元格的行列范围(多单元格) /// </summary> private void GetSelectedCellRange(out int minCol, out int maxCol, out int minRow, out int maxRow) { minCol = int.MaxValue; maxCol = int.MinValue; minRow = int.MaxValue; maxRow = int.MinValue; // 遍历所有选中单元格,确定行列极值 foreach (DataGridViewCell cell in this.SelectedCells) { if (cell.ColumnIndex < minCol) minCol = cell.ColumnIndex; if (cell.ColumnIndex > maxCol) maxCol = cell.ColumnIndex; if (cell.RowIndex < minRow) minRow = cell.RowIndex; if (cell.RowIndex > maxRow) maxRow = cell.RowIndex; } // 容错:没有选中单元格时重置为0 if (minCol == int.MaxValue) minCol = maxCol = 0; if (minRow == int.MaxValue) minRow = maxRow = 0; } /// <summary> /// 获取选中区域的所有单元格数据(存入二维数组,保留原始结构) /// </summary> private object[,] GetSelectedCellData() { // 计算起始区域的行列数量 int colCount = _startMaxCol - _startMinCol + 1; int rowCount = _startMaxRow - _startMinRow + 1; object[,] cellData = new object[rowCount, colCount]; // 遍历选中区域,保存每个单元格的数据 for (int row = 0; row < rowCount; row++) { for (int col = 0; col < colCount; col++) { var targetCell = this[_startMinCol + col, _startMinRow + row]; cellData[row, col] = targetCell.Value ?? DBNull.Value; } } return cellData; } /// <summary> /// 验证鼠标坐标是否位于指定单元格的右下角填充柄内 /// </summary> private bool IsPointInFillHandle(DataGridViewCell cell, Point mousePos) { Rectangle cellRect = this.GetCellDisplayRectangle(cell.ColumnIndex, cell.RowIndex, false); if (cellRect.IsEmpty) return false; // 计算填充柄的区域(单元格右下角 5x5 方块) Rectangle fillHandleRect = new Rectangle( cellRect.Right - _fillHandleSize.Width, cellRect.Bottom - _fillHandleSize.Height, _fillHandleSize.Width, _fillHandleSize.Height ); return fillHandleRect.Contains(mousePos); } #endregion #region 核心业务:多单元格数据填充逻辑(循环复制,贴近Excel) /// <summary> /// 多单元格批量填充:将起始区域数据循环复制到目标拖动区域 /// </summary> private void FillMultiCellData() { // 1. 确定目标填充区域的行列范围 int targetMinCol = Math.Min(_startMinCol, _endCell.ColumnIndex); int targetMaxCol = Math.Max(_startMaxCol, _endCell.ColumnIndex); int targetMinRow = Math.Min(_startMinRow, _endCell.RowIndex); int targetMaxRow = Math.Max(_startMaxRow, _endCell.RowIndex); // 2. 起始区域的行列数量(用于循环填充) int startColCount = _startMaxCol - _startMinCol + 1; int startRowCount = _startMaxRow - _startMinRow + 1; if (startColCount <= 0 || startRowCount <= 0) return; // 3. 锁定控件更新,避免填充过程中闪烁 this.SuspendLayout(); this.BeginEdit(false); // 4. 遍历目标区域,循环复制起始区域数据(Excel默认填充逻辑) for (int targetRow = targetMinRow; targetRow <= targetMaxRow; targetRow++) { for (int targetCol = targetMinCol; targetCol <= targetMaxCol; targetCol++) { // 计算当前目标单元格对应起始区域的索引(循环取模) int startRowIndex = (targetRow - _startMinRow) % startRowCount; int startColIndex = (targetCol - _startMinCol) % startColCount; // 避免索引越界(处理反向拖动) if (startRowIndex < 0) startRowIndex += startRowCount; if (startColIndex < 0) startColIndex += startColCount; var targetCell = this[targetCol, targetRow]; // 跳过只读单元格,避免报错 if (!targetCell.ReadOnly) { // 复制起始区域对应位置的数据 targetCell.Value = _startCellData[startRowIndex, startColIndex]; } } } // 5. 解锁控件更新,刷新显示 this.EndEdit(); this.ResumeLayout(true); this.Invalidate(); } #endregion #region 辅助方法:重置状态&绘制填充柄+拖动高亮+内容预览 /// <summary> /// 重置所有拖动相关的状态变量 /// </summary> private void ResetDragState() { _isDragging = false; _isHoverFillHandle = false; _startCellData = null; _endCell = null; _startMinCol = _startMaxCol = _startMinRow = _startMaxRow = 0; this.Cursor = Cursors.Default; this.Invalidate(); // 重置时清除高亮和预览内容 } /// <summary> /// 重绘控件:绘制填充柄 + 拖动高亮 + 实时内容预览 /// </summary> protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // ========== 1. 绘制填充柄(原有逻辑) ========== if (this.SelectedCells.Count > 0 && _isHoverFillHandle) { GetSelectedCellRange(out int minCol, out int maxCol, out int minRow, out int maxRow); var bottomRightCell = this[maxCol, maxRow]; if (bottomRightCell != null) { Rectangle cellRect = this.GetCellDisplayRectangle(bottomRightCell.ColumnIndex, bottomRightCell.RowIndex, false); if (!cellRect.IsEmpty) { Rectangle fillHandleRect = new Rectangle( cellRect.Right - _fillHandleSize.Width, cellRect.Bottom - _fillHandleSize.Height, _fillHandleSize.Width, _fillHandleSize.Height ); using (var brush = new SolidBrush(Color.Black)) { e.Graphics.FillRectangle(brush, fillHandleRect); } } } } // ========== 2. 绘制拖动目标区域高亮 + 实时内容预览 ========== if (_isDragging && _endCell != null && _startCellData != null) { // 计算目标区域的行列范围 int targetMinCol = Math.Min(_startMinCol, _endCell.ColumnIndex); int targetMaxCol = Math.Max(_startMaxCol, _endCell.ColumnIndex); int targetMinRow = Math.Min(_startMinRow, _endCell.RowIndex); int targetMaxRow = Math.Max(_startMaxRow, _endCell.RowIndex); // 起始区域的行列数量(用于循环计算预览值) int startColCount = _startMaxCol - _startMinCol + 1; int startRowCount = _startMaxRow - _startMinRow + 1; // 遍历目标区域的每个单元格 for (int targetRow = targetMinRow; targetRow <= targetMaxRow; targetRow++) { for (int targetCol = targetMinCol; targetCol <= targetMaxCol; targetCol++) { // 强容错:跳过超出表格范围、表头行列 if (targetRow < 0 || targetCol < 0 || targetRow >= this.RowCount || targetCol >= this.ColumnCount) continue; Rectangle cellRect = this.GetCellDisplayRectangle(targetCol, targetRow, false); if (cellRect.IsEmpty) continue; // ===== 步骤1:绘制半透明高亮背景(不覆盖原生内容) ===== using (var highlightBrush = new SolidBrush(_highlightColor)) { // 缩小绘制区域,留出1像素边距 Rectangle drawRect = new Rectangle( cellRect.X + 1, cellRect.Y + 1, cellRect.Width - 2, cellRect.Height - 2 ); e.Graphics.FillRectangle(highlightBrush, drawRect); } // ===== 步骤2:计算并绘制预览内容 ===== // 计算当前目标单元格对应起始区域的索引(循环取模) int startRowIndex = (targetRow - _startMinRow) % startRowCount; int startColIndex = (targetCol - _startMinCol) % startColCount; // 处理反向拖动的负索引 if (startRowIndex < 0) startRowIndex += startRowCount; if (startColIndex < 0) startColIndex += startColCount; // 获取预览值(跳过起始区域自身,只预览填充的新内容) bool isInStartArea = targetRow >= _startMinRow && targetRow <= _startMaxRow && targetCol >= _startMinCol && targetCol <= _startMaxCol; if (!isInStartArea) { object previewValue = _startCellData[startRowIndex, startColIndex]; if (previewValue != DBNull.Value && previewValue != null) { // 设置文字格式(居中对齐,与DataGridView原生样式一致) StringFormat sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; // 绘制预览文字(深灰色半透明,区别于原生内容) using (var textBrush = new SolidBrush(_previewTextColor)) { e.Graphics.DrawString( previewValue.ToString(), this.Font, textBrush, cellRect, sf ); } } } // ===== 步骤3:绘制单元格边框(增强视觉效果) ===== using (var borderPen = new Pen(Color.DarkBlue, 1)) { e.Graphics.DrawRectangle(borderPen, cellRect); } } } } } #endregion } }