Delphi’s T*Grid components have an annoying little feature whereby they will scroll the cell into view if you click on a partially visible cell at the right or the bottom of the window. Then, this couples with a timer that causes the scroll to continue as long as the mouse button is held down and the cell it is over is partially visible. This typically means that if a user clicks on a partially visible cell, they end up selecting a cell several rows or columns away from where they intended to click.
In my view, this is a bug that should be fixed in Delphi. I’m not the only person who thinks this. I’ve reported it to Embarcadero at RSP-18542.
In the meantime, here’s a little unit that works around the issue.
{
Stop scroll on mousedown on bottom row of grid when bottom row
is a partial cell: have to block both initial scroll and timer-
based scroll.
This code is pretty dependent on the implementation in Vcl.Grids.pas,
so it should be checked if we upgrade to new version of Delphi.
}
{$IFNDEF VER320}
{$MESSAGE ERROR 'Check that this fix is still applicable for a new version of Delphi. Checked against Delphi 10.2' }
{$ENDIF}
unit ScrollFixedStringGrid;
interface
uses
System.Classes,
Vcl.Controls,
Vcl.Grids,
Winapi.Windows;
type
TScrollFixedStringGrid = class(TStringGrid)
private
TimerStarted: Boolean;
HackedMousedown: Boolean;
protected
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
function SelectCell(ACol, ARow: Longint): Boolean; override;
end;
implementation
{ TScrollFixedStringGrid }
procedure TScrollFixedStringGrid.MouseDown(Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
// When we first mouse-down, we know the grid has
// no active scroll timer
TimerStarted := False;
// Call the inherited event, blocking the default MoveCurrent
// behaviour that scrolls the cell into view
HackedMouseDown := True;
try
inherited;
finally
HackedMouseDown := False;
end;
// Cancel scrolling timer started by the mousedown event for selecting
if FGridState = gsSelecting then
KillTimer(Handle, 1);
end;
procedure TScrollFixedStringGrid.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
// Start the scroll timer if we are selecting and mouse
// button is down, on our first movement with mouse down
if not TimerStarted and (FGridState = gsSelecting) then
begin
SetTimer(Handle, 1, 60, nil);
TimerStarted := True;
end;
inherited;
end;
function TScrollFixedStringGrid.SelectCell(ACol, ARow: Longint): Boolean;
begin
Result := inherited;
if Result and HackedMousedown then
begin
// MoveColRow calls MoveCurrent, which
// calls SelectCell. If SelectCell returns False, then
// movement is blocked. But we fake it by re-calling with Show=False
// to get the behaviour we want
HackedMouseDown := False;
try
MoveColRow(ACol, ARow, True, False);
finally
HackedMouseDown := True;
end;
Result := False;
end;
end;
end.
To display the partially visible cell to be selected in its entirety, override MouseUp with:
Procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: INTEGER); override;
and
Procedure TScrollFixedStringGrid.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: INTEGER);
var
i,ARow,ACol: integer;
begin
inherited MouseUp(Button, Shift, X, Y);
//# Its a hack to scroll selection full in view
MouseToCell(X,Y,ACol,ARow);
If TimerStarted and (FGridState = gsNormal) then
begin
i := ACol-1;
If i >= FixedCols then
Col := i;
Col := ACol;
end;
end;
Thank you for the comment. In my case, I am trying to do the opposite — prevent the scroll from happening on a mousedown (mouseup is too late, the scroll has already happened).