How To AutoScroll a DataGridView during Drag and D

2020-02-29 06:52发布

问题:

One of the forms in my C# .NET application has multiple DataGridViews that implement drag and drop to move the rows around. The drag and drop mostly works right, but I've been having a hard time getting the DataGridViews to AutoScroll - when a row is dragged near the top or bottom of the box, to scroll it in that direction.

So far, I've tried implementing a version of this solution. I have a ScrollingGridView class inheriting from DataGridView that implements the described timer, and according to the debugger, the timer is firing appropriately, but the timer code:

const int WM_VSCROLL = 277;
private static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);

private void ScrollingGridViewTimerTick(object sender, EventArgs e)
{
    SendMessage(Handle, WM_VSCROLL, (IntPtr)scrollDirectionInt, IntPtr.Zero);
}

doesn't do anything as far as I can tell, possibly because I have multiple DataGridViews in the form. I also tried modifying the AutoScrollOffset property, but that didn't do anything either. Investigation of the DataGridView and ScrollBar classes doesn't seem to suggest any other commands or functions that will actually make the DataGridView scroll. Can anyone help me with a function that will actually scroll the DataGridView, or some other way to solve the problem?

回答1:

I haven't looked at this code in a while. But a while back I implemented a DataGridView that supported just this.

    class DragOrderedDataGridView : System.Windows.Forms.DataGridView
{
    public delegate void RowDroppedEventHangler(object source, DataGridViewRow sourceRow, DataGridViewRow destinationRow);
    public event RowDroppedEventHangler RowDropped;

    bool bDragging = false;
    System.Windows.Forms.DataGridView.HitTestInfo hti = null;
    System.Threading.Timer scrollTimer = null;
    delegate void SetScrollDelegate(int value);

    public bool AllowDragOrdering { get; set; }

    protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
    {
        if (AllowDragOrdering)
        {
            DataGridView.HitTestInfo hti = this.HitTest(e.X, e.Y);

            if (hti.RowIndex != -1
             && hti.RowIndex != this.NewRowIndex
             && e.Button == MouseButtons.Left)
            {
                bDragging = true;
            }
        }

        base.OnMouseDown(e);
    }

    protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs e)
    {
        if (bDragging && e.Button == MouseButtons.Left)
        {
            DataGridView.HitTestInfo newhti = this.HitTest(e.X, e.Y);
            if (hti != null && hti.RowIndex != newhti.RowIndex)
            {
                System.Diagnostics.Debug.WriteLine("invalidating " + hti.RowIndex.ToString());
                Invalidate();
            }
            hti = newhti;
            System.Diagnostics.Debug.WriteLine(string.Format("{0:000} {1}  ", hti.RowIndex, e.Location));

            Point clientPoint = this.PointToClient(e.Location);


            System.Diagnostics.Debug.WriteLine(e.Location + "  " + this.Bounds.Size);
            if (scrollTimer == null
            && ShouldScrollDown(e.Location))
            {
                //
                // enable the timer to scroll the screen
                //
                scrollTimer = new System.Threading.Timer(new System.Threading.TimerCallback(TimerScroll), 1, 0, 250);
            }
            if (scrollTimer == null
            && ShouldScrollUp(e.Location))
            {
                scrollTimer = new System.Threading.Timer(new System.Threading.TimerCallback(TimerScroll), -1, 0, 250);
            }

        }
        else
        {
            bDragging = false;
        }

        if (!(ShouldScrollUp(e.Location) || ShouldScrollDown(e.Location)))
        {
            StopAutoScrolling();
        }
        base.OnMouseMove(e);
    }

    bool ShouldScrollUp(Point location)
    {
        return location.Y > this.ColumnHeadersHeight
            && location.Y < this.ColumnHeadersHeight + 15
            && location.X >= 0
            && location.X <= this.Bounds.Width;
    }

    bool ShouldScrollDown(Point location)
    {
        return location.Y > this.Bounds.Height - 15
            && location.Y < this.Bounds.Height
            && location.X >= 0
            && location.X <= this.Bounds.Width;
    }

    void StopAutoScrolling()
    {
        if (scrollTimer != null)
        {
            //
            // disable the timer to scroll the screen
            // 
            scrollTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
            scrollTimer = null;
        }
    }

    void TimerScroll(object state)
    {
        SetScrollBar((int)state);
    }

    bool scrolling = false;

    void SetScrollBar(int direction)
    {
        if (scrolling)
        {
            return;
        }
        if (this.InvokeRequired)
        {
            this.Invoke(new Action<int>(SetScrollBar), new object[] {direction});
        }
        else
        {
            scrolling = true;

            if (0 < direction)
            {
                if (this.FirstDisplayedScrollingRowIndex < this.Rows.Count - 1)
                {
                    this.FirstDisplayedScrollingRowIndex++;
                }
            }
            else
            {
                if (this.FirstDisplayedScrollingRowIndex > 0)
                {
                    this.FirstDisplayedScrollingRowIndex--;
                }
            }

            scrolling = false;
        }

    }



    protected override void OnMouseUp(System.Windows.Forms.MouseEventArgs e)
    {
        bDragging = false;
        HitTestInfo livehti = hti;
        hti = null;

        if (RowDropped != null
         && livehti != null
         && livehti.RowIndex != -1
         && this.CurrentRow.Index != livehti.RowIndex)
        {
            RowDropped(this, this.CurrentRow, this.Rows[livehti.RowIndex]);
        }
        StopAutoScrolling();

        Invalidate();
        base.OnMouseUp(e);
    }

    protected override void OnCellPainting(System.Windows.Forms.DataGridViewCellPaintingEventArgs e)
    {
        if (bDragging && hti != null && hti.RowIndex != -1
         && e.RowIndex == hti.RowIndex)
        {
            //
            // draw the indicator
            //
            Pen p = new Pen(Color.FromArgb(0, 0, 215));
            p.Width = 4;
            e.Graphics.DrawLine(p, e.CellBounds.Left, e.CellBounds.Top, e.CellBounds.Right, e.CellBounds.Top);
        }

        base.OnCellPainting(e);
    }
}


