views:

546

answers:

3

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
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!

+2  A: 

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:

  • get the relative coordinates of the mouse (the MouseEventArgs contains the coordinates, but I think they're screen coordinates, so you can use DataGridView.PointToClient() to convert them to relative)
  • determine which row is at that X position (is there a method for this? If not, you can calculate it by adding up the row + row header heights, but remember that the grid may have been scrolled)
  • highlight that row or darken its border. One way you may be able to darken one border is by changing the DataGridViewRow.DividerHeight property.
  • when the mouse moves outside that row, restore it to how it previously looked.

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.

Charles
Yes there is a method to get the row/column, it's `DataGridView.HitTest()`. However, unless I can darker only one edge of the border, this doesn't tell me anything new: the inserted row would appear **between** two current rows, rather than replacing one, so I want a dark line between the two rows (see example above). What could I do once I have the row's display Rectangle?
BlueRaja - Danny Pflughoeft
Forget the rectangle, I have a better idea: create a handler for the DataGridView.RowPostPaint event. When the mouse is over the row, activate this handler. In the event handler, repaint the bottom border (or top, depending where the drop will land) with a heavier brush. (I will update my answer)But before you try that, you might play with the DataGridViewRow.DividerHeight property, which is the bottom border of the row. If you temporarily double the height of the border, it may give you the visual impact you're looking for.
Charles
The DividerHeight works reasonably well, for now. I will have to look into RowPostPaint later, when I have more time. Thanks!
BlueRaja - Danny Pflughoeft
A: 

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.

Mason
Unfortunately, this doesn't work - hovering over the panel triggers the DragLeave event! (also, if they happen to be hovering over the panel when they let go of the mouse, the drag-drop won't occur)
BlueRaja - Danny Pflughoeft
Just took a look at it on my app. Turns out that the DragLeave event does get triggered when you pass over the panel, but in my code, all that DragLeave does is hide the panel, which then makes the drag enter the DataGridView again, and the HitTest call in DragOver then moves the panel up again.
Mason
A: 

Here was my eventual solution. This control:

  • Allows dragging one row to another
  • Highlights the insert position using a divider
  • Auto-scrolls when the user gets to the edge of the control while dragging
  • Supports multiple instances of the control
    • Can drag rows from one instance to another
    • Only one row will be selected throughout all instances of the control
  • Custom highlighting of rows

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
    }
}
BlueRaja - Danny Pflughoeft