我自定义的控件闪烁。是什么原因导致它,并如何消除它?

4

介绍

我正在编写一个自定义控件,它是从TScrollBox派生的,但是我遇到了一些困难,似乎应该很容易解决。

该控件将用于在顶部显示静态的标题栏(即当滚动框滚动时不会移动),然后在标题栏下面,我将在其自己的列中绘制一些值,如行号等。

这就是目前的控件的外观,以便更好地理解(非常早期的工作进展):

enter image description here

闪烁问题

我面临的问题是闪烁,我没有找到消除它的简单方法。我有一种感觉,闪烁是因为我试图在我的标题栏下面进行绘制,当闪烁发生时,您实际上可以看到在标题栏下面绘制的值,尽管我的假设可能完全错误。

所有的绘图都是在一个TGraphicControl上完成的,它是滚动框的子元素,当快速滚动时,闪烁发生得非常频繁,当使用滚动条按钮时,它仍然会闪烁,但不会像快速滚动时那样频繁。

我无法捕捉到闪烁并在此处显示图像,但是通过下面的代码,您可以构建并安装到一个新包中进行测试:

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.

问题

为了解决闪烁问题,我需要做出哪些改变?

似乎将滚动框的DoubleBuffered设置为True没有什么效果。我尝试使用WM_ERASEBACKGROUND消息进行实验,但这只会使滚动框变成黑色。

我还尝试在滚动框上实现画布,并直接在其上绘制标题栏,然后将滚动框的填充设置为我的标题栏高度,并在我的TGraphicControl上绘制其余部分,但这导致闪烁更严重。此时,我不知道究竟是什么导致了闪烁,也不知道如何消除它。

最后一个问题是,当使用滚动条拖块滚动时,如何使滚动条以固定增量滚动?我将垂直滚动条增量设置为相当于行高的值,这在按下滚动条按钮时可以正常工作,但在使用滚动条拖块向上和向下滚动时,它并不是以固定增量滚动。我正在尝试让滚动条以固定增量滚动而不是松散滚动。


3
看起来你最好使用标题、滚动条和绘画表面而不是滚动框。 - Sertac Akyuz
@SertacAkyuz 感谢您的建议,尽管我想完全自定义绘制所有内容,但我会看看标题控件的功能 - 我以前从未使用过它。 - Craig
我不认为你有使用双缓冲的必要。通常这是解决闪烁问题的错误方法。抵制诱惑,正确地修复代码。需要进行重大重新设计。 - David Heffernan
@DavidHeffernan您说得完全正确。实际上,“DoubleBuffering”可能会导致其他图形故障和问题等,因此通常最好避免使用它,因为这只是隐藏了真正的问题。这只是我尝试了解其行为的一个例子。我遇到的主要问题,您自己也知道,就是基本上需要重新开始,但我无法找到最佳方法来实现这一点。其他人提到了使用VCL标题/面板等,但我必须自己完全自定义绘制整个内容,因此宁愿避免使用子控件。 - Craig
我认为避免使用子控件可能是关键。 - David Heffernan
4个回答

4
一种快速解决方法是在TMyGrid.WMVScroll中将Self.Invalidate替换为FGrid.Repaint(或.Update.Refresh)。这样可以消除闪烁,但当你拖动滚动条拇指时,它仍然会显示多个标题栏的问题。说明:Invalidate将重绘请求放入消息队列中,该队列被延迟到为空时执行,因此不会立即处理,即不会在您要求时处理。Repaint立即执行。但通常Invalidate就足够了...
问题的主要原因在于“粘性”标题(或标题栏)与客户空间中的布局。使用TControlScrollBar的每个窗口控件都内部使用ScrollWindow,它会根据滚动方向“移动”标题栏的位置。您可以通过一些技巧阻止,但从设计角度来看,当滚动条从标题下方开始时,这也更美观。
然后有几个选项可用于组件的内部布局:
  • 为标题使用alTop对齐的PaintBox,使用alRight对齐的ScrollBar和用于网格的alClient对齐的PaintBox。这是Sertac评论的方式,需要3个控件。
  • 为标题使用alTop对齐的PaintBox,在其中使用alClient对齐的ScrollBox,并在其中使用alTop对齐的PaintBox来绘制网格。该设计具有嵌套控件。
  • 使用TScrollingWinControl并在顶部添加一个非客户端边框用于标题,并且使用alTop对齐的PaintBox来绘制网格。此组件只包含1个控件。
  • 使用TScrollingWinControl并在顶部添加一个非客户端边框用于标题,然后在其PaintWindow方法中绘制网格。此设计不需要任何额外的控件。
  • ...