回答2:

private void TargetReasonGrid_DragOver(object sender, DragEventArgs e)
{
    e.Effect = DragDropEffects.Move;

    //Converts window position to user control position (otherwise you can use MousePosition.Y)
    int mousepos = PointToClient(Cursor.Position).Y;

    //If the mouse is hovering over the bottom 5% of the grid
    if (mousepos > (TargetReasonGrid.Location.Y + (TargetReasonGrid.Height * 0.95)))
    {
        //If the first row displayed isn't the last row in the grid
        if (TargetReasonGrid.FirstDisplayedScrollingRowIndex < TargetReasonGrid.RowCount - 1)
        {
            //Increase the first row displayed index by 1 (scroll down 1 row)
            TargetReasonGrid.FirstDisplayedScrollingRowIndex = TargetReasonGrid.FirstDisplayedScrollingRowIndex + 1;
        }
    }

    //If the mouse is hovering over the top 5% of the grid
    if (mousepos < (TargetReasonGrid.Location.Y + (TargetReasonGrid.Height * 0.05)))
    {
        //If the first row displayed isn't the first row in the grid
        if (TargetReasonGrid.FirstDisplayedScrollingRowIndex > 0)
        {
            //Decrease the first row displayed index by 1 (scroll up 1 row)
            TargetReasonGrid.FirstDisplayedScrollingRowIndex = TargetReasonGrid.FirstDisplayedScrollingRowIndex - 1;
        }
    }
}

Lots of helpful answers here, just thought I'd add a less-complicated solution to this issue. The code above is called when rows are dragged within a DataGridView. Mine is named "TargetReasonGrid".

I added notes that explain what I'm doing, but here's the steps spelled out:

  1. Convert the mouse position so it's relative to grid locations (within your form/control)

  2. Set imaginary regions on the edge of the displayed grid where mouse travel will trigger a scroll

  3. Check to make sure you actually have different rows to scroll to

  4. Scroll in increments of 1 row

Thanks to C4u (commented above), gave me the "imaginary regions" idea.



回答3:

Thanks for the help with my problem, perhaps I can help you with yours.

From this page:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);

//winuser.h constants
private const int WM_VSCROLL = 277; // Vertical scroll
private const int SB_LINEUP = 0;    // Scrolls one line up
private const int SB_LINEDOWN = 1;  // Scrolls one line down
private const int SB_ENDSCROLL = 8; // Ends the scrolling

//Call this when you want to scroll
private void ScrollGridview(int direction)
{
    SendMessage(Handle, WM_VSCROLL, (IntPtr)direction, VerticalScrollBar.Handle);
    SendMessage(Handle, WM_VSCROLL, (IntPtr)SB_ENDSCROLL, VerticalScrollBar.Handle);
}

(The second SendMessage does not seem to be necessary, but I included it for good measure)

I wrote a DataGridView control which incorporates both this gbogumil's autoscroll solution and a correctly-functioning OnPaint highlighting - you can find it here.

I'd like to also point out this control, which I just found from another thread. It looks really nice, but unfortunately it's GPL, so you can only use it for GPL projects. It does all the things we need plus a lot more, though.