Create a VCL Forms Application, put a TButton and a TMemo on the Form, and write this code in the button's OnClick handler:
uses
OtlParallel, OtlTaskControl;
procedure TForm2.btnStartLoopClick(Sender: TObject);
var
starttime: Cardinal;
k: Integer;
begin
mmoTest.Lines.Clear;
for k := 1 to 50 do
mmoTest.Lines.Add('Line ' + IntToStr(k));
starttime := GetTickCount;
Parallel.Async(
procedure
var
i: Integer;
begin
for i := 1 to 50 do
begin
Sleep(100);
mmoTest.Lines[i - 1] := mmoTest.Lines[i - 1] + FormatDateTime(' nn:ss:zzz', Now);
end;
end,
Parallel.TaskConfig.SetPriority(TOTLThreadPriority.tpHighest).OnTerminated(
procedure
begin
mmoTest.Lines.Add(IntToStr(GetTickCount - starttime) + ' milliseconds');
end));
end;
Now run the program and make this test:
Click on the button, simply wait for the loop to complete and look at the time displayed in the last line of the memo: It should be approximately 5300 milliseconds.
Now click again on the button, click and hold the form's title bar and move the form around quickly until the loop has finished. Now look again at the memo's last line: In my tests, the time was over 7000 milliseconds. Obviously, the main thread is blocking the parallel thread!
So how can the main thread blocking the parallel thread be avoided?
First, this code is not thread-safe, as the asynchronous code is directly accessing the TMemo from a task thread outside of the main UI thread. You cannot do that. A worker thread MUST synchronize with the UI thread in order to access UI controls safely or else bad things can happen. You can use TThread.Synchronize(), TThread.Queue(), or IOmniTask.Invoke() for that synchronization.
Second, while you are holding down the mouse on the title bar, the main UI message loop is blocked (a separate modal message loop is being run by the OS until you let go of the mouse). As such, the task's OnTerminate event handler may not be run until the main message loop regains control. That would account for why your timer duration is reportedly longer than expected, not because the task loop was blocked.
Third, Sleep() is not absolute. It will sleep for at least the requested amount of time, but may sleep for longer. So your task loop will run for at least 5 seconds, but may be a little longer.
Related
In my manga viewer i try to load a large number of images (around 200), i try loading these images inside an anonymous thread and when the loading finishes i call a Thread.synchronize, this works fine, but in some cases i keep seeing some images not drawn on the form (I think they are loaded because i can see the information about the image), how can i solve this?
This issue only happens in multi thread so i decided to set the image visible to false and then at the end of the loading inside a Thread.synchronize turn the visible to true, i had hoped this would force some kind of refresh or redraw, but the problem i mentioned consists
TThread.CreateAnonymousThread(procedure
begin
for imageIndex := 0 to fileList.count - 1 do
begin
filePath := fileList[imageIndex]
//Please note that mImageList is an array of FMX.Objects.TImage
//which was filled when the Form is created (I create 300 TImage
//at Form create and later i load the images to the bitmap when
//the user clicks on a new directory(Chapter)
mImageList[imageIndex].beginUpdate();
try
mImageList[imageIndex].loadFromFile(filePath);
mImageList[imageIndex].tagString := filePath;
mImageList[imageIndex].Scale := mImageScale;
mImageList[imageIndex].Visible := false;
except
mImageList[imageIndex].EmptyBitmaps;
end;
mImageList[imageIndex].endUpdate();
end;
TThread.Synchronize(nil, procedure
var
I: Integer;
begin
for I := Low(mImageList) to High(mImageList) do
begin
mImageList[I].Visible := true;
end;
end);
end).Start;
I expect that when i scroll down through the images loaded on the form, i can see them all and they all get drawn on the form properly.
I was creating a Form with some buttons in runtime in my application and I realized something that's bothering me a lot and I couldn't figure for myself or anywhere in the internet.
Look at the following code.
procedure TfrmTest.CreateFourButtons(Sender: TObject);
var
i: Integer; B: TButton;
begin
for i := 1 to 4 do
begin
B := TButton.Create(frmTest);
B.Parent := frmTest;
B.SetBounds(250,(70+(30*i)),75,25);
B.Caption := 'Button' + IntToStr(i);
B.Visible := True;
end;
end;
So I just created four buttons at runtime on a form in specific locations. So far so good right?
But now let's imagine that I want to change the Caption property of the first button to "HotPotato"? How do I reference the first button now since I used just one variable to create those buttons? I've been told to store those objects in an array of TButtons or better yet in an TObjectList and these are all fine solutions to this problem. But then one question came to my mind!
Where are those buttons located in the memory? Are the any ways for me to reference them without using arrays or object lists?
The normal way to keep track of multiple similar object is to use an array.
procedure TfrmTest.CreateFourButtons(Sender: TObject);
var
i: Integer;
B: TArray<TButton>; //or array of TButton for older versions
begin
SetLength(B, 4);
for i := 0 to 3 do begin
B[i] := TButton.Create(frmTest);
B[i].Parent := frmTest;
B[i].SetBounds(250,(70+(30*i)),75,25);
B[i].Caption := 'Button' + IntToStr(i);
B[i].Visible := True;
end;
B[0].Caption:= 'HotPotato';
end;
When placing buttons on a form this is not strictly needed, the form already uses a list to keep track of child controls placed on it, but you'll need some way to tell the different buttons apart.
You can use the tag property for this:
for i := 1 to 4 do begin
B := TButton.Create(frmTest);
B.Parent := frmTest;
B.Tag:= i;
...
end;
//This will get inefficient if there are many controls on a form.
for var C in frmTest.Controls do begin //10.3 syntax.
if (C is TButton) and (C.tag = 1) then C.Caption:= 'HotPotato'
end;
The button is an object and it is thus located on the heap. If you lose its reference you will never find it again. However if it is placed on a parent control, then that parent will keep track of it and you can always get it using FindChildControl or the Controls list of the form.
FindChildControl does a search by name. This requires you to set the name of the control, or it will not work.
var B:= frmTest.FindChildControl('Button1');
Note: FindChildControl only locates immediate children of the control. It can't find a control that is a child of one of the control's children.
This locating children can get complicated if the button is located in a subpanel. Better to use the array or a list to keep track of a range of buttons.
An installer I'm working on does most of its work in the PrepareToInstall function, as everything I need to do might fail, and therefore this is the appropriate place to handle these things in case they do fail. Any failure can be automatically reported by passing the error message in the function's result. There are only 3 small files which the installer actually copies over.
The problem is that the wizard seems to freeze (or not respond rather) during this function, just showing a blank page titled "Preparing to install..." while in reality, it's going through my install process.
I would like to show the progress to the user with a simple procedure ShowProgress(const S: String); which shows the user what it's actually doing. How can I do this?
This is how I'm doing my install, where I'd like to wrap each call to Log()...
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
R: Integer;
begin
Result:= '';
try
Log('Doing this...');
R:= DoThis;
case R of
0: begin
Result:= '';
end;
1: begin
Result:= 'Error message 1 was raised while doing this.';
end;
else begin
Result:= 'Unexpected error doing this: ' + IntToStr(R);
end;
end;
if Result = '' then begin
Log ('Doing that...');
R:= DoThat;
case R of
0: begin
Result:= '';
end;
1: begin
Result:= 'Error message 1 was raised while doing that.';
end;
else begin
Result:= 'Unexpected error doing that: ' + IntToStr(R);
end;
end;
end;
if Result = '' then begin
Log ('Doing something else...');
R:= DoSomethingElse;
case R of
0: begin
Result:= '';
end;
1: begin
Result:= 'Error message 1 was raised while doing something else.';
end;
else begin
Result:= 'Unexpected error doing something else: ' + IntToStr(R);
end;
end;
end;
//A few more blocks like above
//Error logging
if Result <> '' then begin
Log('FAILURE: '+Result);
end;
except
Result:= 'EXCEPTION';
Log('EXCEPTION');
end;
end;
I mixed all your answers guys and I found another possible solution to this question.
I know this is not a so called 'elegant solution'.
Anyway it is a quick solution that can avoid you to develop a separated DLL with custom controls managing the communication between the installer and the DLL.
The following approach describe how to create a custom Page that is a copy of the Page Inno Setup shows during PrepareToInstall setup phase.
This copy must have the same look and feel of the original one plus a progress bar and a label that developer can use to indicate the current step of the PrepareToInstall setup phase.
Let's see some code.
First of all, prepare the custom Page:
[Code]
var
PrepareToInstallWithProgressPage : TOutputProgressWizardPage;
Now define PrepareToInstall function to add our custom stuff:
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
PrepareToInstallWithProgressPage.SetProgress(0, 0);
PrepareToInstallWithProgressPage.Show;
try
// First preinstallation step: suppose to kill currently app you are going to update
PrepareToInstallWithProgressPage.SetText('Exiting MyApp Running Instances'), '');
ExecuteCmd('taskkill', '/F /IM MyApp');
// Set progress bar to 10%
PrepareToInstallWithProgressPage.SetProgress(1, 10);
// Second preinstallation step
// DoSomething usefull...
// Set progress bar to 20% after done
PrepareToInstallWithProgressPage.SetProgress(2, 10);
// ...do other preinstallation steps till the end
PrepareToInstallWithProgressPage.SetProgress(10, 10);
finally
PrepareToInstallWithProgressPage.Hide;
end;
end;
At this installation phase we completed the steps for prepare to install phase so the default PrepareToInstall Page of Inno Setup is shown for a while.
Now, the user probably cannot understand that the page changes because our PrepareToInstallWithProgressPage has the same look and feel of the original one.
In order to let our page has the same look and feel we can use SetupMessage function to get the 2 strings of the original Inno Setup PrepareToInstall page.
Using SetupMessage we can avoid to duplicate and localize strings for our custom page to copy the originals.
Just like this:
procedure InitializeWizard;
var
A: AnsiString;
S: String;
begin
// The string msgWizardPreparing has the macro '[name]' inside that I have to replace with the name of my app, stored in a define constant of my script.
S := SetupMessage(msgPreparingDesc);
StringChange(S, '[name]', '{#MY_APPNAME}');
A := S;
PrepareToInstallWithProgressPage := CreateOutputProgressPage(SetupMessage(msgWizardPreparing), A);
end;
I hope this help.
Have a look at the CodeDlg.iss example script included with Inno, in particular at the code that uses CreateOutputProgressPage.
This page type allows you to show a status message and/or a progress bar while you are performing other actions. And it automatically pumps messages whenever you change the progress label/value to ensure that the user sees the correct status.
It is intended for precisely this kind of code.
The code written in the PrepareToInstall function blocks the Windows message pump from processing, rendering the wizard form non-responsive. If many controls are needed to be visible in this screen, showing perhaps a progress bar and list of steps to take, this can be done with a form inside of a DLL. Make sure there is a thread in this DLL which does the actual install process, and only update the GUI to display the status to the user. This DLL form may overlay the wizard, or may even be embedded inside of the wizard in a custom wizard page, for example. The idea is to move your install code from the PrepareToInstall function to within this thread, and only then will you be able to achieve a fully responsive GUI during this process. From the PrepareToInstall function, there should be just one DLL call to initiate the install process, which in turn shows the form from the DLL.
The result may look something like this (in the works):
Is there a way to show a message box for a specified time (that means, the message box will close itself when the specified time elapses) ?
Windows API has a function for showing a message box for a specified time, but for some reason is that function undocumented, which means it is not officially supported and may well be subject to change.
That function is called MessageBoxTimeout, and it has even export in the user32.dll library, what makes me feel that the only thing this function lacks is the official documentation. But who knows...
The following script shows how to display a message box for 5 seconds before the wizard form is shown. If the user doesn't click the OK button, nor manually close the window, the message box is automatically closed when that 5 seconds period elapses:
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
const
MB_TIMEDOUT = 32000;
MB_ICONERROR = $10;
MB_ICONQUESTION = $20;
MB_ICONWARNING = $30;
MB_ICONINFORMATION = $40;
function MessageBoxTimeout(hWnd: HWND; lpText: string; lpCaption: string;
uType: UINT; wLanguageId: Word; dwMilliseconds: DWORD): Integer;
external 'MessageBoxTimeout{#AW}#user32.dll stdcall';
procedure InitializeWizard;
begin
MessageBoxTimeout(WizardForm.Handle, 'This message will be automatically ' +
'closed in 5 seconds!', 'Caption...', MB_OK or MB_ICONINFORMATION, 0, 5000);
end;
For more information about parameters and result values refer to the MessageBox function help site and some of the unofficial articles describing the MessageBoxTimeout function itself, like e.g.:
Maurizio Pisano: MessageBoxTimeout API (CodeProject)
Eddie Shipman: Undocumented MessageBoxTimeOut function (Embarcadero)
If you want a more customized implementation than the MessageBoxTimeout from #TLama's answer allows (like countdown display or custom button captions):
create a custom form using CreateCustomForm;
use SetTimer to implement the timeout/count down.
For a complete code, see MsgBox - Make unclickable OK Button and change to countdown - Inno Setup.
Based on this article:
I use this code to make some animation on a given Window Handle while doing some job on my database:
while not Terminated do
begin
// some code....
// draw onto the Window DC
DC := GetDC(FWnd); // FWnd is the Window Handle
// DC := GetDCEx(FWnd, 0, DCX_VALIDATE or DCX_LOCKWINDOWUPDATE);
if DC <> 0 then
try
BitBlt(DC,
FPaintRect.Left,
FPaintRect.Top,
ImageRect.Right,
ImageRect.Bottom,
Bitmap.Canvas.handle,
0, 0,
SRCCOPY);
finally
ReleaseDC(FWnd, DC);
end;
// more code....
end; // end while
Is it thread safe, or should I somehow Lock the DC?
Also, Can I use the GetDCEx?
Thanks.
No, your code is not threadsafe assuming that the window handle (FWnd) is created in main (GUI) thread. A standard VCL approach is to call all GDI functions in GUI thread, via Synchronize or Queue methods of TThread class.