Serious FireMonkey performance issues when there are a lot of controls at screen - performance

It's already a while we are working with FireMonkey at office. After a while we noticed it wasn't exactly so lightning fast due to GPU acceleration as Embarcadero tells us.
So we built a basic application just for testing FireMonkey performance. Basically it's a form with a panel on the bottom (alBottom) that works as status bar and an all client (alClient) Panel. The panel on the bottom has a progressbar and an animation.
We added a method to the form that frees whatever control is present in the all client panel and fulfil it with cells of a custom type and a "mouse over" style and update the animation, the progress bar and the form's caption with info about the fulfilling progress. The most important info is the required time.
Finally we added such method to the OnResize of the form, run the application and maximized the form (1280x1024).
The result with XE2 was really slow. It took around 11 seconds. In addition since the panel is fulfilled till the application is ready to receive user input there is an additional delay of about 10 seconds (like freezing). For an overall of 21 seconds.
With XE3 the situation got worst. For the same operation it took an overall of 25 seconds (14 + 11 freezing).
And rumours tell XE4 is going to be a lot worst of XE3.
This is quite scaring considering exactly the same application, using VCL instead of FireMonkey and using SpeedButtons in order to have the same "mouse over effect" takes just 1.5 seconds!!! So the problem clearly reside in some internal FireMonkey engine problem(s).
I opened a QC (#113795) and a (paid) ticket to embarcadero support but nothing they won't solve it.
I seriously don't understand how they can ignore such heavy issue. For our enterprise is being a show-stopper and a deal breaker. We cannot offer commercial software to our customer with such poor performance. Earlier or later we will be forced to move to another platform (BTW: the same code Delphi Prism with WPF takes 1.5 seconds as the VCL one).
If anybody has any idea about how to solve the issue or try to improve this test performance and want to help I would be really glad of it.
Thank you in advance.
Bruno Fratini
The application is the following one:
unit Performance01Main;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Objects;
const
cstCellWidth = 45;
cstCellHeight = 21;
type
TCell = class(TStyledControl)
private
function GetText: String;
procedure SetText(const Value: String);
function GetIsFocusCell: Boolean;
protected
FSelected: Boolean;
FMouseOver: Boolean;
FText: TText;
FValue: String;
procedure ApplyStyle; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure DoMouseEnter; override;
procedure DoMouseLeave; override;
procedure ApplyTrigger(TriggerName: string);
published
property IsSelected: Boolean read FSelected;
property IsFocusCell: Boolean read GetIsFocusCell;
property IsMouseOver: Boolean read FMouseOver;
property Text: String read GetText write SetText;
end;
TFormFireMonkey = class(TForm)
StyleBook: TStyleBook;
BottomPanel: TPanel;
AniIndicator: TAniIndicator;
ProgressBar: TProgressBar;
CellPanel: TPanel;
procedure FormResize(Sender: TObject);
procedure FormActivate(Sender: TObject);
protected
FFocused: TCell;
FEntered: Boolean;
public
procedure CreateCells;
end;
var
FormFireMonkey: TFormFireMonkey;
implementation
uses
System.Diagnostics;
{$R *.fmx}
{ TCell }
procedure TCell.ApplyStyle;
begin
inherited;
ApplyTrigger('IsMouseOver');
ApplyTrigger('IsFocusCell');
ApplyTrigger('IsSelected');
FText:= (FindStyleResource('Text') as TText);
if (FText <> Nil) then
FText.Text := FValue;
end;
procedure TCell.ApplyTrigger(TriggerName: string);
begin
StartTriggerAnimation(Self, TriggerName);
ApplyTriggerEffect(Self, TriggerName);
end;
procedure TCell.DoMouseEnter;
begin
inherited;
FMouseOver:= True;
ApplyTrigger('IsMouseOver');
end;
procedure TCell.DoMouseLeave;
begin
inherited;
FMouseOver:= False;
ApplyTrigger('IsMouseOver');
end;
function TCell.GetIsFocusCell: Boolean;
begin
Result:= (Self = FormFireMonkey.FFocused);
end;
function TCell.GetText: String;
begin
Result:= FValue;
end;
procedure TCell.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
var
OldFocused: TCell;
begin
inherited;
FSelected:= not(FSelected);
OldFocused:= FormFireMonkey.FFocused;
FormFireMonkey.FFocused:= Self;
ApplyTrigger('IsFocusCell');
ApplyTrigger('IsSelected');
if (OldFocused <> Nil) then
OldFocused.ApplyTrigger('IsFocusCell');
end;
procedure TCell.SetText(const Value: String);
begin
FValue := Value;
if Assigned(FText) then
FText.Text:= Value;
end;
{ TForm1 }
procedure TFormFireMonkey.CreateCells;
var
X, Y: Double;
Row, Col: Integer;
Cell: TCell;
T: TTime;
// Workaround suggested by Himself 1
// Force update only after a certain amount of iterations
// LP: Single;
// Workaround suggested by Himself 2
// Force update only after a certain amount of milliseconds
// Used cross-platform TStopwatch as suggested by LU RD
// Anyway the same logic was tested with TTime and GetTickCount
// SW: TStopWatch;
begin
T:= Time;
Caption:= 'Creating cells...';
{$REGION 'Issue 2 workaround: Update form size and background'}
// Bruno Fratini:
// Without (all) this code the form background and area is not updated till the
// cells calculation is finished
BeginUpdate;
Invalidate;
EndUpdate;
// Workaround suggested by Philnext
// replacing ProcessMessages with HandleMessage
// Application.HandleMessage;
Application.ProcessMessages;
{$ENDREGION}
// Bruno Fratini:
// Update starting point step 1
// Improving performance
CellPanel.BeginUpdate;
// Bruno Fratini:
// Freeing the previous cells (if any)
while (CellPanel.ControlsCount > 0) do
CellPanel.Controls[0].Free;
// Bruno Fratini:
// Calculating how many rows and columns can contain the CellPanel
Col:= Trunc(CellPanel.Width / cstCellWidth);
if (Frac(CellPanel.Width / cstCellWidth) > 0) then
Col:= Col + 1;
Row:= Trunc(CellPanel.Height / cstCellHeight);
if (Frac(CellPanel.Height / cstCellHeight) > 0) then
Row:= Row + 1;
// Bruno Fratini:
// Loop variables initialization
ProgressBar.Value:= 0;
ProgressBar.Max:= Row * Col;
AniIndicator.Enabled:= True;
X:= 0;
Col:= 0;
// Workaround suggested by Himself 2
// Force update only after a certain amount of milliseconds
// Used cross-platform TStopwatch as suggested by LU RD
// Anyway the same logic was tested with TTime and GetTickCount
// SW:= TStopwatch.StartNew;
// Workaround suggested by Himself 1
// Force update only after a certain amount of iterations
// LP:= 0;
// Bruno Fratini:
// Loop for fulfill the Width
while (X < CellPanel.Width) do
begin
Y:= 0;
Row:= 0;
// Bruno Fratini:
// Loop for fulfill the Height
while (Y < CellPanel.Height) do
begin
// Bruno Fratini:
// Cell creation and bounding into the CellPanel
Cell:= TCell.Create(CellPanel);
Cell.Position.X:= X;
Cell.Position.Y:= Y;
Cell.Width:= cstCellWidth;
Cell.Height:= cstCellHeight;
Cell.Parent:= CellPanel;
// Bruno Fratini:
// Assigning the style that gives something like Windows 7 effect
// on mouse move into the cell
Cell.StyleLookup:= 'CellStyle';
// Bruno Fratini:
// Updating loop variables and visual controls for feedback
Y:= Y + cstCellHeight;
Row:= Row + 1;
ProgressBar.Value:= ProgressBar.Value + 1;
// Workaround suggested by Himself 1
// Force update only after a certain amount of iterations
// if ((ProgressBar.Value - LP) >= 100) then
// Workaround suggested by Himself 2
// Force update only after a certain amount of milliseconds
// Used cross-platform TStopwatch as suggested by LU RD
// Anyway the same logic was tested with TTime and GetTickCount
// if (SW.ElapsedMilliseconds >= 30) then
// Workaround suggested by Philnext with Bruno Fratini's enhanchment
// Skip forcing refresh when the form is not focused for the first time
// This avoid the strange side effect of overlong delay on form open
// if FEntered then
begin
Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) +
' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value));
{$REGION 'Issue 4 workaround: Forcing progress and animation visual update'}
// Bruno Fratini:
// Without the ProcessMessages call both the ProgressBar and the
// Animation controls are not updated so no feedback to the user is given
// that is not acceptable. By the other side this introduces a further
// huge delay on filling the grid to a not acceptable extent
// (around 20 minutes on our machines between form maximization starts and
// it arrives to a ready state)
// Workaround suggested by Philnext
// replacing ProcessMessages with HandleMessage
// Application.HandleMessage;
Application.ProcessMessages;
{$ENDREGION}
// Workaround suggested by Himself 1
// Force update only after a certain amount of iterations
// LP:= ProgressBar.Value;
// Workaround suggested by Himself 2
// Force update only after a certain amount of milliseconds
// Used cross-platform TStopwatch as suggested by LU RD
// Anyway the same logic was tested with TTime and GetTickCount
// SW.Reset;
// SW.Start;
end;
end;
X:= X + cstCellWidth;
Col:= Col + 1;
end;
// Bruno Fratini:
// Update starting point step 2
// Improving performance
CellPanel.EndUpdate;
AniIndicator.Enabled:= False;
ProgressBar.Value:= ProgressBar.Max;
Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) +
' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value));
// Bruno Fratini:
// The following lines are required
// otherwise the cells won't be properly paint after maximizing
BeginUpdate;
Invalidate;
EndUpdate;
// Workaround suggested by Philnext
// replacing ProcessMessages with HandleMessage
// Application.HandleMessage;
Application.ProcessMessages;
end;
procedure TFormFireMonkey.FormActivate(Sender: TObject);
begin
// Workaround suggested by Philnext with Bruno Fratini's enhanchment
// Skip forcing refresh when the form is not focused for the first time
// This avoid the strange side effect of overlong delay on form open
FEntered:= True;
end;
procedure TFormFireMonkey.FormResize(Sender: TObject);
begin
CreateCells;
end;
end.

