Search code examples
datagridviewdrag-and-dropscrollcustom-scrolling

DataGridView Column drag and drop with Auto-horizontal scroll


Can anyone please suggest how should I implement column drag and drop (with auto scroll) feature in DataGridView. I know I can use the controll's AllowUserToDragDrop option. However, since my datagridview control has relatively large number of columns, I need an auto scroll feature which follows the current drag-drop position so that users can see the destination column(s) before dropping. I have implemented the custom drag and drop feature but still I am having problem to enable auto scroll option.


Solution

  • I am using the following class to auto-scroll a TTreeView. The TScroller is created in the Create of the Frame on which it sits, passing in the TreeView. It is destroyed in the frame's Destroy. In the OnDragOver of the TreeView I simply call MyDragScroller.Scroll(State);

    type
      TScroller = class(TObject)
      private
        MyTimer: TTimer;
        FControl: TWinControl;
        FSensitiveSize: Integer;
      protected
        procedure HandleTimer(Sender: TObject);
      public
        constructor Create(aControl: TWinControl);
        destructor Destroy; override;
    
        procedure Scroll(const aState: TDragState);
      end;
    
    implementation
    
    { TScroller }
    
    constructor TScroller.Create(aControl: TWinControl);
    begin
      inherited Create;
      MyTimer := TTimer.Create(nil);
      MyTimer.Enabled := False;
      MyTimer.Interval := 20; // Not too short, otherwise scrolling flashes by.
      MyTimer.OnTimer := HandleTimer;
    
      FControl := aControl;
      // Width/Height from edge of FControl within which the mouse has to be for
      // automatic scrolling to occur. By default it is the width of a vertical scrollbar.
      FSensitiveSize := GetSystemMetrics(SM_CXVSCROLL);
    end;
    
    destructor TScroller.Destroy;
    begin
      FreeAndNil(MyTimer);
      FControl := nil;
      inherited;
    end;
    
    procedure TScroller.HandleTimer(Sender: TObject);
    var
      MousePos: TPoint;
      MouseX: Integer;
      MouseY: Integer;
    
      function _MouseInSensitiveSize: Boolean;
      begin
    
        MousePos := FControl.ScreenToClient(Mouse.CursorPos);
        MouseY := MousePos.Y;
        MouseX := MousePos.X;
    
        Result :=
             ((MouseY >= 0) and (MouseY < FSensitiveSize))
          or ((MouseY > FControl.ClientHeight - FSensitiveSize) and (MouseY <= FControl.ClientHeight))
          or ((MouseX >= 0) and (MouseX < FSensitiveSize))
          or ((MouseX > FControl.ClientWidth - FSensitiveSize) and (MouseX <= FControl.ClientWidth))
        ;
    
      end;
    begin
      if Mouse.IsDragging and _MouseInSensitiveSize then begin
        if MouseY < FSensitiveSize then begin
          FControl.Perform(WM_VSCROLL, SB_LINEUP, 0);
        end else if MouseY > FControl.ClientHeight - FSensitiveSize then begin
          FControl.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
        end;
    
        if MouseX < FSensitiveSize then begin
          FControl.Perform(WM_HSCROLL, SB_LINELEFT, 0);
        end else if MouseX > FControl.ClientWidth - FSensitiveSize then begin
          FControl.Perform(WM_HSCROLL, SB_LINERIGHT, 0);
        end;
      end else begin
        MyTimer.Enabled := False;
      end;
    end;
    
    procedure TScroller.Scroll(const aState: TDragState);
    begin
      if not Mouse.IsDragging then Exit;  // Only scroll while dragging.
      if not (aState in [dsDragMove]) then Exit; // No use scrolling on a dsDragLeave and not nice to do so on a dsDragEnter.
    
      MyTimer.Enabled := True;
    end;
    

    Notes: If you have more controls that need auto-scrolling, you would need to create a TScroller per control. In that case it would probably do the performance of your app a lot of good to use some sort of observer/observed mechanism to share a timer between all scrolling controls.