I wish to iterate/recurse through the components on a form.
I plan on iterating/recursing through the components to make bulk changes to a components of a particular type, but in order to do so, I need a handle to all components.
I checked Code Complete and Google but did not have any luck answering my own question.
Use the TWinControl.Controls[] property, eg:
Procedure DoSomething(AControl: TWinControl);
Var
I: Integer;
Ctrl: TControl;
Begin
If AControl is TSomeControl then
Begin
...
End;
For I := 0 to AControl.ControlCount-1 do
Begin
Ctrl := AControl.Controls[I];
If Ctrl is TWinControl then
DoSomething(TWinControl(Ctrl));
End;
End;
Procedure TMyForm.DoIt;
Begin
DoSomething(Self);
End;
Related
I have an installation file for my app and it contains the .exe file and a .zip file as well.
What I want is:
Recently I added a code in the [Code] section that is extracting files from zip, But actually, it happened after the installation is done, and the progress bar is 100%, So what I want is to make that code's (unzipping) process work with the progress bar and show the user what is extracting at the moment.
For example: let's say extracting files will take 50% of the progress bar and the rest will take it the code section while it is unzipping, with the state about what is extracting at the moment.
Here is the code:
[Code]
procedure InitializeWizard;
begin
ForceDirectories(ExpandConstant('{localappdata}\folder-A\app\folder-B'))
end;
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
procedure unzip(ZipFile, TargetFldr: variant);
var
shellobj: variant;
SrcFldr, DestFldr: variant;
shellfldritems: variant;
begin
if FileExists(ZipFile) then begin
if not DirExists(TargetFldr) then
if not ForceDirectories(TargetFldr) then begin
MsgBox('Can not create folder '+TargetFldr+' !!', mbError, MB_OK);
Exit;
end;
shellobj := CreateOleObject('Shell.Application');
SrcFldr := shellobj.NameSpace(ZipFile);
DestFldr := shellobj.NameSpace(TargetFldr);
shellfldritems := SrcFldr.Items;
DestFldr.CopyHere(
shellfldritems, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
unzip(ExpandConstant('{app}\example.zip'),ExpandConstant('{app}\'));
end;
end;
The easiest solution is to use the WizardForm.StatusLabel:
WizardForm.StatusLabel.Caption := 'Extracting...';
For more fancy solution you can use TOutputProgressWizardPage. For an example that even displays a progress bar (though by using a DLL for the extraction), see:
Inno Setup - How to add cancel button to decompressing page?
This post lists more related examples:
Inno Setup: How to modify long running script so it will not freeze GUI?
I use Delphi 7. I create a record that stores info, and using pointers I store that record as an object in a TreeView with more than 100 items.
My problem is, how to release or eliminate all these objects from memory?
type
PMyRec = ^TMyRec;
TMyRec = record
Tipo: string;
parent: string;
end;
var
MyRecPtr: PMyRec;
for x := 1 to 100 do
begin
New(MyRecPtr);
MyRecPtr^.Tipo := '1';
MyRecPtr^.parent := 'paul';
Tree1.Items.AddChildObject(nil, IntToStr(x) + '-NewItem', MyRecPtr);
ListaDePonteiros.Add( MyRecPtr ); // I use a TList to store pointers
ListaDeObjectos.Add( MyRecPtr ); // I use a TList to store Objects
end;
How I try to delete them all:
procedure TForm1.Button2Click(Sender: TObject);
procedure EmptyTList(Var AList: TList);
var
intContador: integer;
begin
for intContador := (AList.Count-1) downto 0 do
begin
if Assigned(AList.Items[intContador]) then
begin
Dispose(AList.Items[intContador]);
AList.Items[intContador] := nil;
AList.Delete(intContador);
end;
end;
end;
begin
if Assigned(MyRecPtr) then
begin
EmptyTList(ListaDePonteiros);
end;
end;
When I delete all items in the TreeView OnDelete event, I have this:
if assigned(Node.Data) then
begin
Dispose(Node.Data);
end;
What I want to do is release all objects from memory!
If I dispose all objects using that list then if I delete any item from the TreeView an invalid pointer error is raised!!
Even with all pointers disposed, MyRecPtr still points to somewhere in memory, and Node.Data too!
Your code is crashing because you are freeing the same memory twice, because you have not defined any clear ownership of your record instances.
Your ListaDePonteiros and ListaDeObjectos lists are redundant and can be removed. The TTreeView can be the owner of the records and you can simply Dispose() of them in the TTreeView.OnDeletion event and be done with it 1.
var
MyRecPtr: PMyRec;
for x := 1 to 100 do
begin
New(MyRecPtr);
try
MyRecPtr^.Tipo := '1';
MyRecPtr^.parent := 'paul';
Tree1.Items.AddChildObject(nil, IntToStr(x) + '-NewItem', MyRecPtr);
except
Dispose(MyRecPtr);
raise;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Tree1.Items.Clear;
end;
procedure TForm1.Tree1Deletion(Sender: TObject; Node: TTreeNode);
begin
if Assigned(Node.Data) then
Dispose(PMyRec(Node.Data));
end;
Otherwise, if you choose to keep a separate list, keep the ListaDeObjectos list and remove the ListaDePonteiros list (as there is no reason to maintain 2 lists tracking the exact same values). You would just need to decide whether you want ListaDeObjectos or Tree1 to own the records you allocate:
If ListaDeObjectos is to be the owner, DO NOT call Dispose(Node.Data) in the TTreeView.OnDeletion event.
var
MyRecPtr: PMyRec;
Idx: Integer;
for x := 1 to 100 do
begin
New(MyRecPtr);
try
MyRecPtr^.Tipo := '1';
MyRecPtr^.parent := 'paul';
Idx := ListaDeObjectos.Add(MyRecPtr);
try
Tree1.Items.AddChildObject(nil, IntToStr(x) + '-NewItem', MyRecPtr);
except
ListaDeObjectos.Delete(Idx);
raise;
end;
except
Dispose(MyRecPtr);
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
procedure EmptyTList(AList: TList);
var
intContador: integer;
begin
for intContador := 0 to (AList.Count-1) do
Dispose(PMyRec(AList[intContador]));
AList.Clear;
end;
begin
Tree1.Items.Clear;
EmptyTList(ListaDePonteiros);
end;
If Tree1 is to be the owner, DO NOT call Dispose(AList.Items[intContador]) in EmptyTList() (in fact, you can get rid of EmptyTList() altogether and just call ListaDeObjectos.Clear() when needed).
var
MyRecPtr: PMyRec;
Node: TNode;
for x := 1 to 100 do
begin
New(MyRecPtr);
try
MyRecPtr^.Tipo := '1';
MyRecPtr^.parent := 'paul';
Node := Tree1.Items.AddChildObject(nil, IntToStr(x) + '-NewItem', MyRecPtr);
except
Dispose(MyRecPtr);
raise;
end;
try
ListaDePonteiros.Add(MyRecPtr);
except
Node.Free;
raise;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
ListaDePonteiros.Clear;
Tree1.Items.Clear;
end;
procedure TForm1.Tree1Deletion(Sender: TObject; Node: TNode);
begin
if Assigned(Node.Data) then
Dispose(PMyRec(Node.Data));
end;
Either way, when not mass-clearing Tree1 and ListaDeObjectos in one go, consider calling ListaDeObjectos.Remove() in the TTreeView.OnDeletion event to keep Tree1 and ListaDeObjectos in sync when removing individual nodes:
procedure TForm1.Tree1Deletion(Sender: TObject; Node: TNode);
begin
if Assigned(Node.Data) then
begin
// only if the TreeView is the owner...
Dispose(PMyRec(Node.Data));
ListaDeObjectos.Remove(Node.Data);
end;
end;
1. Whenever you do Dispose() your record instance, make sure you type-cast raw pointers to PMyRec or else the compiler will not finalize the record's members correctly, leaking memory.
Let's say I want to uncheck components 3-5:
procedure CurPageChanged(CurPageID: Integer);
var i: integer;
begin
if CurPageID = wpSelectComponents then
{ 0 based index }
for i:= 2 to 4 do begin
WizardForm.ComponentsList.Checked[i] := False;
end;
end;
It works fine, they are disabled but total required size at the bottom of the window still shows value for all components. Is there a way to update it?
Edit: tried replacing
WizardForm.ComponentsList.Checked[i] := False;
with
WizardForm.ComponentsList.CheckItem(i, coUncheck);
but it didn't change anything.
Finally found it! Support Classes Reference page shows that TWizardForm.ComponentsList has property OnClickCheck which seems to be a pointer to unexposed TWizardForm.ComponentsListClickCheck procedure. All I neded to add was one line:
WizardForm.ComponentsList.OnClickCheck(nil);
Full example:
procedure CurPageChanged(CurPageID: Integer);
var i: integer;
begin
if CurPageID = wpSelectComponents then begin
{ 0 based index }
for i:= 2 to 4 do begin
WizardForm.ComponentsList.CheckItem(i, coUncheck);
end;
WizardForm.ComponentsList.OnClickCheck(nil);
end;
end;
I'm stuck on simple situation with OnClickCheck property. The problem is that I see a Msgbox every time I turn on backup task, but also (while it's switched on) OnClickCheckappeared on pressing uninst task too! Seems that OnClickCheck checks all clicks, but I need to check click only on the first task.
It is logical to add to "WizardForm.TasksList.OnClickCheck" exact number of task (WizardForm.TasksList.OnClickCheck[0]), but compiler doesn't agree with it.
[Tasks]
Name: backup; Description: do backup
Name: uninst; Description: do not create uninstaller
[Code]
procedure TaskOnClick(Sender: TObject);
begin
if IsTaskSelected('backup') then
begin
MsgBox('backup task has been checked.', mbInformation, MB_OK)
end;
end;
procedure InitializeWizard();
begin
WizardForm.TasksList.OnClickCheck := #TaskOnClick;
end;
There's no way tell exactly what task (list item) was changed in the OnClickCheck event.
To tell which item was checked by the user, you can use the ItemIndex property. The user can check only the selected item.
Though if you have a task hierarchy, even unselected task can be toggled automatically by the installer due to a change in child/parent items. So to tell all changes, all you can do is to remember the previous state and compare it against the current state, when the OnClickCheck is called.
var
TasksState: array of TCheckBoxState;
procedure TasksClickCheck(Sender: TObject);
var
I: Integer;
begin
for I := 0 to WizardForm.TasksList.Items.Count - 1 do
begin
if TasksState[I] <> WizardForm.TasksList.State[I] then
begin
Log(Format('Task %d state changed from %d to %d',
[I, TasksState[I], WizardForm.TasksList.State[I]]));
TasksState[I] := WizardForm.TasksList.State[I];
end;
end;
end;
procedure CurPageChanged(CurPageID: Integer);
var
I: Integer;
begin
if CurPageID = wpSelectTasks then
begin
{ Only now is the task list initialized (e.g. based on selected setup }
{ type and components). Remember what is the current/initial state. }
SetArrayLength(TasksState, WizardForm.TasksList.Items.Count);
for I := 0 to WizardForm.TasksList.Items.Count - 1 do
TasksState[I] := WizardForm.TasksList.State[I];
end;
end;
procedure InitializeWizard();
begin
WizardForm.TasksList.OnClickCheck := #TasksClickCheck;
end;
Instead of using indexes, you can also use task names with use of WizardSelectedTasks or WizardIsTaskSelected. For an example, see Inno Setup: how to auto select a component if another component is selected?
Also see:
Inno Setup ComponentsList OnClick event
Inno Setup Uncheck a task when another task is checked
Inno Setup - Show children component as sibling and show check instead of square in checkbox
Is it possible to disable the the Preparing To Install wpPreparing and the Installing wpInstalling Wizard Pages (i.e. the ones with the progress bar) so that they do not show during installation? There does not appear to be a built-in directive or method (e.g. for the Ready Wizard Page you can use DisableReadyPage=yes to do so). Am I missing something, or is it, as I suspect, simply not possible?
I have already tried using:
function ShouldSkipPage(CurPageID: Integer): Boolean;
begin
if CurPageID = wpPreparing then
Result := True;
if CurPageID = wpInstalling then
Result := True;
end;
Have you tried this - DisableReadyPage=yes in the [Setup] section.
Seems the only other option is to use the "install silently" command line switch. I'd be careful though this essentially installs a potentially destructive program without the users knowledge.
It is not possible to skip the wpPreparing or wpInstalling Wizard Pages. However, if the installer is not actually installing anything and is being used to return something, say an unlock code, as is the use case here, the following could be done instead:
//Disable the Exit confirmation prompt
procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean);
begin
Cancel := True;
Confirm := False;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
//Get the unlock code
if CurPageID = UnlockCodePage.ID then
begin
if UnlockCodePage.Values[0] = '' then
begin
MsgBox('You must enter an installation ID to generate an unlock code.',
mbError, MB_OK);
end
else
begin
UnlockCodePage.Values[1] := GetUnlockCode;
end;
Result := False;
end;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
//Define button visibility, whether the Back button is enabled and change the Next and Cancel button labels
WizardForm.CancelButton.Caption := '&Close';
if CurPageID = UnlockCodePage.ID then
begin
WizardForm.BackButton.Enabled := False;
WizardForm.NextButton.Caption := '&Generate';
end;
end;
Hopefully this may help someone looking to do something similar.