I tried your code, it takes 00:10:439 on my PC on XE3 to fill screen with cells. By disabling these lines:
//ProgressBar.Value:= ProgressBar.Value + 1;
//Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) +
// ' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value));
...
//Application.ProcessMessages;
This goes down to 00:00:106 (!).
Updating visual controls (such as ProgressBar or Form.Caption) is very expensive. If you really think you need that, do that only every 100-th iteration, or better, only every 250 processor ticks.
If that does not help with performance, please run your code with these lines disabled and update the question.
Further, I've added code to test the repainting time:
T:= Time;
// Bruno Fratini:
// The following lines are required
// otherwise the cells won't be properly paint after maximizing
//BeginUpdate;
Invalidate;
//EndUpdate;
Application.ProcessMessages;
Caption := Caption + ', Repaint time: '+FormatDateTime('nn:ss:zzz', Time - T);
When run for a first time, creating all the controls takes 00:00:072, repainting takes 00:03:089. So it's not the object management but the first time repainting which is slow.
Second time repainting is considerably faster.
Since there's a discussion in comments, here's how you do progress updates:
var LastUpdateTime: cardinal;
begin
LastUpdateTime := GetTickCount - 250;
for i := 0 to WorkCount-1 do begin
//...
//Do a part of work here
if GetTickCount-LastUpdateTime > 250 then begin
ProgressBar.Position := i;
Caption := IntToStr(i) + ' items done.';
LastUpdateTime := GetTickCount;
Application.ProcessMessages; //not always needed
end;
end;
end;

