如何使标签在滚动框中平滑居中?

How to keep label smoothly centered in scrollbox?

我在 TScrollBox 中使用 TMemo 来显示一些文本,并在顶部使用 TLabel 作为 header 信息。有时备忘录比滚动框宽,当然 Horizontal scroll bar 可以用来左右滚动以查看备忘录中的文本。 我想要一个标签作为 header 始终以滚动框可见区域为中心。我可以通过设置 Label1.Left:= (Scrollbox1.Width div 2) - (Label1.Width div 2); 来做到这一点并且它可以工作但是它有点闪烁,来回滚动时摇晃。备忘录移动流畅,标签移动不流畅。

这里是单位:

unit Unit1;

interface

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

type

  TScrollBox=Class(VCL.Forms.TScrollBox)
    procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL;
  private
    FOnScrollHorz: TNotifyEvent;
  public
   Property OnScrollHorz:TNotifyEvent read FOnScrollHorz Write FonScrollHorz;
  End;

  TForm1 = class(TForm)
    ScrollBox1: TScrollBox;
    Label1: TLabel;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure ScrollBox1Resize(Sender: TObject);
  private
    procedure MyScrollHorz(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TScrollBox.WMHScroll(var Message: TWMHScroll);
begin
   inherited;
   if Assigned(FOnScrollHorz) then  FOnScrollHorz(Self);
end;

procedure TForm1.MyScrollHorz(Sender: TObject);
begin
    Label1.Left:= (Scrollbox1.Width div 2) - (Label1.Width div 2);
end;

procedure TForm1.ScrollBox1Resize(Sender: TObject);
begin
  Label1.Left:= (Scrollbox1.Width div 2) - (Label1.Width div 2);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  ScrollBox1.OnScrollHorz := MyScrollHorz;
end;

end.

和 dfm:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 212
  ClientWidth = 458
  Color = clBtnFace
  DoubleBuffered = True
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  PixelsPerInch = 96
  TextHeight = 13
  object ScrollBox1: TScrollBox
    Left = 0
    Top = 0
    Width = 458
    Height = 212
    HorzScrollBar.Smooth = True
    HorzScrollBar.Tracking = True
    Align = alClient
    BiDiMode = bdLeftToRight
    DoubleBuffered = True
    ParentBiDiMode = False
    ParentDoubleBuffered = False
    TabOrder = 0
    OnResize = ScrollBox1Resize
    ExplicitHeight = 337
    object Label1: TLabel
      Left = 192
      Top = 30
      Width = 69
      Height = 13
      BiDiMode = bdLeftToRight
      Caption = 'Details header'
      ParentBiDiMode = False
    end
    object Memo1: TMemo
      Left = 24
      Top = 70
      Width = 700
      Height = 89
      Lines.Strings = (
        'Details...')
      TabOrder = 0
    end
  end
end

我尝试使用 DoubleBuffered 但没有帮助。

有什么建议可以让 Label1 在没有 flickering/shaking 的情况下像滚动时的 Memo1 一样流畅吗?


编辑:

设计最终将是我在表单上有 3 个或滚动框,每个滚动框最多包含 3 个 header 的备忘录。滚动需要通过滚动框进行,因为同一滚动框中的所有备忘录都需要同时滚动。这意味着我看不出将标签放在表单或面板上然后放在滚动框外部的表单上如何工作:


编辑 2:

下面的答案确实提供了很好的解决方案,但它们确实需要将居中的 Labels 放置在 Scrollbox 之外并放在 Form 本身上。然后直接在 Form 上移动 Scrollbox's scroll barsscroll bars。这确实得到了预期的效果,但它增加了一些不便,因为 Labels 不再是 Scrollbox 的一部分。

-" 备忘录移动流畅,标签不流畅。"

那是因为你试图阻止它移动。分离您的 OnScrollHorz 处理程序,标签将顺利移动。但这不是你想要的,它将不再以表格为中心。

问题是,在 inherited 调用 (WM_HSCROLL) 期间,标签会随着备忘录一起移动。默认处理后,您重新定位标签,因此出现闪烁。

您可以公开一个额外的事件处理程序,它将在默认滚动 (OnBeforeHorzScroll) 之前触发,并在触发时隐藏标签。虽然 平滑地居​​中 ,但它会导致标签暂时消失的另一种闪烁。可能还是不尽如人意。

解决方案是使用窗体的父级控件,即滚动框的同级控件。你不能用 TLabel 来做,因为它是一个图形控件,但你可以使用 TStaticText。如果 static 在设计时不小心放在滚动框后面,IDE 的 "structure pane" 可能会派上用场。

你可以这样做:

在您的表单上放置一个 ScrollBar,而不是 ScrollBox。将其对齐方式设置为底部(如果您希望有更多列或者您可以将每一列放在自己的面板中,则手动设置其大小和位置)。然后设置备忘录的大小并将标签放在表格的中心。设置备忘录的大小后(可能通过代码动态设置)放置此代码:

ScrollBar1.Min:=0-Memo1.Left;
ScrollBar1.Max:=Memo1.Width-Form1.ClientWidth+Memo1.Left;

最后就是设置ScrollBar的OnChange事件:

procedure TForm1.ScrollBar1Change(Sender: TObject);
begin
  Memo1.Left:=0-ScrollBar1.Position;
  Memo2.Left:=0-ScrollBar1.Position;
  ...
  MemoXY.Left:=0-ScrollBar1.Position;
end;

您的表单应如下所示:

完成!你有一个稳定的居中标签和平滑滚动的备忘录。

编辑:

这是一个版本,每个面板有 3 列,还带有垂直滚动条:

以及整个源代码:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Memo1: TMemo;
    Memo2: TMemo;
    Memo3: TMemo;
    Memo4: TMemo;
    Memo5: TMemo;
    Memo6: TMemo;
    ScrollBar1: TScrollBar;
    ScrollBar2: TScrollBar;
    ScrollBar3: TScrollBar;
    ScrollBar4: TScrollBar;
    ScrollBar5: TScrollBar;
    ScrollBar6: TScrollBar;
    procedure FormCreate(Sender: TObject);
    procedure ScrollBarHChange(Sender: TObject);
    procedure ScrollBarVChange(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var cycle: Integer;
begin
  //GENERATE YOUR COMPONENTS HERE

  //sets every components tag to its default top position
  //(you can do this in any other way for example using array)
  for cycle:=0 to Form1.ComponentCount-1 do
  begin
    if(Form1.Components[cycle] is TControl)then
      Form1.Components[cycle].Tag:=(Form1.Components[cycle] as TControl).Top
  end;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  //changes the panels sizes and positions
  Panel1.Width:=Form1.ClientWidth div 3;
  Panel2.Width:=Form1.ClientWidth div 3;
  Panel3.Width:=Form1.ClientWidth div 3;
  Panel2.Left:=Panel1.Width+1;
  Panel3.Left:=Panel1.Width+Panel2.Width+1;

  //if you dont want all scrollbars to reset on window resize, you need to handle the positioning of elements when window (and panels) size is changing
  ScrollBar1.Position:=ScrollBar1.Min;
  ScrollBar2.Position:=ScrollBar2.Min;
  ScrollBar3.Position:=ScrollBar3.Min;
  ScrollBar4.Position:=ScrollBar4.Min;
  ScrollBar5.Position:=ScrollBar5.Min;
  ScrollBar6.Position:=ScrollBar6.Min;

  //make these tests on the widest element of each panel (8 is just a margin so the memo has some space on the right)
  if((Memo1.Left+Memo1.Width)>(Panel1.ClientWidth-ScrollBar4.Width-8))then
  begin
    ScrollBar1.Enabled:=true;
    ScrollBar1.Max:=Memo1.Width-Panel1.ClientWidth+Memo1.Left+ScrollBar4.Width+8;
  end
  else
    ScrollBar1.Enabled:=false;

  if((Memo3.Left+Memo3.Width)>(Panel2.ClientWidth-ScrollBar5.Width-8))then
  begin
    ScrollBar2.Enabled:=true;
    ScrollBar2.Max:=Memo3.Width-Panel1.ClientWidth+Memo3.Left+ScrollBar5.Width+8;
  end
  else
  begin
    ScrollBar2.Position:=ScrollBar2.Min;
    ScrollBar2.Enabled:=false;
  end;

  if((Memo5.Left+Memo5.Width)>(Panel3.ClientWidth-ScrollBar6.Width-8))then
  begin
    ScrollBar3.Enabled:=true;
    ScrollBar3.Max:=Memo5.Width-Panel1.ClientWidth+Memo5.Left+ScrollBar6.Width+8;
  end
  else
    ScrollBar3.Enabled:=false;

  //make these tests on the bottom element of each panel (16 is just a margin so the memo has some space on the bottom)
  if((Memo2.Top+Memo2.Height)>(Panel1.ClientHeight-ScrollBar1.Height-16))then
  begin
    ScrollBar4.Enabled:=true;
    ScrollBar4.Max:=Memo2.Top+Memo2.Height-Panel1.ClientHeight+ScrollBar1.Height+16;
  end
  else
    ScrollBar4.Enabled:=false;

  if((Memo4.Top+Memo4.Height)>(Panel2.ClientHeight-ScrollBar2.Height-16))then
  begin
    ScrollBar5.Enabled:=true;
    ScrollBar5.Max:=Memo4.Top+Memo4.Height-Panel2.ClientHeight+ScrollBar2.Height+16;
  end
  else
    ScrollBar5.Enabled:=false;

  if((Memo6.Top+Memo6.Height)>(Panel3.ClientHeight-ScrollBar3.Height-16))then
  begin
    ScrollBar6.Enabled:=true;
    ScrollBar6.Max:=Memo6.Top+Memo6.Height-Panel3.ClientHeight+ScrollBar3.Height+16;
  end
  else
    ScrollBar6.Enabled:=false;
end;

procedure TForm1.ScrollBarHChange(Sender: TObject);
var cycle: Integer;
begin
  for cycle:=0 to ((Sender as TScrollBar).Parent as TPanel).ControlCount-1 do
  begin
    if(((Sender as TScrollBar).Parent as TPanel).Controls[cycle] is TMemo)then
      (((Sender as TScrollBar).Parent as TPanel).Controls[cycle] as TMemo).Left:=0-(Sender as TScrollBar).Position+8;
  end;
end;

procedure TForm1.ScrollBarVChange(Sender: TObject);
var cycle: Integer;
begin
  for cycle:=0 to ((Sender as TScrollBar).Parent as TPanel).ControlCount-1 do
  begin
    if(not (((Sender as TScrollBar).Parent as TPanel).Controls[cycle] is TScrollBar))then
      (((Sender as TScrollBar).Parent as TPanel).Controls[cycle] as TControl).Top:=(((Sender as TScrollBar).Parent as TPanel).Controls[cycle] as TControl).Tag-(Sender as TScrollBar).Position;
  end;
end;

end.

和 .dfm:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 473
  ClientWidth = 769
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  OnResize = FormResize
  DesignSize = (
    769
    473)
  PixelsPerInch = 96
  TextHeight = 13
  object Panel1: TPanel
    Left = 0
    Top = 0
    Width = 257
    Height = 473
    Anchors = [akLeft, akTop, akBottom]
    BevelOuter = bvNone
    BorderStyle = bsSingle
    TabOrder = 0
    object Label1: TLabel
      Left = 104
      Top = 16
      Width = 31
      Height = 13
      Caption = 'Label1'
    end
    object Label2: TLabel
      Left = 104
      Top = 152
      Width = 31
      Height = 13
      Caption = 'Label2'
    end
    object Memo1: TMemo
      Left = 8
      Top = 32
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo1')
      TabOrder = 0
    end
    object Memo2: TMemo
      Left = 8
      Top = 168
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo2')
      TabOrder = 1
    end
    object ScrollBar1: TScrollBar
      AlignWithMargins = True
      Left = 0
      Top = 452
      Width = 236
      Height = 17
      Margins.Left = 0
      Margins.Top = 0
      Margins.Right = 17
      Margins.Bottom = 0
      Align = alBottom
      PageSize = 0
      TabOrder = 2
      OnChange = ScrollBarHChange
      ExplicitWidth = 253
    end
    object ScrollBar4: TScrollBar
      Left = 236
      Top = 0
      Width = 17
      Height = 452
      Align = alRight
      Enabled = False
      Kind = sbVertical
      PageSize = 0
      TabOrder = 3
      OnChange = ScrollBarVChange
      ExplicitTop = 248
      ExplicitHeight = 121
    end
  end
  object Panel2: TPanel
    Left = 256
    Top = 0
    Width = 257
    Height = 473
    Anchors = [akLeft, akTop, akBottom]
    BevelOuter = bvNone
    BorderStyle = bsSingle
    TabOrder = 1
    object Label3: TLabel
      Left = 104
      Top = 16
      Width = 31
      Height = 13
      Caption = 'Label3'
    end
    object Label4: TLabel
      Left = 104
      Top = 152
      Width = 31
      Height = 13
      Caption = 'Label4'
    end
    object Memo3: TMemo
      Left = 8
      Top = 32
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo3')
      TabOrder = 0
    end
    object Memo4: TMemo
      Left = 8
      Top = 168
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo4')
      TabOrder = 1
    end
    object ScrollBar2: TScrollBar
      AlignWithMargins = True
      Left = 0
      Top = 452
      Width = 236
      Height = 17
      Margins.Left = 0
      Margins.Top = 0
      Margins.Right = 17
      Margins.Bottom = 0
      Align = alBottom
      PageSize = 0
      TabOrder = 2
      OnChange = ScrollBarHChange
      ExplicitWidth = 253
    end
    object ScrollBar5: TScrollBar
      Left = 236
      Top = 0
      Width = 17
      Height = 452
      Align = alRight
      Enabled = False
      Kind = sbVertical
      PageSize = 0
      TabOrder = 3
      OnChange = ScrollBarVChange
      ExplicitTop = 248
      ExplicitHeight = 121
    end
  end
  object Panel3: TPanel
    Left = 512
    Top = 0
    Width = 257
    Height = 473
    Anchors = [akLeft, akTop, akBottom]
    BevelOuter = bvNone
    BorderStyle = bsSingle
    TabOrder = 2
    object Label5: TLabel
      Left = 104
      Top = 16
      Width = 31
      Height = 13
      Caption = 'Label5'
    end
    object Label6: TLabel
      Left = 104
      Top = 152
      Width = 31
      Height = 13
      Caption = 'Label6'
    end
    object Memo5: TMemo
      Left = 8
      Top = 32
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo5')
      TabOrder = 0
    end
    object Memo6: TMemo
      Left = 8
      Top = 168
      Width = 497
      Height = 89
      Lines.Strings = (
        'Memo6')
      TabOrder = 1
    end
    object ScrollBar3: TScrollBar
      AlignWithMargins = True
      Left = 0
      Top = 452
      Width = 236
      Height = 17
      Margins.Left = 0
      Margins.Top = 0
      Margins.Right = 17
      Margins.Bottom = 0
      Align = alBottom
      PageSize = 0
      TabOrder = 2
      OnChange = ScrollBarHChange
      ExplicitWidth = 253
    end
    object ScrollBar6: TScrollBar
      Left = 236
      Top = 0
      Width = 17
      Height = 452
      Align = alRight
      Enabled = False
      Kind = sbVertical
      PageSize = 0
      TabOrder = 3
      OnChange = ScrollBarVChange
      ExplicitTop = 248
      ExplicitHeight = 121
    end
  end
end

为什么不用两个滚动框。

您使用一个进行垂直滚动。在上面放置标签和带有备忘录的第二个滚动框。

第二个滚动框将在需要时用于水平滚动。

或者甚至更好的解决方案是将 TMemo 替换为其他一些控件,如 TRichEdit,它有自己的滚动条。所以你只有一个滚动框,就像现在一样,当文本太宽时,TRichEdit 会自行滚动。