Search code examples
delphidelphi-7

TStringGrid does not refresh when calling InvalidateCol()


I want to make a string grid to display some kind of vertical cursor to highlight the current selected column. So, in MouseDown I call setCurPos, then I call InvalidateCol to invalidate current column. This calls the DrawCell. DrawCell paints the cursor on current column.

The problem is this: if I have more rows in grid then it can display some of them will not be visible (of course) so grid's vertical scroll bar will automatically appear. When I scroll down to see the rows at the bottom of the grid, the cursor is not painted in these rows. It looks like the number of bottom rows (now visible on screen) in which the cursor is NOT painted is proportional with the number of invisible rows in the top of the grid.

If I minimize and restore the application, the cursor is nicely painted. So, obviously the invalidateColumn() is not working.

procedure TmyGrid.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
VAR aCol, aRow: Integer;
begin
 MouseToCell(X, Y, ACol, ARow);
 ...                                                                  
    inherited MouseDown(Button, Shift, X, Y); 
    CursorPosFocus:= ACol;                          
end;


procedure TmyGrid.setCurPos(CONST NewColumn: Integer);                 
VAR OldPos: Integer;
begin
 ...
 OldPos:= CursorPos;
 FCursorPos:= NewColumn;    
 ...
 //- This is not working:
 //InvalidateCol(OldPos);
 //InvalidateCol(NewColumn);    
 //Update;

 //- THIS WORKS:
 InvalidateGrid; 
end;


procedure TmyGrid.DrawCell(ACol, ARow: integer; ARect: TRect; AState: TGridDrawState);
Var TempRect: TRect;
begin
 inherited;
  ...

 {DRAW CURSOR}
 if CursorPos= ACol then
  begin
   TempRect.Top   := 0;
   TempRect.Left  := ARect.Left;
   TempRect.Right := ARect.Right;
   TempRect.Bottom:= ClientHeight-2;     
   Frame3D(Canvas, TempRect, $909090, $808080, 1);       
  end;
end;

Delphi 7, Win XP


Solution

  • You are doing nothing wrong, you just got caught by a bug in the VCL grid implementation that has been in the Delphi 4 VCL (I don't have any earlier CD here to check, but it might even have been in the 16 bit Delphi VCL already) and is still with us in Delphi 2009.

    Both methods to invalidate a whole row or column do this by calculating an area of cells that is passed to the internal InvalidateRect() method. This area always starts with the column / row 0, and extends to the first completely invisible row / column. It's quite obvious that this will only ever work correctly for an unscrolled client area. What the code should do instead is invalidate to the last column / row, and let the code in the InvalidateRect() helper figure out which cells are indeed visible and calculate the client area that needs to be invalidated from that.

    Since you are writing your own class you could easily implement your own methods to invalidate the correct range of cells; I did the same many years ago, together with more methods to invalidate multiple columns, multiple rows and whole blocks of cells. Since InvalidateRect() is private (great thinking there too) you need to use the Windows API function with the same name, and calculate the rectangle to be invalidated using either the CellRect() or BoxRect() method.

    While InvalidateGrid() does work for you it is really kind of a sledge hammer - it invalidates the whole grid, which I think is not what you wanted when you started using InvalidateCol().

    For your experiments you should make paint cycles for each cell easily visible. Causing the background colour of the cells to change with each update is a simple way to check that you really only do minimum screen redraws. Something like

    StringGrid1.Canvas.Brush.Color := RGB(Random(256), Random(256), Random(256));
    StringGrid1.Canvas.FillRect(Rect);
    

    in the OnDrawCell event handler works fine.