Search code examples
delphidelphi-xe7

I have flickering on my custom control. What is causing it and how can I eliminate it?


Introduction

I am writing a custom control that is derived from a TScrollBox but I am having a few difficulties overcoming what seems like should be an easy enough problem to solve.

The control will be used to display a caption bar at the top which will be static (ie, never moves when the scrollbox is scrolled), and then underneath the caption bar I will be drawing some values within there own columns such as row numbers etc.

This is what the control currently looks like to give a better idea (very much a early work in progress):

enter image description here

Flicker Problem

The problem I am facing is flickering and I do not see an easy way to eliminate it. I have a feeling the flickering is been caused because I am trying to draw underneath my caption bar and when the flickering occurs you can actually see the values been drawing underneath the caption bar, although my assumption could be completely wrong.

All the drawing is done on a TGraphicControl which is child to the scrollbox, the flickering occurs a lot when scrolling fast, when using the scrollbar buttons it still flickers but not as frequently.

I am unable to catch the flickering and show as an image here, but with the code below you can build and install into a new package and test for yourself:

unit MyGrid;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  System.Classes,
  System.SysUtils,
  Vcl.Controls,
  Vcl.Dialogs,
  Vcl.Forms,
  Vcl.Graphics;

type
  TMyCustomGrid = class(TGraphicControl)
  private
    FFont: TFont;
    FRowNumbers: TStringList;
    FRowCount: Integer;
    FCaptionBarRect: TRect;
    FRowNumbersBackgroundRect: TRect;
    FValuesBackgroundRect: TRect;

    procedure CalculateNewHeight;
    function GetMousePosition: TPoint;
    function RowIndexToMousePosition(ARowIndex: Integer): Integer;
    function GetRowHeight: Integer;
    function RowExists(ARowIndex: Integer): Boolean;
    function GetRowNumberRect(ARowIndex: Integer): TRect;
    function GetRowNumberTextRect(ARowIndex: Integer): TRect;
    function GetValueRect(ARowIndex: Integer): TRect;
    function GetValueTextRect(ARowIndex: Integer): TRect;
    function GetFirstVisibleRow: Integer;
    function GetLastVisibleRow: Integer;
  protected
    procedure Paint; override;

    procedure DrawCaptionBar;
    procedure DrawRowNumbers;
    procedure DrawValues;
    procedure DrawColumnLines;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

  TMyGrid = class(TScrollBox)
  private
    FGrid: TMyCustomGrid;
  protected
    procedure Loaded; override;
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

const
  FCaptionBarHeight = 20;
  FRowNumbersWidth  = 85;
  FValuesWidth      = 175;
  FTextSpacing      = 5;

implementation

constructor TMyCustomGrid.Create(AOwner: TComponent);
var
  I: Integer;
begin
  inherited Create(AOwner);

  FFont        := TFont.Create;
  FFont.Color  := clBlack;
  FFont.Name   := 'Tahoma';
  FFont.Size   := 10;
  FFont.Style  := [];

  FRowNumbers := TStringList.Create;

  //FOR TEST PURPOSES
  for I := 0 to 1000 do
  begin
    FRowNumbers.Add(IntToStr(I));
  end;

  Canvas.Font.Assign(FFont);
end;

destructor TMyCustomGrid.Destroy;
begin
  FFont.Free;
  FRowNumbers.Free;
  inherited Destroy;
end;

procedure TMyCustomGrid.Paint;
begin
  FCaptionBarRect := Rect(0, 0, Self.Width, GetRowHeight + TMyGrid(Self.Parent).VertScrollBar.Position + 2);
  FRowCount       := FRowNumbers.Count;

  DrawRowNumbers;
  DrawValues;
  DrawCaptionBar;
  DrawColumnLines;
end;

procedure TMyCustomGrid.DrawCaptionBar;
var
  R: TRect;
  S: string;
