Reputation: 36654
Scenario:
Problem:
So I could write like this
procedure TMyForm.OnMyAction(Sender: TObject);
begin
try
// notify Action Manager that the Action is temporarily disabled
SomeGlobalFlag := True;
// disable the action
(Sender as TAction).Enabled := False;
// do the call
ShellExecAndWait( ... );
finally
// enable the action
(Sender as TAction).Enabled := True;
// allow ActionManager to control the action again
SomeGlobalFlag := False;
end;
end;
Is there an easier way? As the title of this question says - could I block input for the execution of the external application?
Upvotes: 0
Views: 7823
Reputation: 1
The implementation of EnableControls
suffers for something weird:
So disabling all (not saving the previous state) is a big miss.
To do it correcty, you need to create a Component list and store a pointer to the component and previous enabled state, so when restoring you can restore previous state.
Worst, that way is also not totally correct, since some code may change the Enabled status of some components, and when restoring that component Enabled status must not be restored... so no easy way to disable/re-enable can be done... not to mention if code depends on Enabled status of some components.
I do not remember now how i had done in the past... but there is a one sentence that block the form from mouse and keyboard (ignore them) and another to unblock it... that one is what i was trying to find when i came here from Google... may it is related to (TMouseActivate
), but i remember it also block keyboard, so maybe this is not enought:
procedure TMyForm.FormMouseActivate(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y, HitTest: Integer; var MouseActivate: TMouseActivate);
begin
if YourCondition
then beign
MouseActivate:=maNoActivateAndEat;
end;
end;
I remember i did not use the following but it can also be used:
procedure BlockInput(ABlockInput:Boolean);stdcall;external 'USER32.DLL';
...
procedure TMyForm.MyEvent(Sender:TObject);
begin
BlockInput(True);
// Your code, long loop, etc
BlockInput(False);
end;
Upvotes: 0
Reputation: 163287
It depends on how friendly you want your program to be to its users.
The approach shown in the question will suffice, but it could leave the user wondering why the button appears disabled. Your program could be more helpful if it left the button enabled, but changed its behavior when clicked. Instead of starting another copy of the program, it could notify the user that the previous program is still running, and perhaps even offer to set focus to that program.
The question title asks how to disable all the controls on the form. (Whether the form is modal is irrelevant; modality deals with disabling the parent form(s), not the modal form itself.) Mjn's answer sort of does that by suspending an action list. That won't disable controls that aren't associated with actions, and it won't disable controls that are associated with a different action list. It could also disable controls on other forms that are associated with the same action list.
Marck's answer implicitly disables all the controls, but will probably confuse the user since none of the controls will look disabled. That seems similar to the idea Sertac apparently mentioned in a comment, to disable the entire form.
To disable all controls on a form, and have them appear disabled, you can use a recursive function like this:
procedure EnableControls(Parent: TWinControl; Enabled: Boolean);
var
i: Integer;
Ctl: TControl;
begin
for i := 0 to Pred(Parent.ControlCount) do begin
Ctl := Parent.Controls[i];
Ctl.Enabled := Enabled;
if Ctl is TWinControl then
EnableControls(TWinControl(Ctl), Enabled);
end;
end;
Use it like this:
procedure TMyForm.OnMyAction(Sender: TObject);
begin
EnableControls(Self, False);
try
ShellExecAndWait(...);
finally
EnableControls(Self, True);
end;
end;
Since we're directly modifying the Enabled
properties of the controls, those properties will be severed from the Enabled
properties of any associated actions. That solves the immediate need, but has the unwanted side effect that further modifications to an action's Enabled
property will not affect the controls on this form.
Actions can be associated with multiple controls, and controls can reside on multiple forms. Since it's the action that's being updated, and not the controls directly, there isn't really a way to use actions to disable controls on just one form.
Now we come to the matter of whether disabling all the controls on a form is really the right solution to the problem that motivated this question. The question is a bit scattershot with regard to the goal and the proposed solution. The proposed solution is heavy-handed (disable everything on the form) for something that really only needs to prevent a single command from being invoked. And the command that shouldn't be invoked has nothing to do with the form; no matter how many controls are associated with the action on any number of forms, none should invoke the command. So, we should either disable the action, and implicitly disable whatever controls are associated with it, or we should modify the OnExecute
event handler to detect re-entrance.
The solution shown in the question is the way to disable the action. Set a flag to indicate that the action is executing, and clear it when execution completes. Check that flag in the OnUpdate
event handler. There's no need to manually disable the action in the OnExecute
handler, though; actions already update themselves in their Execute
methods. So we have this code:
var
ActionIsExecuting: Boolean = False;
procedure TMyForm.OnMyAction(Sender: TObject);
begin
// notify Action Manager that the Action is temporarily disabled
ActionIsExecuting := True;
try
// do the call
ShellExecAndWait( ... );
finally
// allow ActionManager to control the action again
ActionIsExecuting := False;
end;
end;
procedure TSomeModule.ActionUpdate(Sender: TObject);
begin
(Sender as TAction).Enabled := not ActionIsExecuting and ...
end;
That requires multiple sections of code to cooperate regarding how to handle this action's execution. The action-updating code needs to know that the action needs to be able to temporarily disable itself.
For a more self-contained solution, we can leave the OnUpdate
event alone and just keep the action enabled all the time. Instead, we'll keep track of re-entrance locally and notify the user:
procedure TMyForm.OnMyAction(Sender: TObject);
{$J+} // a.k.a. $WRITABLECONST ON
const
ActionIsExecuting: Boolean = False;
{$J-}
begin
if ActionIsExecuting then begin
ShowMessage('The program is still running. Please wait.');
exit;
end;
ActionIsExecuting := True;
try
ShellExecAndWait(...);
finally
ActionIsExecuting := False;
end;
end;
Mjn's answer calls the five lines of code for setting the state and managing the try-finally block "too much boilerplate." You can reduce it to one line with an interfaced object and a helper function:
type
TTemporaryFlag = class(TInterfacedObject)
private
FFlag: PBoolean;
public
constructor Create(Flag: PBoolean);
destructor Destroy; override;
end;
function TemporaryFlag(Flag: PBoolean): IUnknown;
begin
Result := TTemporaryFlag.Create(FFlag);
end;
constructor TTemporaryFlag.Create;
begin
inherited;
FFlag := Flag;
FFlag^ := True;
end;
destructor TTemporaryFlag.Destroy;
begin
FFlag^ := False;
inherited;
end;
Use it like this:
begin
TemporaryFlag(@ActionIsExecuting);
ShellExecAndWait(...);
end;
The function returns an interface reference, which the compiler stores in an implicitly declared temporary variable. At the end of the code, that variable is destroyed, and the stored interfaced object gets deallocated, returning the flag to its previous value.
Upvotes: 7
Reputation: 676
You could place all ui elements on a panel, and disable the panel. Your application will still respond to moves, resizes, repaints, etc.
Upvotes: 0
Reputation: 36654
This solution depends on the actual Action or ActionManager component. Still too much "boilerplate" code. Also very fragile, as it assumes that the Sender is a TAction instance.
procedure TMyForm.OnMyAction(Sender: TObject);
begin
try
// disable all actions
(Sender as TAction).ActionList.State := asSuspended;
// do the call
ShellExecAndWait( ... );
finally
// enable all actions
(Sender as TAction).ActionList.State := asNormal;
end;
end;
Upvotes: 2