Users drag rows up and down in my DataGridView. I have the dragging logic down-pat, but I'd like there to be a dark marker indicating where the row will be placed after I let go of the mouse.
Example from Microsoft Access http://img718.imageshack.us/img718/8171/accessdrag.png
Example from Microsoft Access; I want to drag rows instead of columns
Does anyone know how I'd go about doing this? Is this built-in, or would I have to draw my own marker (if so, how do I do that)?
Thanks!
Here was my eventual solution. This control:
You can do whatever you want with this code (no warranty, etc.)
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
}
}
I did this for a treeview a couple years ago; can't remember exactly how, but consider using the MouseMove
event of the DataGridView.
While the drag is occurring, your MouseMove handler should:
DataGridView.PointToClient()
to convert them to relative) DataGridViewRow.DividerHeight
property.If you wanted to do something custom with the appearance of the row under the mouse (instead of just using the available properties), you can use the DataGridView.RowPostPaint
event. If you implement a handler for this event which is only used when a row is being dragged over another row, you can repaint the top or bottom border of the row with a bolder brush. MSDN example here.
The application that I'm working on does the marker as a separate Panel object with a height of 1 and a BackColor of 1. The Panel object is kept hidden until a drag and drop is actually in progress. This function, triggered on the DragOver event, implements most of the logic:
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;
}
Other than that, you just have to hide the Panel again when the drag and drop is complete or moved out of the DataGridView.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With