begin
  {background}
  Canvas.Brush.Color  := clSkyBlue;
  Canvas.Brush.Style  := bsSolid;
  Canvas.FillRect(FCaptionBarRect);

  {text}
  Canvas.Brush.Style := bsClear;
  R := Rect(FTextSpacing, FCaptionBarRect.Top + TMyGrid(Self.Parent).VertScrollBar.Position, FRowNumbersWidth - FTextSpacing, FCaptionBarRect.Bottom);
  S := 'Row No.';
  DrawText(Canvas.Handle, PChar(S), Length(S), R, DT_LEFT or DT_SINGLELINE or DT_VCENTER);

  R := Rect(FTextSpacing + FRowNumbersWidth, FCaptionBarRect.Top + TMyGrid(Self.Parent).VertScrollBar.Position, FValuesWidth - FTextSpacing, FCaptionBarRect.Bottom);
  S := 'Item No.';
  DrawText(Canvas.Handle, PChar(S), Length(S), R, DT_LEFT or DT_SINGLELINE or DT_VCENTER);
end;

procedure TMyCustomGrid.DrawRowNumbers;
var
  I, Y: Integer;
  R: TRect;
  S: string;
begin
  {background}
  FRowNumbersBackgroundRect := Rect(0, FCaptionBarRect.Bottom, FRowNumbersWidth, FCaptionBarRect.Height + GetLastVisibleRow * GetRowHeight + 1);
  Canvas.Brush.Color := clCream;
  Canvas.Brush.Style := bsSolid;
  Canvas.FillRect(FRowNumbersBackgroundRect);

  {text}
  Y := 0;

  // a bit of optimization here, instead of iterating every item in FRowNumbers
  // which would be slow - instead determine the the top and last visible row
  // and paint only that area.
  for I := GetFirstVisibleRow to GetLastVisibleRow do
  begin
    if RowExists(I) then
    begin
      R := GetRowNumberTextRect(I);
      S := FRowNumbers.Strings[I];
      DrawText(Canvas.Handle, PChar(S), Length(S), R, DT_LEFT or DT_SINGLELINE or DT_VCENTER);
      Inc(Y, GetRowHeight);
    end;
  end;
end;

procedure TMyCustomGrid.DrawValues;
var
  I, Y: Integer;
  R: TRect;
  S: string;
begin
  {background}
  FValuesBackgroundRect := Rect(FRowNumbersBackgroundRect.Width, FCaptionBarRect.Bottom, FValuesWidth + FRowNumbersWidth, FCaptionBarRect.Height + GetLastVisibleRow * GetRowHeight + 1);
  Canvas.Brush.Color    := clMoneyGreen;
  Canvas.Brush.Style    := bsSolid;
  Canvas.FillRect(FValuesBackgroundRect);

  {text}
  Y := 0;

  // a bit of optimization here, instead of iterating every item in FRowNumbers
  // which would be slow - instead determine the the top and last visible row
  // and paint only that area.
  for I := GetFirstVisibleRow to GetLastVisibleRow do
  begin
    if RowExists(I) then
    begin
      R := GetValueTextRect(I);
      S := 'This is item number ' + FRowNumbers.Strings[I];
      DrawText(Canvas.Handle, PChar(S), Length(S), R, DT_LEFT or DT_SINGLELINE or DT_VCENTER);
      Inc(Y, GetRowHeight);
    end;
  end;
end;

procedure TMyCustomGrid.DrawColumnLines;
begin
  Canvas.Brush.Style  := bsClear;
  Canvas.Pen.Color    := clBlack;

  {row numbers column}
  Canvas.MoveTo(FRowNumbersBackgroundRect.Right, FCaptionBarRect.Top);
  Canvas.LineTo(FRowNumbersBackgroundRect.Right, FCaptionBarRect.Height + GetLastVisibleRow * GetRowHeight + 1);

  {values column}
  Canvas.MoveTo(FValuesBackgroundRect.Right, FCaptionBarRect.Top);
  Canvas.LineTo(FValuesBackgroundRect.Right, FCaptionBarRect.Height + GetLastVisibleRow * GetRowHeight + 1);
end;

procedure TMyCustomGrid.CalculateNewHeight;
var
  I, Y: Integer;
begin
  FRowCount := FRowNumbers.Count;

  Y := 0;
  for I := 0 to FRowCount -1 do
  begin
    Inc(Y, GetRowHeight);
  end;

  if Self.Height <> Y then
    Self.Height := Y + FCaptionBarHeight + 1;
end;

function TMyCustomGrid.GetMousePosition: TPoint;
var
  P: TPoint;
