在 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 控件中的行时显示不同的颜色

移动行时 ListView contentY 发生变化

winform如何从DataGridView中从右键菜单获取一行数据

C# DataGridView 数据显示到最后一行后,如何使滚动条继续向下滚动。

winform中dataGridView上怎么修改、保存数据啊,急用啊?

跪求c#datagridview的一些用法!具体如下