I only have XE2 and the code is not exactlly the same but, as said by some other guys the pb seems to be on the
Application.ProcessMessages;
line.
So I sugess to 'refresh' your components with realign ex :
ProgressBar.Value:= ProgressBar.Value + 1;
Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) +
' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value));
// in comment : Application.ProcessMessages;
// New lines : realign for all the components needed to be refreshes
AniIndicator.Realign;
ProgressBar.Realign;
On my PC, a 210 Cells screen is generated in 0.150 s instead of 3.7 s with the original code, to be tested in your environnement...

Why are you testing
"Repaint", "InvalidateRect", "Scene.EndUpdate"
I can see from your code that most expensive operation is recreating controls.
And why are you doing it in OnResize event (maybe put some button for recreating controls)
this loop alone can eat like 30% of execution time
while (CellPanel.ControlsCount > 0) do
CellPanel.Controls[0].Free;
it should be like: (avoid list memory copy after each free)
for i := CellPanel.ControlsCount - 1 downto 0 do
CellPanel.Controls[i].Free;
and don't run ProcessMessages in loop (or at least run only in every 10th iteration or so)
use AQTime to profile your code (it will show what is tacking that long)

Related

Windows slowing down with mouse pointer on destop

I've a strange problem. I started approx. 160 processes. Now, if the mouse pointer is on the Desktop, some actions which used to take 100ms, now take 10 seconds although the total load of the system is 13-16%. Even thrid party programs like processhacker slowing down and doesn't refresh their gui. If I move the mouse pointer over some window no matter which one (could be notepad) even the taskbar can help all goes back to normal. Processhacker is refreshing his lists and the responsivness is back to 100ms.
Since Microsoft-Support won't help use - since or processes are programmed in Borland-Delphi we have no idea how to find out what's going on here.
A colleague tries to reproduce the effect with this little test program:
unit Unit1;
interface
uses
Forms,
ExtCtrls,
Classes,
Controls,
StdCtrls;
const
DEFAULT_INTERVAL = 31;
MOD_VALUE = 5;
MOD_INTERVAL = DEFAULT_INTERVAL * MOD_VALUE;
DEVIATION_BLACK = 2;
DEVIATION_RED = 10;
type
TForm1 = class(TForm)
Label1: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
Timer: TTimer;
lastTime: TDateTime;
procedure OnTimer(Sender: TObject);
procedure SetLabel(lbl: TLabel);
end;
var
Form1: TForm1;
GCounterT: Integer;
implementation
uses
Windows,
Graphics,
SysUtils,
DateUtils;
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
Self.DoubleBuffered := True;
Timer := TTimer.Create(nil);
Timer.Interval := DEFAULT_INTERVAL;
Timer.OnTimer := OnTimer;
GCounterT := 0;
lastTime := Now();
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Timer.Free;
end;
procedure TForm1.OnTimer(Sender: TObject);
begin
Inc(GCounterT);
if (GCounterT mod MOD_VALUE) = 0 then begin
SetLabel(Label1);
GCounterT := 0;
end;
end;
procedure TForm1.SetLabel(lbl: TLabel);
var
newValue: string;
nowTime: TDateTime;
msDiff: Integer;
newColor: TColor;
begin
if IsIconic(Application.Handle) then Exit;
nowTime := Now();
msDiff := MilliSecondsBetween(nowTime, lastTime);
lastTime := nowTime;
newValue := Format('TTimer: %s dev: %d',[FormatDateTime('ss.zzz', nowTime), msDiff - MOD_INTERVAL]);
if (msDiff <= (MOD_INTERVAL + DEVIATION_BLACK))
and (msDiff >= (MOD_INTERVAL - DEVIATION_BLACK)) then
newColor := clGreen
else if (msDiff <= (MOD_INTERVAL + DEVIATION_RED))
and (msDiff >= (MOD_INTERVAL - DEVIATION_RED)) then
newColor := clBlack
else
newColor := clRed;
try
lbl.Font.Color := newColor;
lbl.Caption := newValue;
except
end;
end;
end.
The effect in not as strong as with the original processes, but it's reproduceable.
If one starts 180 of this you can see the same effect only the slowdown is not that severe.
Update Aug 04:
I've added a screenshot from a WPA-Analyze-Session. Here one can see the sequence. Starting with mouse on a Window, then Desktop, Window, Desktop and ending with mouse on Window.
You can see, that the Thread: CSwitch count is going nearly half if the mouse is on the Desktop. What you also could see is that the system load is between 10-17% the whole time.
After we managed to add debug-symbols to some of our processes, we found the issue in the Delphi-VCL/Forms.pas.
In a new trace, with debug-symbols, we saw that the Application.DoMouseIdle method spends a lot of time finding VCLWindows, get Parents of these and so on.
The source of the slowdown is the "FindDragTarget" method. Our processes need no drag'n'drop functionality and they need no hint showing somewhere. So we cut this function call out of the code, which was not easy.
Now everything is running fast undependend from the mouse position.