begin
  Winapi.Windows.GetCursorPos(P);
  Winapi.Windows.ScreenToClient(Self.Parent.Handle, P);
  Result := P;
end;

function TMyCustomGrid.RowIndexToMousePosition(
  ARowIndex: Integer): Integer;
begin
  if RowExists(ARowIndex) then
    Result := ARowIndex * GetRowHeight;
end;

function TMyCustomGrid.GetRowHeight: Integer;
begin
  Result := 18;
end;

function TMyCustomGrid.RowExists(ARowIndex: Integer): Boolean;
var
  I: Integer;
  Y: Integer;
begin
  Result := False;

  Y := 0;
  for I := GetFirstVisibleRow to GetLastVisibleRow -1 do
  begin
    if ARowIndex = I then
    begin
      Result := True;
      Break;
    end;

    Inc(Y, GetRowHeight);
  end;
end;

function TMyCustomGrid.GetRowNumberRect(ARowIndex: Integer): TRect;
begin
  Result.Bottom := RowIndexToMousePosition(ARowIndex) + FCaptionBarHeight + GetRowHeight;
  Result.Left   := 0;
  Result.Right  := FRowNumbersWidth;
  Result.Top    := RowIndexToMousePosition(ARowIndex) + FCaptionBarHeight + 1;
end;

function TMyCustomGrid.GetRowNumberTextRect(ARowIndex: Integer): TRect;
begin
  Result := GetRowNumberRect(ARowIndex);
  Result.Inflate(-FTextSpacing, 0);
end;

function TMyCustomGrid.GetValueRect(ARowIndex: Integer): TRect;
begin
  Result.Bottom := RowIndexToMousePosition(ARowIndex) + FCaptionBarHeight + GetRowHeight;
  Result.Left   := FRowNumbersWidth;
  Result.Right  := FValuesBackgroundRect.Right;
  Result.Top    := RowIndexToMousePosition(ARowIndex) + FCaptionBarHeight + 1;
end;

function TMyCustomGrid.GetValueTextRect(ARowIndex: Integer): TRect;
begin
  Result := GetValueRect(ARowIndex);
  Result.Inflate(-FTextSpacing, 0);
end;

function TMyCustomGrid.GetFirstVisibleRow: Integer;
begin
  Result := TMyGrid(Self.Parent).VertScrollBar.Position div GetRowHeight;
end;

function TMyCustomGrid.GetLastVisibleRow: Integer;
begin
  Result := GetFirstVisibleRow + TMyGrid(Self.Parent).Height div GetRowHeight -1;
end;

constructor TMyGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  Self.DoubleBuffered           := True;
  Self.Height                   := 150;
  Self.HorzScrollBar.Visible    := False;
  Self.TabStop                  := True;
  Self.Width                    := 250;

  FGrid                         := TMyCustomGrid.Create(Self);
  FGrid.Align                   := alTop;
  FGrid.Parent                  := Self;
  FGrid.CalculateNewHeight;

  Self.VertScrollBar.Smooth     := False;
  Self.VertScrollBar.Increment  := FGrid.GetRowHeight;
  Self.VertScrollBar.Tracking   := True;
end;

destructor TMyGrid.Destroy;
begin
  FGrid.Free;
  inherited Destroy;
end;

procedure TMyGrid.Loaded;
begin
  inherited Loaded;
  Self.VertScrollBar.Range := FGrid.Height - FGrid.FCaptionBarRect.Height;
end;

procedure TMyGrid.WMVScroll(var Message: TWMVScroll);
begin
  inherited;
  Self.Invalidate;
end;

end.

Question

What should I be doing differently to overcome the flicker?

Setting DoubleBuffered to True for the scrollbox makes little difference here it seems. I experimented a little with the WM_ERASEBACKGROUND message which just made the scrollbox black.

I also tried implementing a canvas onto the scrollbox and drawing my caption bar directly onto it then setting the padding on the scrollbox to the height of my caption bar and drawing the rest on my TGraphicControl but this lead to even worse flickering. At this point I don't know what exactly is causing the flickering and how to eliminate it?

