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.