You are not logged in.
Pages: 1
Thanks for the explanations.
I think I will use your implementation.
It makes the most sense to me.
A couple of posts back I asked about the possible advantages of
making TTaskDialog a class. Probably better to add a small class
using TTaskDialogEx to the SynTaskDialog unit.
The reason is in a class you can implement a procedure
to send messages. Here is a very simplified example to
illustrate the idea.
// interface
TTaskDialogX = class
private
FtaskDialogX: TTaskDialogEx;
procedure SendExecuteTD(Handle: HWND);
procedure MessageHandler(var Msg: TMessage); message TH_MESSAGE;
protected
public
constructor Create();
destructor Destroy; override;
//property TDialogX: read FTaskDialogX; ??
end;
//implementation
procedure TTaskDialogX.SendExecuteTD(Handle: HWND);
var pd: ^FTaskDialogX;
begin
SendMessage(Handle, TH_MESSAGE, 0, LPARAM(pd));
end;
procedure TTaskDialogX.MessageHandler(var Msg: TMessage);
var pd: ^FTaskDialogX;
begin
pd := Pointer(Message.LParam);
try
pd^.Execute(Handle);
finally
Dispose(pd);
end;
end;
But if you call SendExecuteTD from a thread using
the main form/thread handle, does the MessageHandler()
recieve it in the main thread/form?
In other words, is it thread safe?
I just threw together a mini test program -
It works! No more TTaskDlg record. Cool. Implementing the
thread-safe emulation mode solution is fairly easy now.
Almost everything necessary is already in SyTaskDialog.pas.
After looking over the TMyThread declaration and .Execute,
I saw the only thing I really needed to change was adding
pd^ := DefaultTaskDialog
I eliminated FHandle in TMyThread and used Form1.Handle
for PostMessage() in TMyThread.Execute. It works just as well.
Unless you think using FHandle is safer for some reason?
I can still use
Include(pd^.CommonButtons, cbCancel)
but I think
pd^.CommonButtons := [cbCancel]
is better.
And I put the SyTaskDialog unit down in the Implementation
uses clause where it belongs. Thanks.
But it was adding
pd^ := DefaultTaskDialog
that did the trick. Except I do not understand, why?
It's getting difficult to follow this endless stream of code snippets. :-)
I think the best would be if you build a small (!) project showing the wrong behaviour and post it. AFAICS there is no way to attach it on the forum, so you could use some file hoster.
Yes I agree. I will see what I can do.
I see virtually no difference between TTaskDlg and TTaskDialogEx.
They are identical except one calls the TTaskDialog execute function
directly and the other calls it from a function.
But I have to go with what works for now; one works and the other does not.
One simplification you still could make in your code is to make TDparams a pointer to
TTaskDialogEx and drop your own TTaskDlg record.
Then the only additional parameter needed for calling Execute would be the ParentHandle.
It sounds great. No more TTaskDlg record.
But I cannot get it to work in XP emulation mode.
I have a test section for TaskDialog I use in the thread.
I call 2 instances of it speparated by a 4 second delay
using Sleep(4000). The 2nd instance has different
values for the fields and parameters.
When I changed TDparams to ^TTaskDialogEx, the 1st instance
form has no color, no icons, the text is all black and
the width is off the screen. The 2nd instance looks normal
but it is displayed over the 1st instance form which never
goes away.
Here is the code I used. Maybe you can tell
me what I am doing wrong.
procedure thread.execute;
var // ...
TDparams: ^TTaskDialogEx; // declare a pointer to TTaskDialogEx
// ...
begin
// ...
New(TDparams);
try
// to fill in the fields I replaced "Task" with "Base" (TaskDialog object)
TDparams^.Base.Title := 'Title';
TDparams^.Base.Inst := 'General Topic';
TDparams^.Base.Content := 'Details';
TDparams^.Base.Footer := 'Additional Info';
// other fields
// TCommonButtons and TTaskDialogFlags parameters are sets and
// need to be handled differently. I used Include() ?
Include(TDparams^.CommonButtons, cbOK); // or whatever button(s) you want
// all other parameters are the same as before
TDparams^.ButtonDef := mrOk;
TDparams^.DialogIcon := tiInformation;
TDparams^.FooterIcon := tfiInformation;
// other parameters
PostMessage(Form1.Handle, TH_MESSAGE, TH_SYNTD, Integer(TDparams));
except
Dispose(TDparams);
end;
// ...
end; // end thread
And I made the change to the thread message handler as you posted:
TDparams^.Execute(Handle);
I am thinking the final step would be to
implement it in the SynTaskDialog unit
so users do not have to do it separately.
Maybe add a procedure to the object that uses a SendMessage()
to a message handler or WinProc where Task.Execute() is called?
I know next to nothing about making a class but if making SynTaskDialog
a class would make implementation easier or would open up opportunities
for an even better solution, it might be worthwhile.
What do you think?
No problem. Both pointers point to the same record (as there is only one New).
So you need only one Dispose, too. After the call to Dispose both pointers
point to garbage and shouldn't be used anymore, of course.
Understood. It's cool how that works.
I have learned a lot from you and ab. You guys have enabled
this solution to be distilled down to some reasonably easy
code to implement.
FWIW: I'd get rid of the version checking (IsWinXP, IsVistaOrHigher) und just always show the task dialog from the main thread.
Yes good idea. Thanks.
procedure thread.execute;
var // ...
TDparams: PTTaskDiag; // declare a pointer to the TTaskDiag record
// ...
begin
// ...
New(TDparams);
try
TDparams^.Task.Title := 'Title';
TDparams^.Task.Inst := 'General Topic';
TDparams^.Task.Content := 'Details';
TDparams^.Task.Footer := 'Additional Info';
// other fields
TDparams^.CommonButtons := cbOK;
TDparams^.ButtonDef := mrOk;
TDparams^.DialogIcon := tiInformation;
TDparams^.FooterIcon := tfiInformation;
// other parameters
PostMessage(Form1.Handle, TH_MESSAGE, TH_SYNTD, Integer(TDparams));
except
Dispose(TDparams);
end;
// ...
end; // end thread
I am still confused about something.
In the thread, the record pointer (TDparams) is only disposed
if there is an exception. This is good because we do not want
it disposed until the thread message handler uses it.
After thread message handler uses it, the local record pointer (TDparams)
in the message handler is disposed in try/finally.
But what about the local record pointer (TDparams) in the thread ?
OK for the try...finally.
But why do you use a temporary Task local variable?
You do not need it, I think, since TDParams is already private to your message.
I understand now. In the thread, I use the TTaskDialog inside the
TTaskDiag record to fill in the fields.
TDparams^.Task.Title := 'Title';
// etc.
TDparams^.Task.Footer := 'Additional Info';
// etc.
The fields and the Task.Execute() parameters are then sent to the
thread message handler.
In the message handler these same fields are received inside the
TTaskDiag record (pointer). All that is necessary is to call the
Task.Execute() within the TTaskDiag record (pointer).
A TTaskDialog variable is not needed.
procedure TForm1.ThreadMessage(var Message: TMessage);
var TDparams: PTTaskDiag;
// ...
begin
case Message.WParam of
TH_SYNTD:
begin
TDparams := Pointer(Message.LParam);
try
TDparams^.Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon,
TDparams^.FooterIcon, 0, 0, 0, False, False);
finally
Dispose(TDparams);
end;
end;
TH_LABEL: // ...
TH_PROGBAR: // ...
// ... others
end;
end;
The TTaskDiag record can now be moved from the
interface section to the implementation section.
In the actual code in my app I have renamed the TTaskDiag/PTTaskDiag record
to TTaskDlg/PTaskDlg to avoid confusion.
implementation
{$R *.DFM}
// uses
type
PTaskDlg = ^TTaskDlg;
TTaskDlg = record
Task: TTaskDialog;
CommonButtons: TCommonButton;
ButtonDef: Integer;
Flags: TTaskDialogFlag;
DialogIcon: TTaskDialogIcon;
FooterIcon: TTaskDialogFooterIcon;
RadioDef,
Width: Integer;
ParentHandle: HWND;
NonNative,
EmulateClassicStyle: Boolean;
end;
// revised thread message handler
// uses TDparams to fill Task: TTaskDialog fields
// at one time instead of each one separately
TH_SYNTD:
begin
TDparams := Pointer(Message.LParam);
Task := TDparams^.Task; // fills in TTaskDialog fields
try
Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon,
TDparams^.FooterIcon, 0, 0, 0, False, False);
finally
Dispose(TDparams);
end;
end;
In the message handler it would be best to put it in a try/finally:
TH_SYNTD:
begin
TDparams := Pointer(Message.LParam);
try
Task.Title := TDparams^.Task.Title;
// ...
TDparams^.Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon,
TDparams^.FooterIcon, 0, 0, 0, False, False);
finally
Dispose(TDparams);
end;
end;
and maybe enclose all in a conditional using the custom message identifier:
if Message.Msg = TH_MESSAGE then
begin
case Message.WParam of
// ...
end;
end;
Uli and ab, you have me convinced about (not) using global vars.
I am going through my app now and trying to eliminate most
or all of them.
Here is an implementation of Uli's idea, no global var and some corrections.
interface
uses
//...,
SynTaskDialog,
//...;
const
// used by thread message handler
TH_MESSAGE = WM_USER + 5;
TH_SYNTD = 1;
TH_LABEL = 2;
TH_PROGBAR = 3;
type
PTTaskDiag = ^TTaskDiag;
TTaskDiag = record
Task: TTaskDialog;
CommonButtons: TCommonButton;
ButtonDef: Integer;
Flags: TTaskDialogFlag;
DialogIcon: TTaskDialogIcon;
FooterIcon: TTaskDialogFooterIcon;
RadioDef,
Width: Integer;
ParentHandle: HWND;
NonNative,
EmulateClassicStyle: Boolean;
end;
// ...
type
TForm1 = class(TForm)
// ...
procedure Button1Click(Sender: TObject);
// ...
protected
{ Protected declarations }
private
{ Private declarations }
procedure ThreadMessage(var Message: TMessage); message TH_MESSAGE;
public
{ Public declarations }
end;
var
Form1: TForm1;
// ...
// no TTaskDialog global variable is used
//Task: TTaskDialog;
implementation
{$R *.DFM}
{$R XP.RES} // used for themes & using SynTaskDialog
// there are other ways too
// all routines in the main form/thread where
// TTaskDialog is used, declare it as a local variable
procedure TForm1.Button1Click(Sender: TObject);
var // ...
Task: TTaskDialog;
begin
// ...
Task.Title := 'Title';
Task.Inst := 'General Topic';
Task.Content := 'Details';
Task.Footer := 'Additional Info';
Task.Execute([cbOK],mrOk,[],tiInformation, tfiInformation, 0, 0, Handle, False, False);
// ...
end;
// in a thread
procedure thread.execute;
var // ...
TDparams: PTTaskDiag; // declare a pointer to the TTaskDiag record
// ...
begin
// ...
New(TDparams);
try
// no critical section needed
TDparams^.Task.Title := 'Title';
TDparams^.Task.Inst := 'General Topic';
TDparams^.Task.Content := 'Details';
TDparams^.Task.Footer := 'Additional Info';
if IsWinXP then
begin
TDparams^.CommonButtons := cbOK;
TDparams^.ButtonDef := mrOk;
TDparams^.DialogIcon := tiInformation;
TDparams^.FooterIcon := tfiInformation;
PostMessage(Form1.Handle, TH_MESSAGE, TH_SYNTD, Integer(TDparams));
end
else if IsVistaOrHigher then
TDparams^.Task.Execute([cbOK], mrOk, [], tiInformation, tfiInformation, 0, 0, 0, False, False);
except
Dispose(TDparams);
end;
// ...
end; // end thread
// in main form thread message handler
procedure TForm1.ThreadMessage(var Message: TMessage);
var TDparams: PTTaskDiag;
Task: TTaskDialog;
// ...
begin
case Message.WParam of
TH_SYNTD:
begin
TDparams := Pointer(Message.LParam);
Task.Title := TDparams^.Task.Title;
Task.Inst := TDparams^.Task.Inst;
Task.Content := TDparams^.Task.Content;
Task.Buttons := TDparams^.Task.Buttons;
Task.Radios := TDparams^.Task.Radios;
Task.Info := TDparams^.Task.Info;
Task.InfoExpanded := TDparams^.Task.InfoExpanded;
Task.InfoCollapse := TDparams^.Task.InfoCollapse;
Task.Footer := TDparams^.Task.Footer;
Task.Verify := TDparams^.Task.Verify;
Task.Selection := TDparams^.Task.Selection;
Task.Query := TDparams^.Task.Query;
Task.RadioRes := TDparams^.Task.RadioRes;
Task.SelectionRes := TDparams^.Task.SelectionRes;
Task.VerifyChecked := TDparams^.Task.VerifyChecked;
TDparams^.Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon,
TDparams^.FooterIcon, 0, 0, 0, False, False);
Dispose(TDparams);
end;
TH_LABEL: // ...
TH_PROGBAR: // ...
// ... others
end;
end;
I don't know if this is the best solution to making the Syn TaskDialog
emulation mode thread safe. Whatever the best solution is, it is worthwhile
because there are still lots of XP users out there. And for good reason
especially for home users.
They discover that upgrading to Win7 will cost them more than
the price of the upgrade. In most cases it means also buying a new
computer or laptop with enough resources to run it. In these difficult economic
times to spend 800-1500 USD/598-1122 EUR or more just to upgrade when XP can still do all
the things they need does not make good sense or is not possible with their budget.
Delphi programmers that understand this will continue to support XP for a while.
Some application types require using a thread. Apps involving internet connection
and use, apps that search large amounts of data and in my case write data to DVD/BD discs.
Doing these resource intensive operations using the main thread causes all the visual
controls to intermittently freeze or perform poorly and interupt the operaion with
sometimes negative results. But put it in a thread and everything runs smoothly.
It is really great to have a message dialog that works for both XP and Win7 and
looks much better than MessageDLG() and has many more customizable features.
I think I had about a half dozen different customized message dialogs which I
have now replaced with the Synpose TaskDialog. It is a big advantage
and convenience.
I have found one or two other open source TaskDialogs with an emulation mode
but they don't look very good. And I found some commercial ones. One is reasonably
priced but its emulation mode does not look as good a Synpose's and I don't think
it has as many customizable features. The others are part of a package and are expensive
and do not look better or not as good and they also do not have more features
or at least not any that are of practical use.
I still do not know exactly what causes the emulation mode to be thread unsafe.
I wish it was possible to fix it. But I will use the above solution until a
better one comes along.
The soapbox is getting wobbly so I’m getting off before I fall off.
uligerhardt,
I need the global TTaskDialog var for the many procedures where I use it in the main form/thread besides in the thread message handler. In the thread I already had several global vars whose values are changed and needed a critical section so a few cases of copy/paste of the enter/leave code was easy.
I cannot see how it is possible to pass the Task.execute() parameters to the message handler using a pointer to TTaskDialog. I am no expert either as you can tell from my posts. I would be happy to find a way to eliminate the TDparams record but you would have to explain to me your idea in more detail or an example.
Usually Task: TTaskDialog; is declared as a global var
in the main (form) thread in which case in secondary threads the
fields need to be protected by a critical section.
// in thread
EnterCriticalSection(CriticalSection);
Task.Title := 'Title';
Task.Inst := 'General Topic';
Task.Content := 'Details';
Task.Footer := 'Additional Info';
LeaveCriticalSection(CriticalSection);
if IsWinXP then
begin
New(TDparams);
try
// etc. as above
-------- revised code ------
ab, thanks for your advice & instruction
I no longer declare TDparams: PTTDexe as a global var in mainform/interface.
// it is declared in the thread ...
procedure thread.execute;
var TDparams: PTTDexe;
// ...
begin
// ...
Task.Title := 'Title';
Task.Inst := 'General Topic';
Task.Content := 'Details';
Task.Footer := 'Additional Info';
if IsWinXP then
begin
New(TDparams);
try
TDparams^.CommonButtons := cbOK;
TDparams^.ButtonDef := mrOk;
TDparams^.DialogIcon := tiInformation;
TDparams^.FooterIcon := tfiInformation;
PostMessage(Form1.Handle, WCDMESSAGE, TH_WCDTD, Integer(TDparams));
except
Dispose(TDparams);
end;
end // end emulation mode for XP
else if IsVistaOrHigher then
Task.Execute([cbOK], mrOk, [], tiInformation, tfiInformation, 0, 0, 0, False, False);
// ...
end; // end thread
// ... and is declared in the message handler (mainform)
procedure TForm1.ThreadMessage(var Message: TMessage);
var TDparams: PTTDexe;
begin
case Message.WParam of
TH_WCDTD:
begin
if Message.LParam > 0 then
begin
TDparams := PTTDexe(Message.LParam);
Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon,
TDparams^.FooterIcon, 0, 0, 0, False, False);
Dispose(TDparams);
end;
end;
// ... more WParams
end; // end case
end; // end message handler
------------------------------------
I don't understand what a "shared record" is. Could you give an example or
reference a synpose project unit where it occurs?
uligerhardt and Administrator thanks for your replies.
This is some sample code of the implementation I made and tried
to describe (not very well) in my last post.
interface
uses ..., SynTaskDialog;
const
TH_MESSAGE = WM_USER + 10;
// for wparams in thread message handler
TH_WCDTD = 1;
TH_PROGBAR = 2;
TH_LABEL1 = 3;
TH_LABEL2 = 4;
// etc.
type
PTTDexe = ^TTDexe;
TTDexe = record
CommonButtons: TCommonButton;
ButtonDef: Integer;
Flags: TTaskDialogFlag;
DialogIcon: TTaskDialogIcon;
FooterIcon: TTaskDialogFooterIcon;
RadioDef,
Width: Integer;
ParentHandle: HWND;
NonNative,
EmulateClassicStyle: Boolean;
end;
TMainForm = class(TForm)
// ...
private
procedure ThreadMessage(var Message: TMessage); message TH_MESSAGE;
var
Form1: TForm1;
// more vars ...
TDparams: PTTDexe; // pointer for Taskdialog parameters record
implementation
procedure TForm1.ThreadMessage(var Message: TMessage);
begin
case Message.WParam of
TH_WCDTD:
begin // In my threads the other params never change.
Task.Execute([TDparams^.CommonButtons], TDparams^.ButtonDef, [], TDparams^.DialogIcon, TDparams^.FooterIcon, 0, 0, 0, False, False);
Dispose(TDparams); // release allocated memory
end;
// other WParam constants for progressbar.stepit/.update, label.update, etc.
// instead of using Synchronize().
end;
end;
//=== within a thread ===//
// I do the following for each instance of SynTaskDialog:
// initialize fields
Task.Title := 'Title';
Task.Inst := 'General Topic';
Task.Content := 'Details';
Task.Footer := 'Additional Info';
// initialize taskdialog.execute() params
// allocate memory, reference TDparams & assign values
New(TDparams);
TDparams^.CommonButtons := cbOK;
TDparams^.ButtonDef := mrOk;
TDparams^.DialogIcon := tiWarning;
TDparams^.FooterIcon := tfiWarning;
// call taskdialog.execute() based on OS version
if IsWinXP then
PostMessage(Form1.Handle, TH_MESSAGE, TH_WCDTD, Integer(TDparams))
else if IsVistaOrHigher then
Task.Execute([cbOK], mrOk, [], tiWarning, tfiWarning, 0, 0, 0, False, False);
//=======================//
I don't like threads. I only use them when absolutely necessary.
I use several secondary threads but only one runs at a time.
Re: "invisible window"
Sorry about using that term to describe what I meant.
What I mean is adding a handle of type HWND to the thread declaration
then assigning it using AllocateHWnd() in thread execute. I was wondering
if the the HWND could then be used as the handle parameter in task.execute().
Yes I should have said. The error only occurs in Win XP emulation mode.
I understand now emulation mode is not thread safe. Thanks.
I've implemented a thread message handler. I also made a record with a
pointer for all the params of the Task.Execute function and then a
var for the record pointer.
For each occurance in the thread I fill in the Task fields and also the
Task.Execute params using the record pointer var which I send via the PostMessage()
Lparam and dereference in the message handler for the Task.Execute call.
It all works great but I'm just an amateur hobbyist, maybe intermediate level at best.
If you or another more expert programmer has a better way to do this I'd appreciate you sharing it.
>> It specifies the parent window of the task dialog.
I'm still curious about the handle thing. Could you assign
it the handle of the invisible window of a thread?
bilm
The Synopse TaskDialog works great in the main thread but
when I call it in a secondary thread I get an invalid operation error:
"Canvas does not allow drawing."
The Consts unit shows this as "SNoCanvasHandle".
I declare
Task: TTaskDialog;
as a var in the interface of the main thread/main form then
in the secondary thread I have:
Task.Title := 'My title';
Task.Inst := 'Main Subject';
Task.Content := 'Detailed information.';
Task.Execute([cbOK], mrOk, [], tiInformation, tfiInformation, 0, 0, Handle, False, False);
The handle parameter should be the thread handle, right?
I have also tried declaring ThTask: TTaskDialog; as a var in the
secondary thread with but with the same result.
Strangley it works fine when executed inside the Delph IDE (D7).
Do I need to make a thread message handler and use Send/Postmessage?
How would I use their params to call it that way?
I hope there is a solution to this because I want to use
SynTaskDialog compared to the others available but
I need one that can run from a thread.
Thanks for any help.
bilm
Pages: 1