Windows progress bar update animation [duplicate]

I have ran into what I consider to be a progress bar bug on Windows 7. To demonstrate the bug I created a WinForm application with a button and a progress bar. In the button's 'on-click' handle I have the following code.
private void buttonGo_Click(object sender, EventArgs e)
{
this.progressBar.Minimum = 0;
this.progressBar.Maximum = 100;
this.buttonGo.Text = "Busy";
this.buttonGo.Update();
for (int i = 0; i <= 100; ++i)
{
this.progressBar.Value = i;
this.Update();
System.Threading.Thread.Sleep(10);
}
this.buttonGo.Text = "Ready";
}
The expected behavior is for the progress bar to advance to 100% and then the button text to change to 'Ready'. However, when developing this code on Windows 7, I noticed that the progress bar would rise to about 75% and then the button text would change to 'Ready'. Assuming the code is synchronous, this should not happen!
On further testing I found that the exact same code running on Windows Server 2003 produced the expected results. Furthermore, choosing a non aero theme on Windows 7 produces the expected results.
In my mind, this seems like a bug. Often it is very hard to make a progress bar accurate when the long operation involves complex code but in my particular case it was very straight forward and so I was little disappointed when I found the progress control did not accurately represent the progress.
Has anybody else noticed this behavior? Has anybody found a workaround?
It has to do with the animation of the progress bar. If your progress bar is at 0% and you set it to 100% then it will not jump there, but animate the progress bar smoothly filling up. If this is too slow, you will be done before the progress bar finished animating. So even though you have already set it to 80, 90 and 100%, the animation still lags behind.
I never found a way to turn this off, however I have a workaround. The animation is only being done if you increment the progress bar. If you move it backwards, it will immediately jump to that position. So if I want the progress bar to be at x% (x != 100) then I move it to x+1 and then to x. If I want it at 100% I move it to 100, 99 and 100%. (Or whatever values you use, you get the idea.) This works fast enough to not to be visible, and you can leave this code in for previous Windows versions as well. (though I don't)
HTH
I had the same problem. Fozi's tipp was helping me. Before setting a new value I have set the value + 1. To make this work also for 100% the maximum must be increased before. The following worked fine for me.
if (NewValue < progressBar.Maximum)
{
progressBar.Value = NewValue + 1;
progressBar.Value--;
}
else
{
progressBar.Maximum++;
progressBar.Value = progressBar.Maximum;
progressBar.Value--;
progressBar.Maximum--;
}
I think the original problem is related to timing and Win7's (or Aero's) animation mechanism for the progress bar.
This Sub is on the form that contains the progress bar (pBar).
It varies the bar's .Maximum and keeps .Value fixed at 10 for percent completes of 1 to 99. The bar's .Minimum is set to 0 at design time.
This sorted out the problem for me.
Public Sub UpdateStatusPC(ByVal pc As Integer)
Try
If pc < 0 Then
pBar.Maximum = 100
pBar.Value = 0
ElseIf pc > 100 Then
pBar.Maximum = 100
pBar.Value = 100
ElseIf pc = 0 Then
pBar.Maximum = 10
pBar.Value = 0
Else
pBar.Value = 10
pBar.Maximum = 10 / CDbl(pc / 100.0)
End If
pBar.Update()
Catch ex As Exception
MsgBox("UpdateStatusPC: " & ex.Message)
End Try
End Sub
To Delphi users facing the same problem: Below is a unit called ProgressBarFix that you can use to automatically patch the problem without worrying about changing your progress bar code -- just include ProgressBarFix in your form's interface "uses" clause after the ComCtrls uses and you'll get the workaround automatically:
unit ProgressBarFix;
(* The standard progress bar fails under Windows theming -- it fails to animate
all the way to the right side. C.f.,
http://stackoverflow.com/questions/2217688/windows-7-aero-theme-progress-bar-bug
To work around the problem, include ProgressBarFix in the interface section's
"uses" clause *after* ComCtrls (this replaces the TProgressBar definition in
ConCtrls with the one here, effectively allowing the control defined on the
form to be replaced with the patch version.
c.f., http://www.deltics.co.nz/blog/?p=222and http://melander.dk/articles/splitter *)
interface
uses ComCtrls ;
type TProgressBar = class(ComCtrls.TProgressBar)
private
procedure SetPosition(Value: Integer);
function GetPosition: Integer;
published
property Position: Integer read GetPosition write SetPosition default 0;
end ;
implementation
{ TProgressBar }
function TProgressBar.GetPosition: Integer;
begin
result := inherited Position
end;
procedure TProgressBar.SetPosition(Value: Integer);
begin
if Value=inherited Position then
exit ;
if value<Max then begin
inherited Position := value+1 ;
inherited Position := value
end else begin
Max := Max+1 ;
inherited Position := Max ;
inherited Position := value ;
Max := Max-1
end
end;
end.
Disable visual effect option "Animate controls and elements inside windows" in "Performance options". Then the progressbars won't be animated any longer.
I have seen similar issues with progress bars on Vista and Windows 7.
The key problem in my case was the blocking of the UI thread. (Like you do in your sample).
Windows does not like applications that don't respond to new messages in the message queue. If you spend too much time on one message, windows will mark your application as "not responsive". In Vista/Win7, windows also decides to stop updating your application window.
As a workaround, you could put the actual work on a background worker, or call Application.DoEvents() every once in a while. You do need to make sure that your progress bar window is modal, or else the DoEvents() may enable new commands to start executing halfway through your background processing.
If that feels to kludgy, the more proper way is to do your background work on a BackgroundWorker thread. It comes with support for sending events to the UI thread to update the progress bar.
(09/2015) I just jumped from D6 to XE8. Having a number of issues. Including this TProgressBar thing. Tabled it for a while. Came across this (Erik Knowles) fix tonight. Fantastic. Except: the first scenario I ran through had a Max value of 9,770,880. And it (Erik Knowles' "original" fix) REALLY added to the time this process took (with all the extra actual updating of the ProgressBar).
So I expanded his class to reduce the amount of times the ProgressBar actually redraws itself. But ONLY IF the "original" Max value is greater than MIN_TO_REWORK_PCTS (I settled on 5000 here).
If so, the ProgressBar only updates itself HUNDO times (here I started with and pretty much settled on 100, hence the "HUNDO" name).
I accounted for some quirkiness at the Max value as well:
if Abs(FOriginalMax - value) <= 1 then
pct := HUNDO
I tested this against my original 9.8m Max. And, with this standalone test app:
:
uses
:
ProgressBarFix;
const
PROGRESS_PTS = 500001;
type
TForm1 = class(TForm)
Label1: TLabel;
PB: TProgressBar;
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
x: integer;
begin
PB.Min := 0;
PB.Max := PROGRESS_PTS;
PB.Position := 0;
for x := 1 to PROGRESS_PTS do
begin
//let's do something
//
Label1.Caption := Format('%d of %d',[x,PROGRESS_PTS]);
Update;
PB.Position := x;
end;
PB.Position := 0;
end;
end.
with PROGRESS_PTS values of:
10
100
1,000
10,000
100,000
1,000,000
It's smooth and "accurate" for all of these values - without really slowing anything down.
In testing, I was able to toggle my compiler directive DEF_USE_MY_PROGRESS_BAR to test both ways (this TProgressBar replacement vs the original).
Note that you might want to uncomment the call to Application.ProcessMessages.
Here is the (my "enhanced") ProgressBarFix source:
unit ProgressBarFix;
interface
uses
Vcl.ComCtrls;
type
TProgressBar = class(Vcl.ComCtrls.TProgressBar)
const
HUNDO = 100;
MIN_TO_REWORK_PCTS = 5000;
private
function GetMax: integer;
procedure SetMax(value: integer);
function GetPosition: integer;
procedure SetPosition(value: integer);
published
property Max: integer read GetMax write SetMax default 100;
property Position: integer read GetPosition write SetPosition default 0;
private
FReworkingPcts: boolean;
FOriginalMax: integer;
FLastPct: integer;
end;
implementation
function TProgressBar.GetMax: integer;
begin
result := inherited Max;
end;
procedure TProgressBar.SetMax(value: integer);
begin
FOriginalMax := value;
FLastPct := 0;
FReworkingPcts := FOriginalMax > MIN_TO_REWORK_PCTS;
if FReworkingPcts then
inherited Max := HUNDO
else
inherited Max := value;
end;
function TProgressBar.GetPosition: integer;
begin
result := inherited Position;
end;
procedure TProgressBar.SetPosition(value: integer);
var
pct: integer;
begin
//Application.ProcessMessages;
if value = inherited Position then
exit;
if FReworkingPcts then
begin
if Abs(FOriginalMax - value) <= 1 then
pct := HUNDO
else
pct := Trunc((value / FOriginalMax) * HUNDO);
if pct = FLastPct then
exit;
FLastPct := pct;
value := pct;
end;
if value < Max then
begin
inherited Position := Succ(value);
inherited Position := value;
end
else
begin
Max := Succ(Max);
inherited Position := Max;
inherited Position := value;
Max := Pred(Max);
end;
end;
end.

Scintilla Horizontal Scrollbar

How do I make it appear as if it were automatic like the vertical one?
The window is 300 wide so I tried setting SCI_SETSCROLLWIDTH to 300 and then less than 300 with SCI_SETSCROLLWIDTHTRACKING turned on but the scrollbar will either still always show or not show at all.
If you want to show/hide the horizontal SB, you need SCI_SETHSCROLLBAR(bool visible), but you need to know where the end of the line is. So you can try what I have below. It is fairly low impact since you are only looking at the currently visible lines.
Note that I use a Delphi wrapper for the scintilla control/DLL, but the calls can all be made with the regular scintilla messages (same basic names), and I have a few functions I use which are below as well. You could call this where you get the SCN_UPDATEUI message.
function GetFirstVisiblePos: Integer;
begin
Result := PositionFromPoint(0,0);
end;
function GetLastVisiblePos: Integer;
begin
Result := PositionFromPoint(clientwidth,clientheight);
end;
function GetFirstVisibleLine: Integer;
begin
Result := LineFromPosition(GetFirstVisiblePos);
end;
function GetLastVisibleLine: Integer;
begin
Result := LineFromPosition(GetLastVisiblePos);
end;
[...]
var
i: integer;
x, endPos: integer;
needHSB: boolean;
begin
if not WordWrap then //Only need to do this if not wordwrapped
begin
x := ClientWidth ;
needHSB := false;
//Check currently visible lines only
for i := GetFirstVisibleLine to GetLastVisibleLine do
begin
//GetXOffset adds left scroll spacing if we are already scrolled left some.
endPos := PointXFromPosition(GetLineEndPosition(i) ) - x + GetXOffset ;
needHSB := endPos > ClientWidth;
if needHSB then break; //once set, don't need to set again...
end;
SetHScrollBar( needHSB );
end;
end;
Try that and that should do what you are after (if I read the original question correctly). It worked for me, although I was after something a little different originally.
I needed a way to try and control the horizontal scroll width which the sci control does not do automatically (for me anyway; SCI_SETSCROLLWIDTHTRACKING seems to be what you'd use for this but I was never able to get to work (at least in the way it implies it should work in the docs). I came up with the code below. In my app the code is in SCN_UPDATEUI message area.
//Set new scroll width if there's a line longer than the current scroll
//width can show:
if not WordWrap then //Only need to do this if not wordwrapped
begin
//vars: i, x, endPos, LeftScrollPos : integer;
x := ClientWidth ;
//Check currently visible lines only
for i := GetFirstVisibleLine to GetLastVisibleLine do
begin
//GetXOffset adds extra left scroll space if we are already scrolled left some.
//24 is just a fudge factor to add a little visual space after a long line.
endPos := PointXFromPosition(GetLineEndPosition(i) ) - x + GetXOffset + 24;
if endPos > 2000 then //Greater than the control's default
if endPos > ( GetScrollWidth ) then //Only need to proceed if we need more room
begin
LeftScrollPos := GetXOffset; //Store our current left scroll position
SetScrollWidth( endPos ) ; //This sets left scroll to 0, so...
SetXOffset( LeftScrollPos ); //Restore current left scroll position
end;
end;
end;

Add function execution into installer progress of inno setup

I'm making a patch for an old game (Command & Conquer 1, Win95 edition), and in some cases, executing the patch requires going through a function written in the Pascal script that could take quite a while.
At the moment, I execute this at the moment the page is changed to the "installing" page, so, after the user has selected all options and confirmed to install, right before the installer starts actually adding (and deleting) files.
procedure CurPageChanged(CurPageID: Integer);
begin
if (CurPageID = wpInstalling) then
begin
// Rename all saveg_hi.### files to savegame.###
renameSaveGames();
// clean up the ginormous files mess left behind if the game was installed from the 'First Decade' compilation pack
cleanupTFD();
end;
end;
But since the process could be rather long, I'd prefer to somehow add it to the actual install progress bar. Is there any way to accomplish this?
You can control the ProgressGauge from the install page of the WizardForm. In the following script is shown how to update the progress bar from a loop (which you'll just replace with your actions). For safety are progress bar values like min, max and position saved before the custom actions are performed and restored when they're done.
[Code]
procedure CurPageChanged(CurPageID: Integer);
var
I: Integer;
ProgressMin: Longint;
ProgressMax: Longint;
ProgressPos: Longint;
begin
if CurPageID = wpInstalling then
begin
// save the original "configuration" of the progress bar
ProgressMin := WizardForm.ProgressGauge.Min;
ProgressMax := WizardForm.ProgressGauge.Max;
ProgressPos := WizardForm.ProgressGauge.Position;
// output some status and setup the min and max progress values
WizardForm.StatusLabel.Caption := 'Doing my own pre-install...';
WizardForm.ProgressGauge.Min := 0;
WizardForm.ProgressGauge.Max := 100;
// here will be your time consuming actions with the progress update
for I := 0 to 100 do
begin
WizardForm.FilenameLabel.Caption := 'I''m on ' + IntToStr(I) + '%';
WizardForm.ProgressGauge.Position := I;
Sleep(50);
end;
// restore the original "configuration" of the progress bar
WizardForm.ProgressGauge.Min := ProgressMin;
WizardForm.ProgressGauge.Max := ProgressMax;
WizardForm.ProgressGauge.Position := ProgressPos;
end;
end;

Windows 7 Aero Theme Progress Bar Bug?

I have ran into what I consider to be a progress bar bug on Windows 7. To demonstrate the bug I created a WinForm application with a button and a progress bar. In the button's 'on-click' handle I have the following code.
private void buttonGo_Click(object sender, EventArgs e)
{
this.progressBar.Minimum = 0;
this.progressBar.Maximum = 100;
this.buttonGo.Text = "Busy";
this.buttonGo.Update();
for (int i = 0; i <= 100; ++i)
{
this.progressBar.Value = i;
this.Update();
System.Threading.Thread.Sleep(10);
}
this.buttonGo.Text = "Ready";
}
The expected behavior is for the progress bar to advance to 100% and then the button text to change to 'Ready'. However, when developing this code on Windows 7, I noticed that the progress bar would rise to about 75% and then the button text would change to 'Ready'. Assuming the code is synchronous, this should not happen!
On further testing I found that the exact same code running on Windows Server 2003 produced the expected results. Furthermore, choosing a non aero theme on Windows 7 produces the expected results.
In my mind, this seems like a bug. Often it is very hard to make a progress bar accurate when the long operation involves complex code but in my particular case it was very straight forward and so I was little disappointed when I found the progress control did not accurately represent the progress.
Has anybody else noticed this behavior? Has anybody found a workaround?
It has to do with the animation of the progress bar. If your progress bar is at 0% and you set it to 100% then it will not jump there, but animate the progress bar smoothly filling up. If this is too slow, you will be done before the progress bar finished animating. So even though you have already set it to 80, 90 and 100%, the animation still lags behind.
I never found a way to turn this off, however I have a workaround. The animation is only being done if you increment the progress bar. If you move it backwards, it will immediately jump to that position. So if I want the progress bar to be at x% (x != 100) then I move it to x+1 and then to x. If I want it at 100% I move it to 100, 99 and 100%. (Or whatever values you use, you get the idea.) This works fast enough to not to be visible, and you can leave this code in for previous Windows versions as well. (though I don't)
HTH
I had the same problem. Fozi's tipp was helping me. Before setting a new value I have set the value + 1. To make this work also for 100% the maximum must be increased before. The following worked fine for me.
if (NewValue < progressBar.Maximum)
{
progressBar.Value = NewValue + 1;
progressBar.Value--;
}
else
{
progressBar.Maximum++;
progressBar.Value = progressBar.Maximum;
progressBar.Value--;
progressBar.Maximum--;
}
I think the original problem is related to timing and Win7's (or Aero's) animation mechanism for the progress bar.
This Sub is on the form that contains the progress bar (pBar).
It varies the bar's .Maximum and keeps .Value fixed at 10 for percent completes of 1 to 99. The bar's .Minimum is set to 0 at design time.
This sorted out the problem for me.
Public Sub UpdateStatusPC(ByVal pc As Integer)
Try
If pc < 0 Then
pBar.Maximum = 100
pBar.Value = 0
ElseIf pc > 100 Then
pBar.Maximum = 100
pBar.Value = 100
ElseIf pc = 0 Then
pBar.Maximum = 10
pBar.Value = 0
Else
pBar.Value = 10
pBar.Maximum = 10 / CDbl(pc / 100.0)
End If
pBar.Update()
Catch ex As Exception
MsgBox("UpdateStatusPC: " & ex.Message)
End Try
End Sub
To Delphi users facing the same problem: Below is a unit called ProgressBarFix that you can use to automatically patch the problem without worrying about changing your progress bar code -- just include ProgressBarFix in your form's interface "uses" clause after the ComCtrls uses and you'll get the workaround automatically:
unit ProgressBarFix;
(* The standard progress bar fails under Windows theming -- it fails to animate
all the way to the right side. C.f.,
http://stackoverflow.com/questions/2217688/windows-7-aero-theme-progress-bar-bug
To work around the problem, include ProgressBarFix in the interface section's
"uses" clause *after* ComCtrls (this replaces the TProgressBar definition in
ConCtrls with the one here, effectively allowing the control defined on the
form to be replaced with the patch version.
c.f., http://www.deltics.co.nz/blog/?p=222and http://melander.dk/articles/splitter *)
interface
uses ComCtrls ;
type TProgressBar = class(ComCtrls.TProgressBar)
private
procedure SetPosition(Value: Integer);
function GetPosition: Integer;
published
property Position: Integer read GetPosition write SetPosition default 0;
end ;
implementation
{ TProgressBar }
function TProgressBar.GetPosition: Integer;
begin
result := inherited Position
end;
procedure TProgressBar.SetPosition(Value: Integer);
begin
if Value=inherited Position then
exit ;
if value<Max then begin
inherited Position := value+1 ;
inherited Position := value
end else begin
Max := Max+1 ;
inherited Position := Max ;
inherited Position := value ;
Max := Max-1
end
end;
end.
Disable visual effect option "Animate controls and elements inside windows" in "Performance options". Then the progressbars won't be animated any longer.
I have seen similar issues with progress bars on Vista and Windows 7.
The key problem in my case was the blocking of the UI thread. (Like you do in your sample).
Windows does not like applications that don't respond to new messages in the message queue. If you spend too much time on one message, windows will mark your application as "not responsive". In Vista/Win7, windows also decides to stop updating your application window.
As a workaround, you could put the actual work on a background worker, or call Application.DoEvents() every once in a while. You do need to make sure that your progress bar window is modal, or else the DoEvents() may enable new commands to start executing halfway through your background processing.
If that feels to kludgy, the more proper way is to do your background work on a BackgroundWorker thread. It comes with support for sending events to the UI thread to update the progress bar.
(09/2015) I just jumped from D6 to XE8. Having a number of issues. Including this TProgressBar thing. Tabled it for a while. Came across this (Erik Knowles) fix tonight. Fantastic. Except: the first scenario I ran through had a Max value of 9,770,880. And it (Erik Knowles' "original" fix) REALLY added to the time this process took (with all the extra actual updating of the ProgressBar).
So I expanded his class to reduce the amount of times the ProgressBar actually redraws itself. But ONLY IF the "original" Max value is greater than MIN_TO_REWORK_PCTS (I settled on 5000 here).
If so, the ProgressBar only updates itself HUNDO times (here I started with and pretty much settled on 100, hence the "HUNDO" name).
I accounted for some quirkiness at the Max value as well:
if Abs(FOriginalMax - value) <= 1 then
pct := HUNDO
I tested this against my original 9.8m Max. And, with this standalone test app:
:
uses
:
ProgressBarFix;
const
PROGRESS_PTS = 500001;
type
TForm1 = class(TForm)
Label1: TLabel;
PB: TProgressBar;
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
x: integer;
begin
PB.Min := 0;
PB.Max := PROGRESS_PTS;
PB.Position := 0;
for x := 1 to PROGRESS_PTS do
begin
//let's do something
//
Label1.Caption := Format('%d of %d',[x,PROGRESS_PTS]);
Update;
PB.Position := x;
end;
PB.Position := 0;
end;
end.
with PROGRESS_PTS values of:
10
100
1,000
10,000
100,000
1,000,000
It's smooth and "accurate" for all of these values - without really slowing anything down.
In testing, I was able to toggle my compiler directive DEF_USE_MY_PROGRESS_BAR to test both ways (this TProgressBar replacement vs the original).
Note that you might want to uncomment the call to Application.ProcessMessages.
Here is the (my "enhanced") ProgressBarFix source:
unit ProgressBarFix;
interface
uses
Vcl.ComCtrls;
type
TProgressBar = class(Vcl.ComCtrls.TProgressBar)
const
HUNDO = 100;
MIN_TO_REWORK_PCTS = 5000;
private
function GetMax: integer;
procedure SetMax(value: integer);
function GetPosition: integer;
procedure SetPosition(value: integer);
published
property Max: integer read GetMax write SetMax default 100;
property Position: integer read GetPosition write SetPosition default 0;
private
FReworkingPcts: boolean;
FOriginalMax: integer;
FLastPct: integer;
end;
implementation
function TProgressBar.GetMax: integer;
begin
result := inherited Max;
end;
procedure TProgressBar.SetMax(value: integer);
begin
FOriginalMax := value;
FLastPct := 0;
FReworkingPcts := FOriginalMax > MIN_TO_REWORK_PCTS;
if FReworkingPcts then
inherited Max := HUNDO
else
inherited Max := value;
end;
function TProgressBar.GetPosition: integer;
begin
result := inherited Position;
end;
procedure TProgressBar.SetPosition(value: integer);
var
pct: integer;
begin
//Application.ProcessMessages;
if value = inherited Position then
exit;
if FReworkingPcts then
begin
if Abs(FOriginalMax - value) <= 1 then
pct := HUNDO
else
pct := Trunc((value / FOriginalMax) * HUNDO);
if pct = FLastPct then
exit;
FLastPct := pct;
value := pct;
end;
if value < Max then
begin
inherited Position := Succ(value);
inherited Position := value;
end
else
begin
Max := Succ(Max);
inherited Position := Max;
inherited Position := value;
Max := Pred(Max);
end;
end;
end.

Resources