Delphi: sliding (animated) panel - delphi

Is there a sliding (animated) panel component for Delphi?
For instance it can be found in Raize Components (a left panel with a "Hot Spot" or hide/show button).
I need not a resizeable panel but a panel that can slide horizontally and smoothly from the left to the right + that has a hide/show button (it's not a big deal if without that button).
Thanks!

Try NLDSideBar, a container component written by myself that is collapsable and aligned along the left or right side of its parent.
Interface:
property Align: TSideBarAlign default alLeft;
property AutoHide: Boolean default False;
property Hint: String;
property MinWidth: Integer default DefWidth;
property OnAutoHideChanged: TNotifyEvent;
property OnHide: TNotifyEvent;
property PinButtonDownHint: String;
property PinButtonUpHint: String;
property PinButtonVisible: Boolean default True;
property Resizable: Boolean default True;
property SideButtonWidth: Integer default DefSideButtonWidth;
property Caption;
property Color default clBtnFace;
property Font;
property ParentColor default False;
property ParentFont;
property ParentShowHint;
property PopupMenu;
property TabOrder;
property TabStop;
Or maybe this older version which is animated. Free to use, free to modify.
Sorry for being self-promotive, but I think it's an answer to the question.

We ended up building our own control. We could not find anything that worked quite how we wanted. It ended up not being that hard. I'm sure there are situations we are not handling correctly, but so for this is working good for us.
The code below is using cxGroupBox because we needed that look to match the rest of our application. That can be switched out for a normal GroupBox.
We are using this in two places. In one case we have a number of these panels inside a standard Delphi Flow Panel (I'm not sure what version that was added). When our DynPanel collapses everything automatically moves up and fills the space.
In the other case we have a window that is split between a main section and a toolbox. The two are separated by a standard splitter. The main window is set to align to client. When our panel collapses or expands. the splitter automatically moves and expands the main section.
We never did quite get the "container" control stuff to work so items you add to the panel can be moved outside the bounds you would normally expect in a group box. But that does not cause us any major problems so we just left it. This also does not account for DPI changes in relation to the button size. The caption will get bigger but the button will not.
unit DynPanel;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, cxGroupBox;
const
DEFAULTBUTTONWIDTH = 16;
DEFAULTWIDTH = 161;
DEFAULTHEIGHT = 81;
cButtonPadding = 8;
cCollapsePadding = 3;
cCaptionPadding = ' ';
cCollapsedSize = DEFAULTBUTTONWIDTH + cCollapsePadding;
cAutoCollapseSize = DEFAULTBUTTONWIDTH + cButtonPadding;
type
TCollapseDirection = (cdUp, cdRight, cdLeft);
TMinDemension = cAutoCollapseSize..High(Integer);
TDynPanel = class(TPanel)
private
FGroupBox: TcxGroupBox;
FButtonPanel: TPanel;
FButtonImage: TImage;
FExpand: Boolean;
FOldHeight: Integer;
FOldWidth: Integer;
FCollapseDirection: TCollapseDirection;
FOrigGroupBoxCaption: String;
FAutoCollapseHeight: TMinDemension;
FAutoCollapseWidth: TMinDemension;
FButtonPadding: integer;
FCollapsePadding: integer;
FCollapsedSize: integer;
procedure SetExpand(Value: Boolean);
procedure SetGroupBoxCaption(Value: string);
procedure ButtonMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure CMShowingChanged(var Message: TMessage); message CM_SHOWINGCHANGED;
procedure EnableControls(Value: Boolean);
procedure SetCollapseDirection(Value: TCollapseDirection);
procedure ConfigurePanel;
procedure SetMinHeight(Value: TMinDemension);
procedure SetMinWidth(Value: TMinDemension);
procedure UpdateImage();
protected
procedure Resize; override;
procedure ChangeScale(M, D: Integer); override;
public
constructor Create(AOwner: TComponent); override;
property OldHeight: Integer read FOldHeight write FOldHeight;
property OldWidth: Integer read FOldWidth write FOldWidth;
property GroupBox: TcxGroupBox read FGroupBox;
published
property Caption: string read FOrigGroupBoxCaption write SetGroupBoxCaption;
property Expand: Boolean read FExpand write SetExpand;
property BevelOuter default bvNone;
property CollapseDirection: TCollapseDirection read FCollapseDirection write SetCollapseDirection default cdUp;
property AutoCollapseHeight: TMinDemension read FAutoCollapseHeight write SetMinHeight default cAutoCollapseSize;
property AutoCollapseWidth: TMinDemension read FAutoCollapseWidth write SetMinWidth default cAutoCollapseSize;
end;
procedure Register;
implementation
{$R 'ButtonImages\ButtonImages.res' 'ButtonImages\ButtonImages.rc'}
uses cxEdit;
procedure Register;
begin
RegisterComponents('AgWare', [TDynPanel]);
end;
{ TDynPanel }
{
TDynPanel.Create
---------------------------------------------------------------------------
}
constructor TDynPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Self.ControlStyle := ControlStyle - [csSetCaption];
Self.Width := DEFAULTWIDTH;
Self.Height := DEFAULTHEIGHT;
BevelOuter := bvNone;
FExpand := True;
FOldHeight := Self.Height;
FOldWidth := Self.Width;
FOrigGroupBoxCaption := 'AgDynPanel';
FCollapseDirection := cdUp;
FAutoCollapseHeight := cAutoCollapseSize;
FAutoCollapseWidth := cAutoCollapseSize;
FGroupBox := TcxGroupBox.Create(Self);
FGroupBox.Parent := Self;
FGroupBox.Align := alClient;
FGroupBox.Alignment := alTopLeft;
FButtonPanel := TPanel.Create(Self);
FButtonPanel.Parent := Self;
FButtonPanel.Top := 0;
FButtonPanel.Width := DEFAULTBUTTONWIDTH;
FButtonPanel.Height := DEFAULTBUTTONWIDTH;
FButtonPanel.Left := Width - DEFAULTBUTTONWIDTH - FButtonPadding;
FButtonPanel.OnMouseDown := ButtonMouseDown;
FButtonImage := TImage.Create(Self);
FButtonImage.Parent := FButtonPanel;
FButtonImage.Align := alClient;
FButtonImage.Stretch := false;
FButtonImage.Center := true;
FButtonImage.OnMouseDown := ButtonMouseDown;
UpdateImage;
// The click should also work for the entire top of the group box.
FGroupBox.OnMouseDown := ButtonMouseDown;
FGroupBox.Caption := FOrigGroupBoxCaption;
FGroupBox.Style.Font.Style := FGroupBox.Style.Font.Style + [fsBold];
FButtonPadding := cButtonPadding;
FCollapsePadding := cCollapsePadding;
FCollapsedSize := cCollapsedSize;
end;
{
TDynPanel.SetGroupBoxCaption
---------------------------------------------------------------------------
}
procedure TDynPanel.SetGroupBoxCaption(Value: String);
begin
FOrigGroupBoxCaption := Value;
ConfigurePanel;
end;
{
TDynPanel.SetMinHeight
---------------------------------------------------------------------------
}
procedure TDynPanel.SetMinHeight(Value: TMinDemension);
begin
if Value = FAutoCollapseHeight then
Exit; // >>----->
FAutoCollapseHeight := Value;
if Showing then
Resize;
end;
{
TDynPanel.SetMinWidth
---------------------------------------------------------------------------
}
procedure TDynPanel.SetMinWidth(Value: TMinDemension);
begin
if Value = FAutoCollapseWidth then
Exit; // >>----->
FAutoCollapseWidth := Value;
if Showing then
Resize;
end;
{
TDynPanel.ButtonMouseDown
---------------------------------------------------------------------------
}
procedure TDynPanel.ButtonMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button mbLeft then
Exit; // >>----->
if ((FExpand = True) and (Y FCollapsePadding)) or
((FExpand = False) and (FCollapseDirection = cdLeft) and (X >----->
FExpand := Value;
//ConfigurePanel;
//--------------------------------------------------------------------------
// Set the group box size
//--------------------------------------------------------------------------
//
// I chose to do the resizing of the control here rather than in
// ConfigurePanel because if you do it there the SetBounds will call ReSize
// which will call ConfigurePanel again so that you would need to keep track
// of a boolean variable to determine if you are making recursive calls into
// ConfigurePanel. That is one reason. Another is that when the dfm values
// are streamed in and the properties get set you will resize the control
// before the actual Height and Width properties are set. This will cause
// bogus default values to be stored for FOldHeight and FOldWidth and when
// the control is displayed the dimensions will be wrong. If you size the
// control here then, on creation, Resize will not get called and the
// FOldHeight and FOldWidth values will not get saved off until
// CMShowingChanged will explicitly call ReSize after the dimensions are
// properly set. If you move this code into ConfigurePanel then when the
// caption is streamed in and set from the dfm then ConfigurePanel would get
// called, we would SetBounds there and then Resize would fire storing off the
// default invalid values for the FOld variables as mentioned above.
// Hope this makes sense. Leave the SetBounds calls here and make your life
// easier. :)
//--------------------------------------------------------------------------
// Changing to Expanded
if FExpand = True then
begin
// Up
if FCollapseDirection = cdUp then
SetBounds(Left, Top, Width, FOldHeight)
// Right
else if FCollapseDirection = cdRight then
SetBounds((Left + Width) - FOldWidth, Top, FOldWidth, Height)
// Left
else if FCollapseDirection = cdLeft then
SetBounds(Left, Top, FOldWidth, Height);
end
// Changing to Collapsed
else
begin
// Up
if FCollapseDirection = cdUp then
begin
// Reset the AutoCollapseHeight just to make sure we don't try to
// recollapse on resize.
if FAutoCollapseHeight FGroupBox) and
(Self.Controls[i] FButtonPanel) then
begin
Self.Controls[i].Enabled := Value;
Self.Controls[i].Visible := Value;
end;
end;
end;
{
TDynPanel.CMShowingChanged
---------------------------------------------------------------------------
}
procedure TDynPanel.CMShowingChanged(var Message: TMessage);
begin
inherited;
if Showing then
Resize;
end;
{
TDynPanel.Resize
---------------------------------------------------------------------------
}
procedure TDynPanel.Resize;
begin
if FExpand = True then
begin
if (FCollapseDirection = cdUp) and (Height FAutoCollapseHeight then
begin
FOldHeight := Height;
Expand := True;
end
else
Height := FCollapsedSize;
end
else if (FCollapseDirection = cdLeft) or (FCollapseDirection = cdRight) then
begin
if (Width > FAutoCollapseWidth) then
begin
FOldWidth := Width;
Expand := True;
end
else
Width := FCollapsedSize;
end;
end;
ConfigurePanel;
end;
{
TDynPanel.ChangeScale
---------------------------------------------------------------------------
}
procedure TDynPanel.ChangeScale(M, D: Integer);
begin
FAutoCollapseHeight := MulDiv(FAutoCollapseHeight, M, D);
FAutoCollapseWidth := MulDiv(FAutoCollapseWidth, M, D);
FButtonPadding := MulDiv(FButtonPadding, M, D);
FCollapsePadding := MulDiv(FCollapsePadding, M, D);
FCollapsedSize := MulDiv(FCollapsedSize, M, D);
FOldHeight := MulDiv(FOldHeight, M, D);
FOldWidth := MulDiv(FOldWidth, M, D);
// inherited will cause resize to be called. I need to update
// my internal values before that happens, otherwise I will resize based
// on the old values.
inherited;
end;
{
TDynPanel.SetCollapseDirection
---------------------------------------------------------------------------
}
procedure TDynPanel.SetCollapseDirection(Value: TCollapseDirection);
begin
if Value = FCollapseDirection then
Exit; // >>----->
FCollapseDirection := Value;
ConfigurePanel;
end;
{
TDynPanel.ConfigurePanel
---------------------------------------------------------------------------
}
procedure TDynPanel.ConfigurePanel;
begin
//--------------------------------------------------------------------------
// Set the group box style, caption alignment, caption, button position, and
// button image
//--------------------------------------------------------------------------
// Changing to Expanded
if FExpand = True then
begin
FGroupBox.Style.Color := clWhite;
// Up
if FCollapseDirection = cdUp then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end
// Right
else if FCollapseDirection = cdRight then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := ' ' + FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := FButtonPadding;
end
// Left
else if FCollapseDirection = cdLeft then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end;
end
// Changing to Collapsed
else
begin
FGroupBox.Style.Color := clGradientActiveCaption;
// Up
if FCollapseDirection = cdUp then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end
// Right
else if FCollapseDirection = cdRight then
begin
FGroupBox.Alignment := alRightTop;
FGroupBox.Caption := ' ' + FOrigGroupBoxCaption;
FButtonPanel.Top := FButtonPadding;
FButtonPanel.Left := FCollapsePadding;
end
// Left
else if FCollapseDirection = cdLeft then
begin
FGroupBox.Alignment := alLeftTop;
FGroupBox.Caption := FOrigGroupBoxCaption + ' ';
FButtonPanel.Top := FButtonPadding;
FButtonPanel.Left := 0;
end;
end;
UpdateImage;
// Now draw the button and invalidate Self
Self.Invalidate;
end;
{
TDynPanel.UpdateImage
---------------------------------------------------------------------------
}
procedure TDynPanel.UpdateImage();
begin
case FCollapseDirection of
cdUp:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageUp')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageDown');
end;
cdLeft:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageLeft')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageRight');
end;
cdRight:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageRight')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageLeft');
end;
end;
end;
end.
Close to the Left
Close to the Top

The new Delphi version will include such kind of sliding panels ( trough the integration of FireMonkey, formely vgScene/dxScene ). You'll just have to click on height or position props and an option will allow to create a animation for this, with various option ( interpolation kind, duration etc).

Since version 2009, there is a TCategoryPanelGroup where you add TCategoryPanel.

FoldingPanel
See FoldingPanel v1.3 here: http://www.torry.net/authorsmore.php?id=2386
I used it for years.
It comes with nice chevron bitmap too.
Small issue: it does not support composite controls (like TLabeledEdit).
Advantage: The component comes as a single PAS file (easy to install into the Palette). They say that it is for Delphi 5 but I have it installed in XE7 and works without problems (which indicates quality).
Freeware
TSplitView
If you have a newer version of Delphi (like Tokyo) you can use TSplitView.
Note: The Align property it missing. And at the beginning it might seems that it can only be aligned to Left. But it is not true. Instead of an Align property it has a Placement property (with two values: svpRight / svpLeft).
Note: It has some small glitches related to control size/placement.
Note: It is not (NOT EVEN BY FAR) as complete as FoldingPanel. You still have to write some cod eto implement some kind of chevron to collapse/expend the panel.
https://www.youtube.com/watch?v=3hUG8o7PpCU
Freeware (if you have Delphi Tokyo).
TCategoryPanelGroup
Also take a look at TCategoryPanelGroup. It might or might not work, depending on what you need it for.
Freeware (if you have Delphi XE7)

Related

Screen becomes black when repositioning Form to second monitor using Parallels VM

I am working with Delphi 10.4.2 in Windows 10 (virtualized in Parallels) on a dual monitor system. To recreate the problem on a multi-monitor system, create a new Windows VCL Application and place two buttons on the form: btnPrimaryMonitor and btnSecondaryMonitor. Then insert this code by creating click handlers for the two buttons:
procedure TForm1.btnPrimaryMonitorClick(Sender: TObject);
begin
RepositionFormToMonitor(0);
EnableDisableButtons;
end;
procedure TForm1.RepositionFormToMonitor(const aMonitor: Integer);
const
offset = 2;
begin
Self.Width := Screen.Monitors[aMonitor].Width - offset;
Self.Height := Screen.Monitors[aMonitor].Height - offset;
Self.Top := Screen.Monitors[aMonitor].Top;
Self.Left := Screen.Monitors[aMonitor].Left;
end;
procedure TForm1.btnSecondaryMonitorClick(Sender: TObject);
begin
RepositionFormToMonitor(1);
EnableDisableButtons;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
EnableDisableButtons;
Self.BorderStyle := bsNone;
Self.FormStyle := fsStayOnTop;
RepositionFormToMonitor(0);
end;
procedure TForm1.EnableDisableButtons;
begin
btnPrimaryMonitor.Enabled := (Self.Monitor.MonitorNum = 1);
btnSecondMonitor.Enabled := (Self.Monitor.MonitorNum = 0);
end;
This works perfectly, but as soon as I set offset = 1 or offset = 0 the screen becomes black!
The purpose of the code is to reposition the maximized stay-on-top Form from the primary monitor to the secondary monitor by clicking on the btnSecondMonitor button and then back to the primary monitor by clicking on the btnPrimaryMonitor button.
How can this problem be avoided?
A few issues:
You should not set WindowState to wsMaximized. In fact, you shouldn't touch this property at all.
Setting BoundsRect will set Left, Top, Width, and Height, so there is no need to set Left and Top separately.
To go back to the primary monitor, just set the form's BoundsRect.
Here's an example:
Create a new VCL project. Set the main form's BorderStyle to bsNone.
Then add the following code:
procedure TForm1.FormCreate(Sender: TObject);
begin
for var i := 0 to Screen.MonitorCount - 1 do
begin
var btn := TButton.Create(Self);
btn.Parent := Self;
btn.Caption := i.ToString;
btn.Tag := i;
btn.OnClick := MonitorButtonClick;
btn.Top := 8;
btn.Left := 8 + (btn.Width + 8) * i;
end;
end;
procedure TForm1.MonitorButtonClick(Sender: TObject);
begin
BoundsRect := Screen.Monitors[(Sender as TButton).Tag].BoundsRect;
end;
If this code doesn't work properly on your system, you probably have some problem with that Windows system. This should work flawlessly.

Create floating TToolbar at run-time without flicker

I'm attempting to create a custom TToolbar at run-time that floats over the form (below the control it is associated with).
My issue is that the process of floating and positioning the toolbar at creation creates a hideous flicker where it is initially drawn at the top-left of the monitor before being moved to my desired position on the form.
I cannot find a way to avoid this. Is there a way?
procedure TMainForm.Button3Click(Sender: TObject);
var
newToolbar : TToolbar;
newButton : TToolButton;
begin
newToolbar := TToolbar.Create(Self);
newToolbar.Visible := False;
newToolbar.ManualFloat( Rect( 0, 0, newToolbar.Width, newToolbar.Height ));
newToolbar.Parent := Self;
newToolbar.left := 100;
newToolbar.Top := 100;
newToolbar.ShowCaptions := True;
newButton := TToolButton.Create(Self);
newButton.Parent := newToolbar;
newButton.Caption := 'Test';
newToolbar.Visible := True;
end;
References:
- Create TToolbutton runtime
- toolbutton with action created at runtime
- Delphi - Create a custom TToolBar component
I am a little puzzled with your solution, so I provide my two takes on the subject. Specifically I don't understand why you are using ManualFloat() and few lines later set the parent of the toolbar, which makes it non-floating.
Here is a solution for a floating toolbar, using ManualFloat().
The toolbar is floating above the form in its own temporary TCustomDockForm,
at the given location.
The record needed by ManualFloat() is setup for the final location, thus no flicker in the wrong place, and the control
is immediately correctly positioned.
procedure TForm1.Button3Click(Sender: TObject);
var
newToolbar : TToolbar;
newButton : TToolButton;
p: TPoint;
begin
newToolbar := TToolbar.Create(Self);
// calculate position in screen coordinates for the floating toolbar
p := ClientOrigin;
p.Offset(100, 100);
// and make it floating in final position
newToolbar.ManualFloat( Rect(p.X, p.Y, p.X+NewToolbar.Width, p.Y+newToolbar.Height) );
newToolbar.Visible := False; // really needed ?
// Then create the toolbar buttons
newToolbar.ShowCaptions := True;
newButton := TToolButton.Create(self);
newButton.Parent := newToolbar;
newButton.Caption := 'Test';
newToolbar.Visible := True;
end;
However, since you actually seem to want a non-floating toolbar, that is just
located anywhere you like on the form (and not in the default top of the form),
a better solution is to skip the ManualFloat() altogether and just set the
Align property of the toolbar to alNone. This enables it to be moved anywhere
on the parent form.
procedure TForm1.Button4Click(Sender: TObject);
var
newToolbar : TToolbar;
newButton : TToolButton;
begin
newToolbar := TToolbar.Create(Self);
newToolbar.Align := alNone; // constructor sets it to alTop
newToolbar.Visible := False; // really needed ?
newToolbar.Parent := Self;
newToolbar.Left := 100;
newToolbar.Top := 200;
newToolbar.ShowCaptions := True;
newButton := TToolButton.Create(self);
newButton.Parent := newToolbar;
newButton.Caption := 'Test';
newToolbar.Visible := True; //
end;
This gives you the same appearance as your own code, but omits the ManualFloat().
Finally, an image to show the appearances:
The bottom toolbar is created with Button4Click()
Thanks #TomBrunberg for your suggestion.
What was needed to make it position over the form without any pre-drawing:
Position it off-screen when ManualFloat is called
Set Visible to false after call to ManualFloat (because ManualFloat sets it true)
Revised code:
procedure TMainForm.Button3Click(Sender: TObject);
var
newToolbar : TToolbar;
newButton : TToolButton;
begin
newToolbar := TToolbar.Create(Self);
// Float with off-screen position
newToolbar.ManualFloat( Rect( 0, -200, newToolbar.Width, newToolbar.Height - 200 ));
// Must hide after ManualFloat call, as it resets Visible to true
newToolbar.Visible := False;
// Set parent so we can add buttons, sets props, etc.
newToolbar.Parent := Self;
// Move to desired position over form
newToolbar.left := 100;
newToolbar.Top := 100;
// Add our button content...
newToolbar.ShowCaptions := True;
newButton := TToolButton.Create(Self);
newButton.Parent := newToolbar;
newButton.Caption := 'Test';
// Now we can show it
newToolbar.Visible := True;
end;

How to get the number of displayed lines in TMemo?

I need to get the number of displayed lines in TMemo (this include the lines that was wrapped because WordWrap is set to true). I need this to auto adjust the height of the Tmemo to it's content.
lines.count of course don't care about wrapped lines so i can't use it. strangely TextPosToPos also don't care about wrapped lines so i can't use it too ...
I m under firemonkey and delphi Berlin
Edit after I learned about ContentsBounds. My original (and obsolete) answer is still visible in the revisions.
Why do you need the number of displayed lines to adjust the height of the TMemo? This resizes the TMemo to its content:
Memo1.Height := Memo1.ContentBounds.Height + 5; // Small const to allow for rendering margins
It also takes word-wrap into consideration.
I don't know why using ContentBounds is "not really ideal". Here's how I do it:
uses
FMX.TextLayout, FMX.Graphics;
function MeasureTextHeight(const AFont: TFont; const AText: string): Single;
var
LLayout: TTextLayout;
begin
LLayout := TTextLayoutManager.DefaultTextLayout.Create;
try
LLayout.BeginUpdate;
try
LLayout.WordWrap := False;
LLayout.Font.Assign(AFont);
LLayout.Text := AText;
finally
LLayout.EndUpdate;
end;
Result := LLayout.TextHeight;
finally
LLayout.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
LTextHeight: Single;
LLines: Integer;
begin
LTextHeight := MeasureTextHeight(Memo1.TextSettings.Font, Memo1.Text);
LLines := Round(Memo1.ContentBounds.Height / LTextHeight);
end;
This is the rough way i use to calculate the number of lines an TMemo (styled) under firemonkey :
type
_TStyledMemoProtectedAccess = class(TStyledMemo);
procedure TALStyledMemo.OnApplyStyleLookupImpl(sender: Tobject);
Var I, j, k, l: integer;
begin
// TALStyledMemo
// TStyledMemo
// TLayout
// TActiveStyleObject
// TLayout
// TScrollBar
// TScrollBar
// TLayout
// TSmallScrollBar
// TSmallScrollBar
// TScrollContent
for I := 0 to controls.Count - 1 do begin
if (controls[i] is TStyledMemo) then begin // << TStyledMemo
fStyledMemo := TStyledMemo(controls[i]);
end;
end;
end;
function TALStyledMemo.getLineHeight: Single;
begin
if fStyledMemo <> nil then result := _TStyledMemoProtectedAccess(fStyledMemo).GetLineHeight
else result := 0;
end;
function TALStyledMemo.getLineCount: integer;
var aLineHeight: Single;
begin
aLineHeight := getLineHeight;
if compareValue(aLineHeight, 0, Tepsilon.Position) > 0 then result := round(ContentBounds.Height / aLineHeight)
else result := 0;
end;

TScrollBox with dynamically created Memos issue [duplicate]

I have a TScrollBox that has a RichEdit that is bigger than the scrollbox, so both side scrollbars appear in the scrollbox. Then I have a function DoTask that calls RichEdit.SetFocus.
When I scroll down to where I want to see part of the text control, and then call DoTask, the ScrollBox will automatically scroll to the top of the RichEdit. How can I avoid that?
As you wish, here are some suggestions:
Override SetFocusedControl in the form:
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
begin
if Control = RichEdit then
Result := True
else
Result := inherited SetFocusedControl(Control);
end;
Or:
type
TCustomMemoAccess = class(TCustomMemo);
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
var
Memo: TCustomMemoAccess;
Scroller: TScrollingWinControl;
Pt: TPoint;
begin
Result := inherited SetFocusedControl(Control);
if (Control is TCustomMemo) and (Control.Parent <> nil) and
(Control.Parent is TScrollingWinControl) then
begin
Memo := TCustomMemoAccess(Control);
Scroller := TScrollingWinControl(Memo.Parent);
SendMessage(Memo.Handle, EM_POSFROMCHAR, Integer(#Pt), Memo.SelStart);
Scroller.VertScrollBar.Position := Scroller.VertScrollBar.Position +
Memo.Top + Pt.Y;
end;
end;
Interpose TScrollBox:
type
TScrollBox = class(Forms.TScrollBox)
protected
procedure AutoScrollInView(AControl: TControl); override;
end;
procedure TScrollBox.AutoScrollInView(AControl: TControl);
begin
if not (AControl is TCustomMemo) then
inherited AutoScrollInView(AControl);
end;
Or:
procedure TScrollBox.AutoScrollInView(AControl: TControl);
begin
if (AControl.Top > VertScrollBar.Position + ClientHeight) xor
(AControl.Top + AControl.Height < VertScrollBar.Position) then
inherited AutoScrollInView(AControl);
end;
Or use any creative combination of all of the above. How and when you like it to be scrolled only you know.
the simpliest solution would be
var a, b : Integer;
begin
a := ScrollBox1.VertScrollBar.Position;
b := ScrollBox1.HorzScrollBar.Position;
richEdit1.SetFocus;
ScrollBox1.VertScrollBar.Position:=a ;
ScrollBox1.HorzScrollBar.Position:=b ;
end;
Without hacking into VCL/deriving custom components there's only one solution - TForm.SetFocusedControl override + re-setting the positions of scrollbars as said above. One thing I added is disabling/enabling window redraw to avoid ugly jumps.
Here's my final snippet:
sbContainer is TScrollBox and NoScrCtrl is a control laying inside it which gets focus but we don't want it to be scrolled-in-view.
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
var hpos, vpos: integer;
begin
if Control = NoScrCtrl then
begin
sbContainer.Perform(WM_SETREDRAW, WPARAM(False), 0);
hpos := sbContainer.HorzScrollBar.Position;
vpos := sbContainer.VertScrollBar.Position;
Result := inherited SetFocusedControl(Control);
sbContainer.HorzScrollBar.Position := hpos;
sbContainer.VertScrollBar.Position := vpos;
sbContainer.Perform(WM_SETREDRAW, WPARAM(True), 0);
sbContainer.Refresh;
end
else
Result := inherited SetFocusedControl(Control);
end;
To disable scroll-into-view behavior from my main form, I used this solution: (C++Builder)
bool __fastcall TMainForm::SetFocusedControl(TWinControl *Control) {
LockWindowUpdate(Handle);
int vpos = VertScrollBar->Position;
int hpos = HorzScrollBar->Position;
bool result = TForm::SetFocusedControl(Control);
if (VertScrollBar->Position != vpos) {
VertScrollBar->Position = vpos;
}
if (HorzScrollBar->Position != hpos) {
HorzScrollBar->Position = hpos;
}
LockWindowUpdate(0);
return result;
}

TEdit not redrawing correctly with Invalidate in Delphi 5

There's a problem with the TScrollBox in Delphi 5 when using Cirtix, on some systems, when a user scrolls by clicking the button at the top or bottom of the end of scrollbar the whole application freezes. We had the issue in QucikReports previews initially and got round it by implementing our own scrollbars in the TScrollBox.
We now have a piece of bespoke work that uses a TScrollBox and the client is reporting a similar problem so I'm working round it in the same way. I hide the TScrollBox scrollbars and add in my own. When those are clicked I call the following.
Note, this test code is not currently running in Citrix, I've tested on XP and Window 7.
I am turning off redrawing of the control, moving all the child controls, then turning drawing back on and calling Invalidate. I would expect invalidate to fully redraw the control but that's not happening.
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
try
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if (FScrollBox.Controls[I] = FVScrollBar) or (FScrollBox.Controls[I] = FHScrollBar) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
InvalidateEverything(FScrollBox);
end;
Code to redraw controls
procedure TScrollBoxScrollReplacement.InvalidateEverything(AControl: TControl);
var
I: Integer;
begin
AControl.Invalidate();
if (AControl is TWinControl) then
for I := 0 to TWinControl(AControl).ControlCount - 1 do
InvalidateEverything(TWinControl(AControl).Controls[I]);
end;
I added in the Invalidate, Refresh and Reapint and loop through all child controls in an effort to get it working, but still no luck. The edit boxes look as follows:
If I set Visible to false and back to true then they'll redraw correctly, but there is obviously a horrible flicker. They also redraw correctly if I minimise the maximise the window, or drag it off and on the screen.
Any help would be much appreciated.
edit : Some info about the answers.
Users looking for a solution, I'd recommend you try both. David's and Sertac's. David's looks like it is the correct solution according to Microsoft's documentation. However, with the Delphi scrollbox, labels placed directly in the scrollbox flicker, where are labels placed in groupboxes in the scrollbox are perfectly smooth. I think this might be an issue with all components that don't descend from TWinControl. Scrolling itself is smoother with David's solution, but there's less flicking using WM_SETREDRAW and RedrawWindow. I would have liked to accept both as answers as both have their advantages and disadvantages.
edit : Code for the whole class below
To test just add a scrollbox with some controls to your form and call
TScrollBoxScrollReplacement.Create(ScrollBox1);
.
unit ScrollBoxScrollReplacement;
interface
uses extctrls, stdctrls, SpScrollBox, forms, Controls, classes, Messages, Windows, Sysutils, Math;
type
TScrollBoxScrollReplacement = class(TComponent)
private
FLastVScrollPos: Integer;
FLastHScrollPos: Integer;
FScrollBox: TScrollBox;
FVScrollBar: TScrollBar;
FHScrollBar: TScrollBar;
FVScrollBarVisible: Boolean;
FHScrollBarVisible: Boolean;
FCornerPanel: TPanel;
FMaxRight: Integer;
FMaxBottom: Integer;
FOriginalResizeEvent: TNotifyEvent;
FOriginalCanResizeEvent: TCanResizeEvent;
FInScroll: Boolean;
function GetHScrollHeight: Integer;
function GetVScrollWidth: Integer;
procedure ReplaceScrollBars;
function SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
procedure ScrollBoxResize(Sender: TObject);
procedure ScrollBarEnter(Sender: TObject);
procedure PositionScrollBars;
procedure Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
procedure ScrollControls(x, y: Integer);
procedure CalculateControlExtremes();
procedure ResetVScrollBarRange;
procedure ResetHScrollBarRange;
function IsReplacementControl(AControl: TControl): Boolean;
property HScrollHeight: Integer read GetHScrollHeight;
property VScrollWidth: Integer read GetVScrollWidth;
procedure ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
public
constructor Create(AScrollBox: TScrollBox); reintroduce; overload;
destructor Destroy(); override;
procedure ResetScrollBarRange();
procedure BringReplacementControlsToFront();
end;
implementation
{ TScrollBoxScrollReplacement }
constructor TScrollBoxScrollReplacement.Create(AScrollBox: TScrollBox);
begin
// Set up the scrollbox as our owner so we're destroyed when the scrollbox is
inherited Create(AScrollBox);
FScrollBox := AScrollBox;
ReplaceScrollBars();
// We make a note of any existing resize and can resize events so we can call them to make sure we don't break anything
FOriginalResizeEvent := FScrollBox.OnResize;
FScrollBox.OnResize := ScrollBoxResize;
FOriginalCanResizeEvent := FScrollBox.OnCanResize;
FScrollBox.OnCanResize := ScrollBoxCanResize;
end;
// This is called (unintuitively) when controls are moved within the scrollbox. We can use this to reset our scrollbar ranges
procedure TScrollBoxScrollReplacement.ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
begin
if (not FInScroll) then
begin
ResetScrollBarRange();
BringReplacementControlsToFront();
end;
if (Assigned(FOriginalCanResizeEvent)) then
FOriginalCanResizeEvent(Sender, NewWidth, NewHeight, Resize);
end;
procedure TScrollBoxScrollReplacement.ScrollBoxResize(Sender: TObject);
begin
if (Assigned(FOriginalResizeEvent)) then
FOriginalResizeEvent(Sender);
ResetScrollBarRange();
end;
// Hides the original scrollbars and adds in ours
procedure TScrollBoxScrollReplacement.ReplaceScrollBars();
begin
FVScrollBar := SetUpScrollBar(FScrollBox.VertScrollBar, sbVertical);
FVScrollBarVisible := FVScrollBar.Visible;
FHScrollBar := SetUpScrollBar(FScrollBox.HorzScrollBar, sbHorizontal);
FHScrollBarVisible := FHScrollBar.Visible;
FCornerPanel := TPanel.Create(FScrollBox);
FCornerPanel.Parent := FScrollBox;
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.PositionScrollBars();
begin
// Align our scrollbars correctly
FVScrollBar.Top := 0;
FVScrollBar.Left := FScrollBox.ClientWidth - FVScrollBar.Width;
FVScrollBar.Height := FScrollBox.ClientHeight - HScrollHeight;
// FVScrollBar.BringToFront();
FHScrollBar.Left := 0;
FHScrollBar.Top := FScrollBox.ClientHeight - FHScrollBar.Height;
FHScrollBar.Width := FScrollBox.ClientWidth - VScrollWidth;
// FHScrollBar.BringToFront();
// If both scrollbars are visible we'll put a panel in the corner so we can't see components through it
if (FVScrollBar.Visible) and (FHScrollBar.Visible) then
begin
FCornerPanel.Left := FHScrollBar.Width;
FCornerPanel.Top := FVScrollBar.Height;
FCornerPanel.Width := FVScrollBar.Width;
FCornerPanel.Height := FHScrollBar.Height;
FCornerPanel.Visible := True;
// FCornerPanel.BringToFront();
end
else
FCornerPanel.Visible := False;
end;
procedure TScrollBoxScrollReplacement.ResetScrollBarRange();
begin
CalculateControlExtremes();
ResetVScrollBarRange();
ResetHScrollBarRange();
PositionScrollBars();
end;
procedure TScrollBoxScrollReplacement.ResetVScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the right of the screen, but there are controls off the left then we'll scroll right.
ScrollMax := FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height;
if (ScrollMax < 0) and (FLastVScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastVScrollPos);
ScrollControls(0, ScrollAmount);
FLastVScrollPos := FLastVScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FVScrollBar.Max := Max(FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height + FLastVScrollPos, 0);
FVScrollBar.Visible := (FVScrollBar.Max > 0) and FVScrollBarVisible;
end;
procedure TScrollBoxScrollReplacement.ResetHScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the bottom of the screen, but there are controls off the top then we'll scroll up.
ScrollMax := FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width;
if (ScrollMax < 0) and (FLastHScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastHScrollPos);
ScrollControls(ScrollAmount, 0);
FLastHScrollPos := FLastHScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FHScrollBar.Max := Max(FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width + FLastHScrollPos, 0);
FHScrollBar.Visible := (FHScrollBar.Max > 0) and FHScrollBarVisible;
end;
function TScrollBoxScrollReplacement.SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
begin
Result := TScrollBar.Create(FScrollBox);
Result.Visible := AControlScrollBar.Visible;
Result.Parent := FScrollBox;
Result.Kind := AKind;
Result.Ctl3D := False;
Result.Max := AControlScrollBar.Range;
Result.OnEnter := ScrollBarEnter;
Result.OnScroll := Scroll;
Result.SmallChange := 5;
Result.LargeChange := 20;
AControlScrollBar.Visible := False;
end;
destructor TScrollBoxScrollReplacement.Destroy;
begin
inherited;
end;
procedure TScrollBoxScrollReplacement.ScrollBarEnter(Sender: TObject);
begin
// We just call this here to make sure our ranges are set correctly - a backup in case things go wrong
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.Scroll(Sender: TObject;
ScrollCode: TScrollCode; var ScrollPos: Integer);
var
Change: Integer;
begin
ResetScrollBarRange();
if (Sender = FVScrollBar) then
begin
Change := FLastVScrollPos - ScrollPos;
ScrollControls(0, Change);
FLastVScrollPos := ScrollPos;
end
else if (Sender = FHScrollBar) then
begin
Change := FLastHScrollPos - ScrollPos;
ScrollControls(Change, 0);
FLastHScrollPos := ScrollPos;
end;
end;
// Moves all the controls in the scrollbox except for the scrollbars we've added
{procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
FInScroll := True;
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
FInScroll := False;
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
RedrawWindow(FSCrollBox.Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
end; }
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
// works out where our right most and bottom most controls are so we can set the scrollbars correctly
procedure TScrollBoxScrollReplacement.CalculateControlExtremes();
var
I: Integer;
Right: Integer;
Bottom: Integer;
begin
FMaxRight := 0;
FMaxBottom := 0;
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
Right := FScrollBox.Controls[I].Left + FScrollBox.Controls[I].Width;
Bottom := FScrollBox.Controls[I].Top + FScrollBox.Controls[I].Height;
FMaxRight := Max(FMaxRight, Right);
FMaxBottom := Max(FMaxBottom, Bottom);
end;
end;
function TScrollBoxScrollReplacement.GetHScrollHeight: Integer;
begin
if (FHScrollBar.Visible) then
Result := FHScrollBar.Height
else
Result := 0;
end;
function TScrollBoxScrollReplacement.GetVScrollWidth: Integer;
begin
if (FVScrollBar.Visible) then
Result := FVScrollBar.Width
else
Result := 0;
end;
// Returns true if the passed control is one of the controls we've added
function TScrollBoxScrollReplacement.IsReplacementControl(
AControl: TControl): Boolean;
begin
Result := (AControl = FVScrollBar) or (AControl = FHScrollBar) or (AControl = FCornerPanel);
end;
procedure TScrollBoxScrollReplacement.BringReplacementControlsToFront;
begin
FVScrollBar.BringToFront();
FHScrollBar.BringToFront();
FCornerPanel.BringToFront();
end;
end.
I found that your code started working once I remove the two WM_SETREDRAW messages. That's your fundamental problem. You will need to remove the WM_SETREDRAW messages.
That will no doubt mean you still need to solve your problem with flickering, but that's a different problem. My quick experiments suggest that DeferWindowPos could solve that problem. For example:
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
Your non-windowed controls will still flicker, but you can make them windowed, or indeed put the whole content of the scroll box inside a windowed control. Heck, if you just did that, it would be enough to solve the problem!
For what it is worth, my trials indicate that DeferWindowPos gives smoother scrolling than WM_SETREDRAW and RedrawWindow. But these tests were hardly exhaustive and you might find different outcomes in your app.
Some asides regarding your code:
Your use of try/finally is incorrect. The pattern must be:
BeginSomething;
try
Foo;
finally
EndSomething;
end;
You get that wrong with your calls to SendMessage.
And you use an incorrect cast in InvalidateEverything. You cannot blindly cast a TControl to TWinControl. That said, that function does no good. You can remove it altogether. What it is attempting to do can be performed with a single call to Invalidate of the parent control.
You can replace your
FScrollBox.Invalidate();
with
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
to have all controls invalidated and updated properly. RDW_ERASE is for erasing the previous positions of controls and RDW_ALLCHILDREN is for taking care of windowed controls inside. Non-win controls like labels should already be repainted because of RDW_INVALIDATE.
Although this approach may help avoiding the flicker that you observe, it may also cause some loss of smoothness of scrolling while thumb tracking. That's because the scroll position might need to be updated more often than a paint cycle is processed. To circumvent this, instead of invalidating you can update the control positions immediately:
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);

Resources