One last thing is how can I make the scrollbar scroll at a set increment when using the scrollbar thumb? I have set the vertical scrollbar increment to the equivalent of the row height and this works when pressing the scrollbar button, when using the scrollbar thumb to scroll up and down it is not at a fixed increment. I am trying to get the scrollbar to work in increments not scroll loosely.


Solution

  • A quick fix is to replace Self.Invalidate with FGrid.Repaint (or .Update or .Refresh) in TMyGrid.WMVScroll. You will see this eliminates flickering, but it still also demonstrates problems with multiple caption bars drawn when you drag the scroll bar thumb. Explanation: Invalidate puts a repaint request in the message queue, which is postponed until the queue is empty and thus won't be handled right away, i.e. not when you want to. Repaint on the other hand is performed immediately. But normally Invalidate should be sufficient...

    The main source of your problem lies in the layout with the 'sticky' header (or caption bar) within the client space. Every windowed control with a TControlScrollBar uses ScrollWindow internally which 'moves' your caption bar up and down, depending on scroll direction. You could prevent that with some hacking, but from a design point of view it is also much more nice when the scroll bar starts below the header.

    You then have a few options for the internal layout of your component:

    • Use an alTop aligned PaintBox for the header, an alRight aligned ScrollBar and an alClient aligned PaintBox for the grid. This is what Sertac commented and requires 3 controls within your component.
    • Use an alTop aligned PaintBox for the header, an alClient aligned ScrollBox and therein an alTop aligned PaintBox for the grid. This design has nested controls.
    • Use a TScrollingWinControl with an added non-client border on the top for the header and an alTop aligned PaintBox for the grid. This component contains 1 control.
    • Use a TScrollingWinControl with an added non-client border on the top for the header and draw the grid in its PaintWindow method. This design requires no extra controls at all.
    • ...

    As an example, hereby an implementation of the third option:

    unit MyGrid;
    
    interface
    
    uses
      System.Classes, System.SysUtils, Winapi.Windows, Winapi.Messages,
      Vcl.Controls, Vcl.Forms, Vcl.Graphics, Vcl.ExtCtrls, System.Math,
      System.UITypes;
    
    type
      TMyCustomGrid = class(TScrollingWinControl)
      private const
        DefHeaderHeight = 20;
        DefRowHeight = 18;
        HeaderColor = clSkyBLue;
        RowIdColCaption = 'Row no.';
        RowIdColWidth = 85;
        RowIdColColor = clCream;
        TextSpacing = 5;
        ValueColCaption = 'Item no.';
        ValueColWidth = 175;
        ValueColColor = clMoneyGreen;
      private
        FHeaderHeight: Integer;
        FPainter: TPaintBox;
        FRowHeight: Integer;
        FRows: TStrings;
        function GetRowCount: Integer;
        procedure PainterPaint(Sender: TObject);
        procedure RowsChanged(Sender: TObject);
        procedure SetHeaderHeight(Value: Integer);
        procedure SetRowHeight(Value: Integer);
        procedure SetRows(Value: TStrings);
        procedure UpdatePainter;
        procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
        procedure WMNCCalcSize(var Message: TWMNCCalcSize); message WM_NCCALCSIZE;
        procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
        procedure WMVScroll(var Message: TWMScroll); message WM_VSCROLL;
      protected
        function CanResize(var NewWidth, NewHeight: Integer): Boolean; override;
        procedure Click; override;
        procedure CreateParams(var Params: TCreateParams); override;
        function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
          MousePos: TPoint): Boolean; override;
        procedure PaintWindow(DC: HDC); override;
        property AutoScroll default True;
        property HeaderHeight: Integer read FHeaderHeight write SetHeaderHeight
          default DefHeaderHeight;
        property RowCount: Integer read GetRowCount;
        property RowHeight: Integer read FRowHeight write SetRowHeight
          default DefRowHeight;
        property Rows: TStrings read FRows write SetRows;
      public
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
      end;
    
      TMyGrid = class(TMyCustomGrid)
      public
        procedure Test;
      published
        property AutoScroll;
        property HeaderHeight;
        property RowHeight;
      end;
    
    implementation
    
    function Round(Value, Rounder: Integer): Integer; overload;
    begin
      if Rounder = 0 then
        Result := Value
      else
        Result := (Value div Rounder) * Rounder;
    end;
    
    { TMyCustomGrid }
    
    function TMyCustomGrid.CanResize(var NewWidth, NewHeight: Integer): Boolean;
    begin
      Result := inherited CanResize(NewWidth, NewHeight);
      NewHeight := FHeaderHeight + Round(NewHeight - FHeaderHeight, FRowHeight);
    end;
    
    procedure TMyCustomGrid.Click;
    begin
      inherited Click;
      SetFocus;
    end;
    
    constructor TMyCustomGrid.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      ControlStyle := [csCaptureMouse, csClickEvents, csOpaque, csDoubleClicks];
      AutoScroll := True;
      TabStop := True;
      VertScrollBar.Tracking := True;
      VertScrollBar.Increment := DefRowHeight;
      Font.Name := 'Tahoma';
      Font.Size := 10;
      FHeaderHeight := DefHeaderHeight;
      FRowHeight := DefRowHeight;
      FPainter := TPaintBox.Create(Self);
      FPainter.ControlStyle := [csOpaque, csNoStdEvents];
      FPainter.Enabled := False;
      FPainter.Align := alTop;
      FPainter.OnPaint := PainterPaint;
      FPainter.Parent := Self;
      FRows := TStringList.Create;
      TStringList(FRows).OnChange := RowsChanged;
      UpdatePainter;
    end;
    
    procedure TMyCustomGrid.CreateParams(var Params: TCreateParams);
    begin
      inherited CreateParams(Params);
      with Params.WindowClass do
        Style := Style and not (CS_HREDRAW or CS_VREDRAW);
    end;
    
    destructor TMyCustomGrid.Destroy;
    begin
      FRows.Free;
      inherited Destroy;
    end;
    
    function TMyCustomGrid.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
      MousePos: TPoint): Boolean;
    var
      Delta: Integer;
    begin
      with VertScrollBar do
      begin
        Delta := Increment * Mouse.WheelScrollLines;
        if WheelDelta > 0 then
          Delta := -Delta;
        Position := Min(Round(Range - ClientHeight, Increment), Position + Delta);
      end;
      Result := True;
    end;
    
    function TMyCustomGrid.GetRowCount: Integer;
    begin
      Result := FRows.Count;
    end;
    
    procedure TMyCustomGrid.PainterPaint(Sender: TObject);
    const
      TextFlags = DT_LEFT or DT_SINGLELINE or DT_VCENTER or DT_NOPREFIX;
    var
      C: TCanvas;
      FromIndex: Integer;
      ToIndex: Integer;
      I: Integer;
      BackRect: TRect;
      TxtRect: TRect;
    begin
      C := FPainter.Canvas;
      FromIndex := (C.ClipRect.Top) div FRowHeight;
      ToIndex := Min((C.ClipRect.Bottom) div FRowHeight, RowCount - 1);
      for I := FromIndex to ToIndex do
      begin
        BackRect := Bounds(0, I * FRowHeight, RowIdColWidth, FRowHeight);
        TxtRect := BackRect;
        TxtRect.Inflate(-TextSpacing, 0);
        C.Brush.Color := RowIdColColor;
        C.FillRect(BackRect);
        DrawText(C.Handle, FRows.Names[I], -1, TxtRect, TextFlags);
        BackRect.Left := RowIdColWidth;
        BackRect.Width := ValueColWidth;
        Inc(TxtRect.Left, RowIdColWidth);
        Inc(TxtRect.Right, ValueColWidth);
        C.Brush.Color := ValueColColor;
        C.FillRect(BackRect);
        DrawText(C.Handle, FRows.ValueFromIndex[I], -1, TxtRect, TextFlags);
        C.MoveTo(BackRect.Left, BackRect.Top);
        C.LineTo(BackRect.Left, BackRect.Bottom);
        BackRect.Offset(ValueColWidth, 0);
        C.Brush.Color := Brush.Color;
        C.FillRect(BackRect);
        C.MoveTo(BackRect.Left, BackRect.Top);
        C.LineTo(BackRect.Left, BackRect.Bottom);
      end;
    end;
    
    procedure TMyCustomGrid.PaintWindow(DC: HDC);
    begin
      if FPainter.Height < ClientHeight then
      begin
        ExcludeClipRect(DC, 0, 0, ClientWidth, FPainter.Height);
        FillRect(DC, ClientRect, Brush.Handle);
      end;
    end;
    
    procedure TMyCustomGrid.RowsChanged(Sender: TObject);
    begin
      UpdatePainter;
    end;
    
    procedure TMyCustomGrid.SetHeaderHeight(Value: Integer);
    begin
      if FHeaderHeight <> Value then
      begin
        FHeaderHeight := Value;
        RecreateWnd;
      end;
    end;
    
    procedure TMyCustomGrid.SetRowHeight(Value: Integer);
    begin
      if FRowHeight <> Value then
      begin
        FRowHeight := Value;
        VertScrollBar.Increment := FRowHeight;
        UpdatePainter;
        Invalidate;
      end;
    end;
    
    procedure TMyCustomGrid.SetRows(Value: TStrings);
    begin
      FRows.Assign(Value);
    end;
    
    procedure TMyCustomGrid.UpdatePainter;
    begin
      FPainter.Height := RowCount * FRowHeight;
    end;
    
    procedure TMyCustomGrid.WMEraseBkgnd(var Message: TWMEraseBkgnd);
    begin
      Message.Result := 1;
    end;
    
    procedure TMyCustomGrid.WMNCCalcSize(var Message: TWMNCCalcSize);
    begin
      inherited;
      Inc(Message.CalcSize_Params.rgrc0.Top, HeaderHeight);
    end;
    
    procedure TMyCustomGrid.WMNCPaint(var Message: TWMNCPaint);
    const
      TextFlags = DT_LEFT or DT_SINGLELINE or DT_VCENTER or DT_NOPREFIX;
    var
      DC: HDC;
      OldFont: HFONT;
      Brush: HBRUSH;
      R: TRect;
    begin
      DC := GetWindowDC(Handle);
      OldFont := SelectObject(DC, Font.Handle);
      Brush := CreateSolidBrush(ColorToRGB(HeaderColor));
      try
        FillRect(DC, Rect(0, 0, Width, FHeaderHeight), Brush);
        SetBkColor(DC, ColorToRGB(HeaderColor));
        SetRect(R, TextSpacing, 0, RowIdColWidth - TextSpacing, FHeaderHeight);
        DrawText(DC, RowIdColCaption, -1, R, TextFlags);
        Inc(R.Left, RowIdColWidth);
        Inc(R.Right, ValueColWidth);
        DrawText(DC, ValueColCaption, -1, R, TextFlags);
        MoveToEx(DC, RowIdColWidth, 0, nil);
        LineTo(DC, RowIdColWidth, FHeaderHeight);
        MoveToEx(DC, RowIdColWidth + ValueColWidth, 0, nil);
        LineTo(DC, RowIdColWidth + ValueColWidth, FHeaderHeight);
      finally
        SelectObject(DC, OldFont);
        DeleteObject(Brush);
        ReleaseDC(Handle, DC);
      end;
      inherited;
    end;
    
    procedure TMyCustomGrid.WMVScroll(var Message: TWMScroll);
    begin
      Message.Pos := Round(Message.Pos, FRowHeight);
      inherited;
    end;
    
    { TMyGrid }
    
    procedure TMyGrid.Test;
    var
      I: Integer;
    begin
      for I := 0 to 40 do
        Rows.Add(Format('%d=This is item number %d', [I, I]));
    end;
    
    end.
    

    Some general comments regarding your code:

    • Your ancestor TMyCustomGrid cannot be without your descendent TMyGrid which is normally a no-no. The code TMyGrid(Self.Parent).VertScrollBar.Position is equal to -Top by the way, which eliminates the need for knowledge of its descendent.
    • There is no need to create a Font. TControl has a Font already, just publish it.
    • Unless you want the border-options from TScrollBox, it generally speaking is better to descend from - in this case - TScrollingWinControl because only then you have control over which properties should be published.

    One last thing is how can I make the scrollbar scroll at a set increment when using the scrollbar thumb?

    By adjusting the scroll position in WM_VSCROLL as done in the code above:

    procedure TMyCustomGrid.WMVScroll(var Message: TWMScroll);
    begin
      if FRowHeight <> 0 then
        Message.Pos := (Message.Pos div FRowHeight) * FRowHeight;
      inherited;
    end;