在 DataGridView 上移动行时的可视标记
Posted
技术标签:
【中文标题】在 DataGridView 上移动行时的可视标记【英文标题】:Visual marker when moving rows on DataGridView 【发布时间】:2011-01-29 13:42:02 【问题描述】:用户在我的 DataGridView 中上下拖动行。我有向下拖动的逻辑,但我希望有一个黑色标记,指示我放开鼠标后该行的放置位置。
Example from Microsoft Access http://img718.imageshack.us/img718/8171/accessdrag.png来自 Microsoft Access 的示例;我想拖动行而不是列
有人知道我会怎么做吗?这是内置的,还是我必须自己绘制标记(如果是,我该怎么做)?
谢谢!
【问题讨论】:
这是在 WPF 中完成的吗? (我不得不承认,从截图上看,它看起来像 WPF,但我对 WPF 还不是很熟悉......) 不,它是 WinForms;该屏幕截图是 Access 2007 的,它也(我相信)不是 WPF 有趣的是,列排序和视觉标记都是内置的。 【参考方案1】:这是我的最终解决方案。这个控件:
允许将一行拖到另一行 使用分隔符突出插入位置 当用户在拖动时到达控件边缘时自动滚动 支持控件的多个实例 可以将行从一个实例拖到另一个实例 在控件的所有实例中只会选择一行 行的自定义突出显示你可以用这个代码做任何你想做的事情(没有保修等)
using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace CAM_Products.General_Controls
public class DataGridViewWithDraggableRows : DataGridView
private int? _predictedInsertIndex; //Index to draw divider at. Null means no divider
private Timer _autoScrollTimer;
private int _scrollDirection;
private static DataGridViewRow _selectedRow;
private bool _ignoreSelectionChanged;
private static event EventHandler<EventArgs> OverallSelectionChanged;
private SolidBrush _dividerBrush;
private Pen _selectionPen;
#region Designer properties
/// <summary>
/// The color of the divider displayed between rows while dragging
/// </summary>
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("The color of the divider displayed between rows while dragging")]
public Color DividerColor
get return _dividerBrush.Color;
set _dividerBrush = new SolidBrush(value);
/// <summary>
/// The color of the border drawn around the selected row
/// </summary>
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("The color of the border drawn around the selected row")]
public Color SelectionColor
get return _selectionPen.Color;
set _selectionPen = new Pen(value);
/// <summary>
/// Height (in pixels) of the divider to display
/// </summary>
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("Height (in pixels) of the divider to display")]
[DefaultValue(4)]
public int DividerHeight get; set;
/// <summary>
/// Width (in pixels) of the border around the selected row
/// </summary>
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("Width (in pixels) of the border around the selected row")]
[DefaultValue(3)]
public int SelectionWidth get; set;
#endregion
#region Form setup
public DataGridViewWithDraggableRows()
InitializeProperties();
SetupTimer();
private void InitializeProperties()
#region Code stolen from designer
this.AllowDrop = true;
this.AllowUserToAddRows = false;
this.AllowUserToDeleteRows = false;
this.AllowUserToOrderColumns = true;
this.AllowUserToResizeRows = false;
this.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
this.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.EnableHeadersVisualStyles = false;
this.MultiSelect = false;
this.ReadOnly = true;
this.RowHeadersVisible = false;
this.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
this.CellMouseDown += dataGridView1_CellMouseDown;
this.DragOver += dataGridView1_DragOver;
this.DragLeave += dataGridView1_DragLeave;
this.DragEnter += dataGridView1_DragEnter;
this.Paint += dataGridView1_Paint_Selection;
this.Paint += dataGridView1_Paint_RowDivider;
this.DefaultCellStyleChanged += dataGridView1_DefaultcellStyleChanged;
this.Scroll += dataGridView1_Scroll;
#endregion
_ignoreSelectionChanged = false;
OverallSelectionChanged += OnOverallSelectionChanged;
_dividerBrush = new SolidBrush(Color.Red);
_selectionPen = new Pen(Color.Blue);
DividerHeight = 4;
SelectionWidth = 3;
#endregion
#region Selection
/// <summary>
/// All instances of this class share an event, so that only one row
/// can be selected throughout all instances.
/// This method is called when a row is selected on any DataGridView
/// </summary>
private void OnOverallSelectionChanged(object sender, EventArgs e)
if(sender != this && SelectedRows.Count != 0)
ClearSelection();
Invalidate();
protected override void OnSelectionChanged(EventArgs e)
if(_ignoreSelectionChanged)
return;
if(SelectedRows.Count != 1 || SelectedRows[0] != _selectedRow)
_ignoreSelectionChanged = true; //Following lines cause event to be raised again
if(_selectedRow == null || _selectedRow.DataGridView != this)
ClearSelection();
else
_selectedRow.Selected = true; //Deny new selection
if(OverallSelectionChanged != null)
OverallSelectionChanged(this, EventArgs.Empty);
_ignoreSelectionChanged = false;
else
base.OnSelectionChanged(e);
if(OverallSelectionChanged != null)
OverallSelectionChanged(this, EventArgs.Empty);
public void SelectRow(int rowIndex)
_selectedRow = Rows[rowIndex];
_selectedRow.Selected = true;
Invalidate();
#endregion
#region Selection highlighting
private void dataGridView1_Paint_Selection(object sender, PaintEventArgs e)
if(_selectedRow == null || _selectedRow.DataGridView != this)
return;
Rectangle displayRect = GetRowDisplayRectangle(_selectedRow.Index, false);
if(displayRect.Height == 0)
return;
_selectionPen.Width = SelectionWidth;
int heightAdjust = (int)Math.Ceiling((float)SelectionWidth/2);
e.Graphics.DrawRectangle(_selectionPen, displayRect.X - 1, displayRect.Y - heightAdjust,
displayRect.Width, displayRect.Height + SelectionWidth - 1);
private void dataGridView1_DefaultcellStyleChanged(object sender, EventArgs e)
DefaultCellStyle.SelectionBackColor = DefaultCellStyle.BackColor;
DefaultCellStyle.SelectionForeColor = DefaultCellStyle.ForeColor;
private void dataGridView1_Scroll(object sender, ScrollEventArgs e)
Invalidate();
#endregion
#region Drag-and-drop
protected override void OnDragDrop(DragEventArgs args)
if(args.Effect == DragDropEffects.None)
return;
//Convert to coordinates within client (instead of screen-coordinates)
Point clientPoint = PointToClient(new Point(args.X, args.Y));
//Get index of row to insert into
DataGridViewRow dragFromRow = (DataGridViewRow)args.Data.GetData(typeof(DataGridViewRow));
int newRowIndex = GetNewRowIndex(clientPoint.Y);
//Adjust index if both rows belong to same DataGridView, due to removal of row
if(dragFromRow.DataGridView == this && dragFromRow.Index < newRowIndex)
newRowIndex--;
//Clean up
RemoveHighlighting();
_autoScrollTimer.Enabled = false;
//Only go through the trouble if we're actually moving the row
if(dragFromRow.DataGridView != this || newRowIndex != dragFromRow.Index)
//Insert the row
MoveDraggedRow(dragFromRow, newRowIndex);
//Let everyone know the selection has changed
SelectRow(newRowIndex);
base.OnDragDrop(args);
private void dataGridView1_DragLeave(object sender, EventArgs e1)
RemoveHighlighting();
_autoScrollTimer.Enabled = false;
private void dataGridView1_DragEnter(object sender, DragEventArgs e)
e.Effect = (e.Data.GetDataPresent(typeof(DataGridViewRow))
? DragDropEffects.Move
: DragDropEffects.None);
private void dataGridView1_DragOver(object sender, DragEventArgs e)
if(e.Effect == DragDropEffects.None)
return;
Point clientPoint = PointToClient(new Point(e.X, e.Y));
//Note: For some reason, HitTest is failing when clientPoint.Y = dataGridView1.Height-1.
// I have no idea why.
// clientPoint.Y is always 0 <= clientPoint.Y < dataGridView1.Height
if(clientPoint.Y < Height - 1)
int newRowIndex = GetNewRowIndex(clientPoint.Y);
HighlightInsertPosition(newRowIndex);
StartAutoscrollTimer(e);
private void dataGridView1_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
if(e.Button == MouseButtons.Left && e.RowIndex >= 0)
SelectRow(e.RowIndex);
var dragObject = Rows[e.RowIndex];
DoDragDrop(dragObject, DragDropEffects.Move);
//TODO: Any way to make this *not* happen if they only click?
/// <summary>
/// Based on the mouse position, determines where the new row would
/// be inserted if the user were to release the mouse-button right now
/// </summary>
/// <param name="clientY">
/// The y-coordinate of the mouse, given with respectto the control
/// (not the screen)
/// </param>
private int GetNewRowIndex(int clientY)
int lastRowIndex = Rows.Count - 1;
//DataGridView has no cells
if(Rows.Count == 0)
return 0;
//Dragged above the DataGridView
if(clientY < GetRowDisplayRectangle(0, true).Top)
return 0;
//Dragged below the DataGridView
int bottom = GetRowDisplayRectangle(lastRowIndex, true).Bottom;
if(bottom > 0 && clientY >= bottom)
return lastRowIndex + 1;
//Dragged onto one of the cells. Depending on where in cell,
// insert before or after row.
var hittest = HitTest(2, clientY); //Don't care about X coordinate
if(hittest.RowIndex == -1)
//This should only happen when midway scrolled down the page,
//and user drags over header-columns
//Grab the index of the current top (displayed) row
return FirstDisplayedScrollingRowIndex;
//If we are hovering over the upper-quarter of the row, place above;
// otherwise below. Experimenting shows that placing above at 1/4
//works better than at 1/2 or always below
if(clientY < GetRowDisplayRectangle(hittest.RowIndex, false).Top
+ Rows[hittest.RowIndex].Height/4)
return hittest.RowIndex;
return hittest.RowIndex + 1;
private void MoveDraggedRow(DataGridViewRow dragFromRow, int newRowIndex)
dragFromRow.DataGridView.Rows.Remove(dragFromRow);
Rows.Insert(newRowIndex, dragFromRow);
#endregion
#region Drop-and-drop highlighting
//Draw the actual row-divider
private void dataGridView1_Paint_RowDivider(object sender, PaintEventArgs e)
if(_predictedInsertIndex != null)
e.Graphics.FillRectangle(_dividerBrush, GetHighlightRectangle());
private Rectangle GetHighlightRectangle()
int width = DisplayRectangle.Width - 2;
int relativeY = (_predictedInsertIndex > 0
? GetRowDisplayRectangle((int)_predictedInsertIndex - 1, false).Bottom
: Columns[0].HeaderCell.Size.Height);
if(relativeY == 0)
relativeY = GetRowDisplayRectangle(FirstDisplayedScrollingRowIndex, true).Top;
int locationX = Location.X + 1;
int locationY = relativeY - (int)Math.Ceiling((double)DividerHeight/2);
return new Rectangle(locationX, locationY, width, DividerHeight);
private void HighlightInsertPosition(int rowIndex)
if(_predictedInsertIndex == rowIndex)
return;
Rectangle oldRect = GetHighlightRectangle();
_predictedInsertIndex = rowIndex;
Rectangle newRect = GetHighlightRectangle();
Invalidate(oldRect);
Invalidate(newRect);
private void RemoveHighlighting()
if(_predictedInsertIndex != null)
Rectangle oldRect = GetHighlightRectangle();
_predictedInsertIndex = null;
Invalidate(oldRect);
else
Invalidate();
#endregion
#region Autoscroll
private void SetupTimer()
_autoScrollTimer = new Timer
Interval = 250,
Enabled = false
;
_autoScrollTimer.Tick += OnAutoscrollTimerTick;
private void StartAutoscrollTimer(DragEventArgs args)
Point position = PointToClient(new Point(args.X, args.Y));
if(position.Y <= Font.Height/2 &&
FirstDisplayedScrollingRowIndex > 0)
//Near top, scroll up
_scrollDirection = -1;
_autoScrollTimer.Enabled = true;
else if(position.Y >= ClientSize.Height - Font.Height/2 &&
FirstDisplayedScrollingRowIndex < Rows.Count - 1)
//Near bottom, scroll down
_scrollDirection = 1;
_autoScrollTimer.Enabled = true;
else
_autoScrollTimer.Enabled = false;
private void OnAutoscrollTimerTick(object sender, EventArgs e)
//Scroll up/down
FirstDisplayedScrollingRowIndex += _scrollDirection;
#endregion
【讨论】:
【参考方案2】:几年前我为树视图做了这个;具体方法不记得了,但可以考虑使用 DataGridView 的 MouseMove
事件。
当拖动发生时,您的 MouseMove 处理程序应该:
获取相对坐标 鼠标(MouseEventArgs 包含 坐标,但我认为它们是屏幕坐标,因此您可以使用DataGridView.PointToClient()
将它们转换为相对坐标)
确定哪一行在那个 X
位置(有这个方法吗?如果没有,你可以通过将行+行标题高度相加来计算,但记住网格可能已经滚动)
突出显示该行或使其变暗
边境。一种可以使边框变暗的方法是更改 DataGridViewRow.DividerHeight
属性。
当鼠标移出
行,将其恢复到以前的状态
看了。
如果您想对鼠标下行的外观进行自定义(而不仅仅是使用可用属性),您可以使用DataGridView.RowPostPaint
事件。如果您为此事件实现一个处理程序,该处理程序仅在将一行拖到另一行上时使用,您可以使用更粗的画笔重新绘制该行的顶部或底部边框。 MSDN example here.
【讨论】:
是的,有一种方法可以获取行/列,它是DataGridView.HitTest()
。但是,除非我只能使边框的一个边缘变暗,否则这并不能告诉我任何新信息:插入的行将出现在 两个当前行之间,而不是替换一个,所以我想要一条暗线在两行之间(参见上面的示例)。获得该行的显示 Rectangle 后,我能做什么?
忘记矩形,我有一个更好的主意:为 DataGridView.RowPostPaint 事件创建一个处理程序。当鼠标悬停在行上时,激活此处理程序。在事件处理程序中,使用较重的画笔重新绘制底部边框(或顶部,取决于水滴的位置)。 (我会更新我的答案)但在您尝试之前,您可能会使用 DataGridViewRow.DividerHeight 属性,它是该行的底部边框。如果您暂时将边框高度加倍,它可能会给您想要的视觉效果。
DividerHeight 目前运行良好。当我有更多时间时,我将不得不研究 RowPostPaint。谢谢!【参考方案3】:
我正在处理的应用程序将标记作为一个单独的 Panel 对象,高度为 1,BackColor 为 1。Panel 对象一直隐藏,直到实际进行拖放。这个函数在 DragOver 事件上触发,实现了大部分逻辑:
public static void frameG_dragover(Form current_form, DataGridView FRAMEG, Panel drag_row_indicator, Point mousePos)
int FRAMEG_Row_Height = FRAMEG.RowTemplate.Height;
int FRAMEG_Height = FRAMEG.Height;
int Loc_X = FRAMEG.Location.X + 2;
Point clientPoint = FRAMEG.PointToClient(mousePos);
int CurRow = FRAMEG.HitTest(clientPoint.X, clientPoint.Y).RowIndex;
int Loc_Y = 0;
if (CurRow != -1)
Loc_Y = FRAMEG.Location.Y + ((FRAMEG.Rows[CurRow].Index + 1) * FRAMEG_Row_Height) - FRAMEG.VerticalScrollingOffset;
else
Loc_Y = FRAMEG.Location.Y + (FRAMEG.Rows.Count + 1) * FRAMEG_Row_Height;
int width_c = FRAMEG.Columns[0].Width + FRAMEG.Columns[1].Width + FRAMEG.Columns[2].Width;
if ((Loc_Y > (FRAMEG.Location.Y)) && (Loc_Y < (FRAMEG.Location.Y + FRAMEG_Height - FRAMEG_Row_Height))) //+ FRAMEG_Row_Height
drag_row_indicator.Location = new System.Drawing.Point(Loc_X, Loc_Y);
drag_row_indicator.Size = new Size(width_c, 1);
if (!drag_row_indicator.Visible)
drag_row_indicator.Visible = true;
除此之外,您只需在拖放完成或移出 DataGridView 时再次隐藏 Panel。
【讨论】:
不幸的是,这不起作用 - 将鼠标悬停在面板上会触发 DragLeave 事件! (另外,如果他们松开鼠标时碰巧将鼠标悬停在面板上,则不会发生拖放) 刚刚在我的应用程序上看了一下。事实证明,当您通过面板时,DragLeave 事件确实会被触发,但在我的代码中,DragLeave 所做的只是隐藏面板,然后使拖动再次进入 DataGridView,然后 DragOver 中的 HitTest 调用移动面板又起来了。以上是关于在 DataGridView 上移动行时的可视标记的主要内容,如果未能解决你的问题,请参考以下文章
C# 选中 DataGridView 控件中的行时显示不同的颜色
winform如何从DataGridView中从右键菜单获取一行数据
C# DataGridView 数据显示到最后一行后,如何使滚动条继续向下滚动。