4

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.

Craig
  • 1,774
  • 9
  • 37
  • 3
    It looks like you'd better use a header, a scroll bar and a painting surface instead of a scroll box. – Sertac Akyuz Sep 24 '15 at 23:44
  • @SertacAkyuz Thanks for the suggestion, although I am looking to completely custom draw everything myself I will see what the Header control is capable of - I have never actually used it before. – Craig Sep 25 '15 at 13:53
  • I don't see any reason for you to use double buffering. Typically that's the wrong solution to a problem with flickering. Resist the temptation to do that and fix the code properly. A major redesign will be needed. – David Heffernan Sep 26 '15 at 08:12
  • @DavidHeffernan you are absolutely right, in fact `DoubleBuffering` can be known to cause other graphical glitches and problems etc so normally it is best avoided as it only hides the real problems which you know. This was just one of those things I tried to see how it would behave. The main problem I have, and you know it yourself is to basically start again but I just can't figure out the best approach of doing this. Others have mentioned the use of VCL headers/panels etc but I must custom draw the whole thing myself and so would rather avoid having child controls. – Craig Sep 26 '15 at 11:05
  • I think that avoiding child controls is probably the key – David Heffernan Sep 26 '15 at 11:08

4 Answers4

4

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;
Community
  • 1
  • 1
NGLN
  • 41,230
  • 8
  • 102
  • 186
  • 2
    Thanks for your input, I always believed that `Repaint` for example was incorrect and that `Invalidate` was the correct use but you explained why in your answer so that's something new I have learned. I did have `FGrid.Invalidate` instead of `Self.Invalidate` but they both did the same so I stuck with `Self.Invalidate`. Generally speaking is my approach of using a `TGraphicControl` for the drawing and then putting it inside a container such as a `TScrollBox` the correct way, or would you say it is best to draw directly onto the `TScrollBox` for example? – Craig Sep 24 '15 at 22:57
  • 1
    Personally, I'd create the control from `TCustomControl`. – Andreas Rejbrand Sep 25 '15 at 08:30
  • 1
    Thank you very much for your help, this certainly gives me a lot of food for thought and some new techniques I will use when needed. In terms of what option I go to I am unsure as of yet, I do know I want the scrollbar to be part of the caption as underneath it will look out of place with the way I want to do my drawing. I underestimated how tricky doing this would be, whenever I run into scrollbox type custom controls more often than not I find some quirks with the way things behave. There is enough information here for me though to sit down and have a rethink of what I want to do next. – Craig Sep 26 '15 at 21:43
0

When you repaint, you repaint row by row. This has the effect of blanking out the first row then redrawing it, then the second row, and some on, which gives the flickering effect. More pleasing to the eye is to paint the entire rectangle in the background colour first. Otherwise you might want to think about implementing and using InvalidateRect instead.

Dsm
  • 5,700
  • 18
  • 24
  • I had considered `InvalidateRect` I just never got round to trying it, I believe the flickering problem is caused by my code and the way I am trying to keep the caption bar static and drawing the rest underneath it, I just can't seem to figure out a more elegant way of doing this. – Craig Sep 25 '15 at 14:01
  • Probably best way is to put the title bar in a panel and the rest of it in the TScrollBox - I realise that this is a bit far down the line to do this. The thing is somebody else (at Microsoft) has already gone through this pain, so it is not a bad idea to take advantage... – Dsm Sep 25 '15 at 16:35
0

The problem is you paint directly to your canvas. Draw your content to a bitmap then the draw that onto your canvas: Here is a modified version of your component:

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;
    FBuffer: TBitmap;
    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 Resize; override;
    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);
  FBuffer := TBitmap.Create;

  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;

  FBuffer.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;

  // Draw the bitmap onto the canvas
  Canvas.Draw(0, 0, FBuffer);
end;

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

  { text }
  FBuffer.Canvas.Brush.Style := bsClear;
  R := Rect(FTextSpacing, FCaptionBarRect.Top + TMyGrid(Self.Parent).VertScrollBar.Position, FRowNumbersWidth - FTextSpacing, FCaptionBarRect.Bottom);
  S := 'Row No.';
  DrawText(FBuffer.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(FBuffer.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);
  FBuffer.Canvas.Brush.Color := clCream;
  FBuffer.Canvas.Brush.Style := bsSolid;
  FBuffer.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(FBuffer.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);
  FBuffer.Canvas.Brush.Color := clMoneyGreen;
  FBuffer.Canvas.Brush.Style := bsSolid;
  FBuffer.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(FBuffer.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
  FBuffer.Canvas.Brush.Style := bsClear;
  FBuffer.Canvas.Pen.Color := clBlack;

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

  { values column }
  FBuffer.Canvas.MoveTo(FValuesBackgroundRect.Right, FCaptionBarRect.Top);
  FBuffer.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;

procedure TMyCustomGrid.Resize;
begin
  inherited;
  FBuffer.SetSize(Width, Height);
  FBuffer.Canvas.Brush.Color := clWhite;
  FBuffer.Canvas.FillRect(ClientRect);
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.
Oh nooo
  • 480
  • 5
  • 19
Jens Borrisholt
  • 5,641
  • 1
  • 25
  • 56
  • Thanks although it still flickers only much worse than before. Also something to point out although you probably know and likely missed it by mistake is you forgot to free `FBuffer` in `TMyCustomGrid.Destroy;` :) – Craig Sep 25 '15 at 13:59
  • Correct I forgot to free my Buffer. But the component does not flicker here at my scree. – Jens Borrisholt Sep 26 '15 at 06:35
0

If you look at the Version Info section inside the Project Options of the Delphi IDE there is a grid control with what seems like a fixed header that does not scroll with the rest of the contents.

The TValueListEditor component appears to be the exact same control. It might be worth looking into ownerdrawing the TValueListEditor or looking deeper at the components source to see how it achieves the effect of having areas of a scrollwindow that do not scroll.

Craig
  • 1,774
  • 9
  • 37