Mike Torrettinni
Mike Torrettinni

Reputation: 1824

How to keep label smoothly centered in scrollbox?

I use a TMemo in TScrollBox to show some text, and a TLabel on top as a header info. Sometimes memo is wider than scroll box and of course Horizontal scroll bar can be used to scroll left and right to see text in memo. I want to have a label as a header always centered to scroll box visible area. I can do this by setting Label1.Left:= (Scrollbox1.Width div 2) - (Label1.Width div 2); and it works but it kind of flickers, shakes when scrolling back and forth. Memo moves smoothly, label doesn't.

enter image description here

Here is unit:

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.

and 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

I tried using DoubleBuffered but doesn't help.

Any suggestions how to make Label1 move without flickering/shaking, as smooth as Memo1 does when scrolling?


EDIT:

The design will eventually be that I have 3 or scrollboxes on form and each one contain up to 3 memos with header. And scrolling needs to be by scrollbox as all memos in same scroll box need to be scrolled at the same time. That means I do not see how it would work with putting label on form or panel and then on form, outside scrollboxes:

enter image description here


EDIT 2:

The answers below do provide good solutions, but they do make necessary to place the Labels, that are centered, out of the Scrollbox and put on the Form itself. And then move either by Scrollbox's scroll bars or by scroll bars directly on Form. This does get desired affect, but it adds a little inconvenience with Labels not being part of Scrollbox, anymore.

Upvotes: 0

Views: 2089

Answers (3)

SilverWarior
SilverWarior

Reputation: 8331

Why not use two scroll boxes.

You use one for vertical scrolling. On it you place your label and your second scroll box with memo on it.

This second scroll box will then be used for horizontal scrolling when needed.

Or perhaps even a better solution would be to replace TMemo with some other control like TRichEdit which has its own scrollbars implemented. So you have only one scroll box like now and TRichEdit Will take care of its own scrolling when the text is to wide.

Upvotes: 1

jano152
jano152

Reputation: 157

You can do it like this:

Instead of ScrollBox put a ScrollBar on your form. Set its alignment to bottom (or set its size and position manually if you wish to have more columns or you can put each one in its own panel). Then set the size of your Memos and place Labels to the center of the Form. After setting the size of the Memos (probably dynamically via code) place this code:

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

The last thing is to set the ScrollBar OnChange event:

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

Your form should look something like this:

Scrollable memos

Done! You have a stable centered Labels and smoothly scrollable Memos.

Edit:

Here is a version with 3 columns each in his own Panel and also with vertical scrollbars:

Vertical scroll

And the whole source code:

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.

And the .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

Upvotes: 2

Sertac Akyuz
Sertac Akyuz

Reputation: 54772

-" Memo moves smoothly, label doesn't."

That's because you're trying to prevent it from moving. Detach your OnScrollHorz handler and the label will move smoothly. But that's not what you want, it will not be centered to the form any more.

The problem is, during the inherited call (WM_HSCROLL), the label moves along with the memo. After the default handling, you relocate the label, hence the flicker.

You can expose an additional event handler that will fire before default scrolling (OnBeforeHorzScroll), and hide the label when it fires. While smoothly centered, it will cause a different kind of flicker where the label momentarily disappears. Still may not be satisfactory.

The solution is to use a control that is parented to the form, a sibling to the scroll box. You can't do that with a TLabel as it is a graphic control, but you can use TStaticText. The "structure pane" of the IDE may come handy if the static accidentally goes behind the scroll box at design time.

Upvotes: 2

Related Questions