以下是第三种选项的实现示例:
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.

关于你的代码的一些普遍评论:

  • 你的祖先TMyCustomGrid没有你的后代TMyGrid是不可行的,这通常是不允许的。顺便说一句,代码TMyGrid(Self.Parent).VertScrollBar.Position等于-Top,这消除了对其后代的了解的需要。
  • 没有必要创建一个字体。 TControl已经有了一个字体,只需发布它。
  • 除非你想从TScrollBox中获取边框选项,一般来说最好从-在这种情况下-TScrollingWinControl派生,因为只有这样你才能控制应该公开哪些属性。

最后一个问题是如何使滚动条在使用滚动条拇指时按设置的增量滚动?

通过像上面的代码中所做的那样,在WM_VSCROLL中调整滚动位置:

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

2
感谢您的回答,我一直认为例如Repaint是不正确的,而Invalidate才是正确的用法,但是您在回答中解释了原因,这是我学到的新知识。我使用的是TGraphicControl进行绘制,然后将其放入容器(如TScrollBox)中,这种方法是否正确?或者您认为最好直接在TScrollBox上绘制?我曾经使用过FGrid.Invalidate而不是Self.Invalidate,但它们都可以实现相同的效果,所以我坚持使用Self.Invalidate - Craig
1
就我个人而言,我会从“TCustomControl”创建控件。 - Andreas Rejbrand
1
非常感谢您的帮助,这确实让我深思熟虑并学到了一些新技巧,以后需要时我会使用它们。至于我要选择哪个选项,我还不确定,但我知道我想让滚动条成为标题的一部分,因为在下面,它看起来与我想要绘制的方式不协调。我低估了做这件事的难度,每当我遇到类似滚动框的自定义控件时,往往会发现一些行为上的怪异之处。不过,这里提供的信息足够让我重新考虑接下来要做什么。 - Craig

0
当你重绘时,需要逐行重绘。这会导致先擦除第一行再重新绘制它,然后是第二行,以此类推,从而产生闪烁效果。更加美观的做法是先用背景色绘制整个矩形。否则,你可能需要考虑实现并使用InvalidateRect。

我曾考虑过InvalidateRect,但从未尝试过。我相信闪烁问题是由我的代码以及我试图保持标题栏静态并在其下绘制其余部分的方式引起的,我只是无法想出更优雅的解决方法。 - Craig
可能最好的方法是将标题栏放在一个面板中,其余部分放在TScrollBox中 - 我意识到这样做有点晚了。问题是,其他人(在微软)已经经历过这种痛苦,所以利用一下并不是个坏主意... - Dsm

0

如果您在 Delphi IDE 的 Project Options 中查看 Version Info 部分,您会发现一个网格控件,它似乎有一个固定的标题,不会随着其余内容滚动。

TValueListEditor 组件似乎是完全相同的控件。值得研究的是如何在 TValueListEditor 上进行所有者绘制,或深入查看组件源码,以了解它如何实现具有不滚动的滚动窗口区域效果。


0
问题在于您直接在画布上绘制。请将内容绘制到位图上,然后再将其绘制到画布上:这是您组件的修改版本:
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.

谢谢,尽管它仍然闪烁,但比以前更严重。还有一件事要指出,虽然您可能知道并且可能因疏忽而错过了,就是您在TMyCustomGrid.Destroy;中忘记释放FBuffer :) - Craig
正确,我忘记释放我的缓冲区了。但是这个组件在我的屏幕上没有闪烁。 - Jens Borrisholt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接