I am currently using Inno Download Plugin to download files for my installer, the biggest problem with this is that it fails to download the file correctly. Because of many reasons like bad connection. I would like to add an alternative method to download files, so the user may chose if he want regular way, or a torrent way. I know that I can use aria2c.exe app. Can someone help me with implementing it to the code of inno setup?
What I need is to download a 7z file using torrent (aria2.exe) and then unpack the contents to defined folder in {app} location.
Good code example is probably all I need.
Just run the aria2c, redirect its output to a file and poll the file contents for progress of the download.
It's actually very similar to my solution for this answer:
Make Inno Setup Installer report its installation progress status to master installer
#define TorrentMagnet "magnet:..."
[Files]
Source: aria2c.exe; Flags: dontcopy
[Code]
function BufferToAnsi(const Buffer: string): AnsiString;
var
W: Word;
I: Integer;
begin
SetLength(Result, Length(Buffer) * 2);
for I := 1 to Length(Buffer) do
begin
W := Ord(Buffer[I]);
Result[(I * 2)] := Chr(W shr 8); { high byte }
Result[(I * 2) - 1] := Chr(Byte(W)); { low byte }
end;
end;
function SetTimer(
Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
external 'SetTimer#user32.dll stdcall';
function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
external 'KillTimer#user32.dll stdcall';
var
ProgressPage: TOutputProgressWizardPage;
ProgressFileName: string;
procedure UpdateProgressProc(
H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
var
S: AnsiString;
I: Integer;
L: Integer;
P: Integer;
Max: Integer;
Progress: string;
Buffer: string;
Stream: TFileStream;
Transferred: string;
Percent: Integer;
Found: Boolean;
begin
Found := False;
try
{ Need shared read as the output file is locked for writting, }
{ so we cannot use LoadStringFromFile }
Stream := TFileStream.Create(ProgressFileName, fmOpenRead or fmShareDenyNone);
try
L := Stream.Size;
Max := 100*2014;
if L > Max then
begin
Stream.Position := L - Max;
L := Max;
end;
SetLength(Buffer, (L div 2) + (L mod 2));
Stream.ReadBuffer(Buffer, L);
S := BufferToAnsi(Buffer);
finally
Stream.Free;
end;
if S = '' then
begin
Log(Format('Progress file %s is empty', [ProgressFileName]));
end;
except
Log(Format('Failed to read progress from file %s', [ProgressFileName]));
end;
if S <> '' then
begin
P := Pos('[#', S);
if P = 0 then
begin
Log('Not found any progress line');
end
else
begin
repeat
Delete(S, 1, P - 1);
P := Pos(']', S);
Progress := Copy(S, 2, P - 2);
Delete(S, 1, P);
P := Pos('[#', S);
until (P = 0);
Log(Format('Found progress line: %s', [Progress]));
P := Pos(' ', Progress);
if P > 0 then
begin
Log('A');
Delete(Progress, 1, P);
P := Pos('(', Progress);
if P > 0 then
begin
Log('b');
Transferred := Copy(Progress, 1, P - 1);
Delete(Progress, 1, P);
P := Pos('%)', Progress);
if P > 0 then
begin
Log('c');
Percent := StrToIntDef(Copy(Progress, 1, P - 1), -1);
if Percent >= 0 then
begin
Log(Format('Transferred: %s, Percent: %d', [Transferred, Percent]));
ProgressPage.SetProgress(Percent, 100);
ProgressPage.SetText(Format('Transferred: %s', [Transferred]), '');
Found := True;
end;
end;
end;
end;
end;
end;
if not Found then
begin
Log('No new data found');
{ no new progress data, at least pump the message queue }
ProgressPage.SetProgress(ProgressPage.ProgressBar.Position, 100);
end;
end;
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
TorrentDownloaderPath: string;
TempPath: string;
CommandLine: string;
Timer: LongWord;
InstallError: string;
ResultCode: Integer;
S: AnsiString;
begin
ExtractTemporaryFile('aria2c.exe');
ProgressPage :=
CreateOutputProgressPage('Torrent download', 'Downloading torrent...');
ProgressPage.SetProgress(0, 100);
ProgressPage.Show;
try
Timer := SetTimer(0, 0, 250, CreateCallback(#UpdateProgressProc));
TempPath := ExpandConstant('{tmp}');
TorrentDownloaderPath := TempPath + '\aria2c.exe';
ProgressFileName := ExpandConstant('{tmp}\progress.txt');
Log(Format('Expecting progress in %s', [ProgressFileName]));
CommandLine :=
Format('"%s" "%s" > "%s"', [
TorrentDownloaderPath, '{#TorrentMagnet}', ProgressFileName]);
Log(Format('Executing: %s', [CommandLine]));
CommandLine := Format('/C "%s"', [CommandLine]);
if not Exec(ExpandConstant('{cmd}'), CommandLine, TempPath, SW_HIDE,
ewWaitUntilTerminated, ResultCode) then
begin
Result := 'Cannot start torrent download';
end
else
if ResultCode <> 0 then
begin
LoadStringFromFile(ProgressFileName, S);
Result := Format('Torrent download failed with code %d', [ResultCode]);
Log(Result);
Log('Output: ' + S);
end;
finally
{ Clean up }
KillTimer(0, Timer);
ProgressPage.Hide;
DeleteFile(ProgressFileName);
end;
end;
For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library.
The BufferToAnsi and its use is based on:
Inno Setup LoadStringFromFile fails when file is open in another process
Related
I created an Input page that executes a command line app using the created variables from those inputs. Naturally, the cmd window pop ups on my screen. I would like to know if there is any way to embed the cmd window (or the output) on my Inno Setup installer page.
I'm running Inno Setup 5.6.1 (because of Windows XP compatibility), but I'm OK if I have to switch to the last version.
[Code]
var
MAIL: TInputQueryWizardPage;
Final: TWizardPage;
BotonIniciar: Tbutton;
procedure BotonIniciarOnClick(Sender: TObject);
begin
WizardForm.NextButton.Onclick(nil);
Exec(ExpandConstant('{tmp}\imapsync.exe'),'MAIL.Values[0]','', SW_SHOW,
ewWaitUntilTerminated, ResultCode);
end;
procedure InitializeWizard;
begin
MAIL := CreateInputQueryPage(wpWelcome, '', '', '');
MAIL.Add('Please input your information', False);
BotonIniciar := TNewButton.Create(MAIL);
BotonIniciar.Caption := 'Iniciar';
BotonIniciar.OnClick := #BotonIniciarOnClick;
BotonIniciar.Parent := WizardForm;
BotonIniciar.Left := WizardForm.NextButton.Left - 250 ;
BotonIniciar.Top := WizardForm.CancelButton.Top - 10;
BotonIniciar.Width := WizardForm.NextButton.Width + 60;
BotonIniciar.Height := WizardForm.NextButton.Height + 10;
end;
I'm might be missing some parts of the code, but I think it's understandable.
Fist I create the input page, then I create a button with the OnClick property that calls to the BotonIniciarOnClick procedure.
Actually, the code works great. But as I said I'm having a floating cmd window.
I would like to see something like this:
It's just a random image I took from google.
What I want to see is similar to a standard "show details" option on an installer.
You can redirect the command output to a file and monitor the file for changes, loading them to list box (or maybe a memo box).
var
ProgressPage: TOutputProgressWizardPage;
ProgressListBox: TNewListBox;
function SetTimer(
Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
external 'SetTimer#user32.dll stdcall';
function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
external 'KillTimer#user32.dll stdcall';
var
ProgressFileName: string;
function BufferToAnsi(const Buffer: string): AnsiString;
var
W: Word;
I: Integer;
begin
SetLength(Result, Length(Buffer) * 2);
for I := 1 to Length(Buffer) do
begin
W := Ord(Buffer[I]);
Result[(I * 2)] := Chr(W shr 8); { high byte }
Result[(I * 2) - 1] := Chr(Byte(W)); { low byte }
end;
end;
procedure UpdateProgress;
var
S: AnsiString;
I, L, Max: Integer;
Buffer: string;
Stream: TFileStream;
Lines: TStringList;
begin
if not FileExists(ProgressFileName) then
begin
Log(Format('Progress file %s does not exist', [ProgressFileName]));
end
else
begin
try
// Need shared read as the output file is locked for writing,
// so we cannot use LoadStringFromFile
Stream :=
TFileStream.Create(ProgressFileName, fmOpenRead or fmShareDenyNone);
try
L := Stream.Size;
Max := 100*2014;
if L > Max then
begin
Stream.Position := L - Max;
L := Max;
end;
SetLength(Buffer, (L div 2) + (L mod 2));
Stream.ReadBuffer(Buffer, L);
S := BufferToAnsi(Buffer);
finally
Stream.Free;
end;
except
Log(Format('Failed to read progress from file %s - %s', [
ProgressFileName, GetExceptionMessage]));
end;
end;
if S <> '' then
begin
Log('Progress len = ' + IntToStr(Length(S)));
Lines := TStringList.Create();
Lines.Text := S;
for I := 0 to Lines.Count - 1 do
begin
if I < ProgressListBox.Items.Count then
begin
ProgressListBox.Items[I] := Lines[I];
end
else
begin
ProgressListBox.Items.Add(Lines[I]);
end
end;
ProgressListBox.ItemIndex := ProgressListBox.Items.Count - 1;
ProgressListBox.Selected[ProgressListBox.ItemIndex] := False;
Lines.Free;
end;
// Just to pump a Windows message queue (maybe not be needed)
ProgressPage.SetProgress(0, 1);
end;
procedure UpdateProgressProc(
H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
begin
UpdateProgress;
end;
procedure BotonIniciarOnClick(Sender: TObject);
var
ResultCode: Integer;
Timer: LongWord;
AppPath: string;
AppError: string;
Command: string;
begin
ProgressPage :=
CreateOutputProgressPage(
'Installing something', 'Please wait until this finishes...');
ProgressPage.Show();
ProgressListBox := TNewListBox.Create(WizardForm);
ProgressListBox.Parent := ProgressPage.Surface;
ProgressListBox.Top := 0;
ProgressListBox.Left := 0;
ProgressListBox.Width := ProgressPage.SurfaceWidth;
ProgressListBox.Height := ProgressPage.SurfaceHeight;
// Fake SetProgress call in UpdateProgressProc will show it,
// make sure that user won't see it
ProgressPage.ProgressBar.Top := -100;
try
Timer := SetTimer(0, 0, 250, CreateCallback(#UpdateProgressProc));
ExtractTemporaryFile('install.bat');
AppPath := ExpandConstant('{tmp}\install.bat');
ProgressFileName := ExpandConstant('{tmp}\progress.txt');
Log(Format('Expecting progress in %s', [ProgressFileName]));
Command := Format('""%s" > "%s""', [AppPath, ProgressFileName]);
if not Exec(ExpandConstant('{cmd}'), '/c ' + Command, '', SW_HIDE,
ewWaitUntilTerminated, ResultCode) then
begin
AppError := 'Cannot start app';
end
else
if ResultCode <> 0 then
begin
AppError := Format('App failed with code %d', [ResultCode]);
end;
UpdateProgress;
finally
// Clean up
KillTimer(0, Timer);
ProgressPage.Hide;
DeleteFile(ProgressFileName);
ProgressPage.Free();
end;
if AppError <> '' then
begin
// RaiseException does not work properly while
// TOutputProgressWizardPage is shown
RaiseException(AppError);
end;
end;
Above was tested with a batch file like:
#echo off
echo Starting
echo Doing A...
echo Extracting something...
echo Doing B...
echo Extracting something...
timeout /t 1 > nul
echo Doing C...
echo Extracting something...
echo Doing D...
echo Extracting something...
timeout /t 1 > nul
echo Doing E...
echo Extracting something...
echo Doing F...
echo Extracting something...
timeout /t 1 > nul
...
If you want to display the output as part of the installation process, instead of on a button click, see:
Execute a batch file after installation and display its output on a custom page before Finished page in Inno Setup
Hello all,
i am having problems getting the file preview (the one shown on the right side in the Windows Explorer window) for a certain file.
So far fetching the file preview works fine, but it takes a long time (between 0.5 and 2 seconds). Thus i do not want it to be executed in the main thread (as this would interrupt the program gui).
I tried to execute the file preview extraction in a worker thread, but this yields a SIGSEGV.
The call stack is also not really useful, it only shows that the exception is raised in ShellObjHelper in Line 141 (see source code below).
Source Code for main unit:
type
TThreadedImageInfo = record
fileName: String;
width: integer;
height: integer;
icon: TIcon;
image: TImage;
bmp: TBitmap;
infoOut: String;
memo: TMemo;
end;
PThreadedImageInfo = ^TThreadedImageInfo;
procedure loadThumbnailImageFromFile(aData: Pointer);
var
XtractImage: IExtractImage;
ColorDepth: integer;
Flags: DWORD;
RT: IRunnableTask;
FileName: string;
pThreadInfo: PThreadedImageInfo;
begin
pThreadInfo := PThreadedImageInfo(aData);
if assigned(pThreadInfo) then begin
FileName := pThreadInfo^.fileName;
ColorDepth := 32;
Flags := IEIFLAG_ASPECT or IEIFLAG_QUALITY or IEIFLAG_ORIGSIZE; // = 580
if FileExists(FileName) then begin
if GetExtractImageItfPtr(FileName, XTractImage) then begin
if ExtractImageGetFileThumbnail(XtractImage, pthreadinfo^.Image.Width,
pthreadinfo^.Image.Height, ColorDepth, Flags, RT, pthreadinfo^.Bmp) then begin
if (Flags and IEIFLAG_CACHE) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not cache the thumbnail.' + #13;
if (Flags and IEIFLAG_GLEAM) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'The image has a gleam.' + #13;
if (Flags and IEIFLAG_NOSTAMP) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an icon stamp on the thumbnail.' + #13;
if (Flags and IEIFLAG_NOBORDER) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an a border around the thumbnail.' + #13;
end else if GetFileLargeIcon(FileName, pThreadInfo^.icon) then begin
pThreadInfo^.infoOut := 'Thumbnail is not available. Default icon displayed.';
end;
end else begin
pThreadInfo^.infoOut := 'Error loading IExtractImage.';
end;
end else begin
pThreadInfo^.infoOut := 'Error: File does not exist.';
end;
end;
end;
procedure threadDone(Sender: TObject; aData: Pointer);
var
pThreadInfo: PThreadedImageInfo;
begin
pthreadInfo := PThreadedImageInfo(aData);
if assigned(pThreadInfo) then begin
if assigned(pthreadInfo^.Bmp) then begin
pthreadinfo^.Image.Picture.Assign(pthreadInfo^.Bmp);
end else if assigned(pthreadInfo^.icon) then begin
pthreadinfo^.Image.Picture.Assign(pthreadInfo^.icon);
end else begin
pThreadInfo^.Image.Picture.Assign(nil);
end;
if assigned(pThreadInfo^.memo) then
pThreadInfo^.memo.Lines.Text := pThreadInfo^.infoOut;
if assigned(pthreadInfo^.icon) then
pthreadInfo^.icon.free();
if assigned(pthreadInfo^.bmp) then
pthreadInfo^.bmp.free();
end;
dispose(pthreadinfo);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
pThreadInfo: PThreadedImageInfo;
begin
new(pThreadInfo);
pThreadInfo^.fileName := Edit1.Text;
pThreadInfo^.image := Image1;
pThreadInfo^.memo := Memo1;
pThreadInfo^.icon := nil;
pThreadInfo^.bmp := nil;
pThreadInfo^.infoOut := '';
// use worker thread:
//TThread.ExecuteInThread(#loadThumbnailImageFromFile, pThreadInfo, #threadDone);
// use main thread:
loadThumbnailImageFromFile(pThreadInfo);
threadDone(nil, pThreadInfo);
end;
Source code for helper unit:
unit ShellObjHelper;
{$MODE objfpc}{$H+}
{$IFDEF VER100}{$DEFINE DELPHI3}{$ENDIF}
interface
uses
Windows, ShlObj, ActiveX, ShellAPI, Graphics, SysUtils, ComObj;
type
{ from ShlObjIdl.h }
IExtractImage = interface
['{BB2E617C-0920-11D1-9A0B-00C04FC2D6C1}']
function GetLocation(Buffer: PWideChar; BufferSize: DWORD; var Priority: DWORD; var Size: TSize;
ColorDepth: DWORD; var Flags: DWORD): HResult; stdcall;
function Extract(var BitmapHandle: HBITMAP): HResult; stdcall;
end;
IRunnableTask = interface
['{85788D00-6807-11D0-B810-00C04FD706EC}']
function Run: HResult; stdcall;
function Kill(fWait: BOOL): HResult; stdcall;
function Suspend: HResult; stdcall;
function Resume: HResult; stdcall;
function IsRunning: Longint; stdcall;
end;
const
{ from ShlObjIdl.h }
ITSAT_MAX_PRIORITY = 2;
ITSAT_MIN_PRIORITY = 1;
ITSAT_DEFAULT_PRIORITY = 0;
IEI_PRIORITY_MAX = ITSAT_MAX_PRIORITY;
IEI_PRIORITY_MIN = ITSAT_MIN_PRIORITY;
IEIT_PRIORITY_NORMAL = ITSAT_DEFAULT_PRIORITY;
IEIFLAG_ASYNC = $001; // ask the extractor if it supports ASYNC extract (free threaded)
IEIFLAG_CACHE = $002; // returned from the extractor if it does NOT cache the thumbnail
IEIFLAG_ASPECT = $004; // passed to the extractor to beg it to render to the aspect ratio of the supplied rect
IEIFLAG_OFFLINE = $008; // if the extractor shouldn't hit the net to get any content needs for the rendering
IEIFLAG_GLEAM = $010; // does the image have a gleam? this will be returned if it does
IEIFLAG_SCREEN = $020; // render as if for the screen (this is exlusive with IEIFLAG_ASPECT)
IEIFLAG_ORIGSIZE = $040; // render to the approx size passed, but crop if neccessary
IEIFLAG_NOSTAMP = $080; // returned from the extractor if it does NOT want an icon stamp on the thumbnail
IEIFLAG_NOBORDER = $100; // returned from the extractor if it does NOT want an a border around the thumbnail
IEIFLAG_QUALITY = $200; // passed to the Extract method to indicate that a slower, higher quality image is desired,
// re-compute the thumbnail
// IShellFolder methods helper
procedure ShellFolderBindToObject(const ShellFolder: IShellFolder; PIDL: PItemIDList; const riid: TGUID; out pv);
function ShellFolderGetUIObjectOf(const ShellFolder: IShellFolder; cidl: DWORD; var PIDL: PItemIDList;
riid: TGUID; out pv): Boolean;
procedure ShellFolderParseDisplayName(const ShellFolder: IShellFolder; const DisplayName: string; out PIDL: PItemIDList);
function GetExtractImageItfPtr(const FileName: string; out XtractImage: IExtractImage): Boolean;
function GetFileLargeIcon(const FileName: string; out LargeIcon: TIcon): Boolean;
function ExtractImageGetFileThumbnail(const XtractImage: IExtractImage; ImgWidth, ImgHeight, ImgColorDepth: Integer;
var Flags: DWORD; out RunnableTask: IRunnableTask; out Bmp: TBitmap): Boolean;
procedure GetShellFolderItfPtr(const FolderName: string; Malloc: IMalloc; out TargetFolder: IShellFolder);
implementation
procedure ShellFolderBindToObject(const ShellFolder: IShellFolder; PIDL: PItemIDList; const riid: TGUID; out pv);
begin
OleCheck(ShellFolder.BindToObject(PIDL, nil, riid, {$IFDEF DELPHI3}Pointer(pv){$ELSE}pv{$ENDIF}));
end;
function ShellFolderGetUIObjectOf(const ShellFolder: IShellFolder; cidl: DWORD; var PIDL: PItemIDList;
riid: TGUID; out pv): Boolean;
begin
Result := NOERROR = ShellFolder.GetUIObjectOf(0, cidl, PIDL, riid, nil, {$IFDEF DELPHI3}Pointer(pv){$ELSE}pv{$ENDIF});
end;
procedure ShellFolderParseDisplayName(const ShellFolder: IShellFolder; const DisplayName: string; out PIDL: PItemIDList);
var
Attributes, Eaten: DWORD;
begin
OleCheck(ShellFolder.ParseDisplayName(0, nil, PWideChar(WideString(DisplayName)), Eaten, PIDL, Attributes));
end;
function GetExtractImageItfPtr(const FileName: string; out XtractImage: IExtractImage): Boolean;
var
TargetFolder: IShellFolder;
FilePath: string;
ItemIDList: PItemIDList;
Malloc: IMalloc;
begin
FilePath := ExcludeTrailingBackslash(ExtractFilePath(FileName));
OleCheck(SHGetMalloc(Malloc));
GetShellFolderItfPtr(FilePath, Malloc, TargetFolder);
ShellFolderParseDisplayName(TargetFolder, ExtractFileName(FileName), ItemIDList);
try
Result := ShellFolderGetUIObjectOf(TargetFolder, 1, ItemIDList, IExtractImage, XtractImage);
finally
Malloc.Free(ItemIDList);
end;
end;
function GetFileLargeIcon(const FileName: string; out LargeIcon: TIcon): Boolean;
var
SFI: TSHFileInfo;
begin
result := SHGetFileInfo(PChar(FileName), FILE_ATTRIBUTE_ARCHIVE, SFI, sizeof(SFI), SHGFI_ICON or SHGFI_LARGEICON) <> 0;
if result then begin
LargeIcon := TIcon.Create;
LargeIcon.Handle := SFI.hIcon;
end;
end;
function ExtractImageGetFileThumbnail(const XtractImage: IExtractImage; ImgWidth, ImgHeight, ImgColorDepth: Integer;
var Flags: DWORD; out RunnableTask: IRunnableTask; out Bmp: TBitmap): Boolean;
var
Size: TSize;
Buf: array[0..MAX_PATH] of WideChar;
BmpHandle: HBITMAP;
Priority: DWORD;
GetLocationRes: HRESULT;
procedure FreeAndNilBitmap;
begin
{$IFNDEF DELPHI3}
FreeAndNil(Bmp);
{$ELSE}
Bmp.Free;
Bmp := nil;
{$ENDIF}
end;
begin
Result := False;
RunnableTask := nil;
Size.cx := ImgWidth;
Size.cy := ImgHeight;
Priority := IEIT_PRIORITY_NORMAL;
Flags := Flags or IEIFLAG_ASYNC;
////////////////////////// EXCEPTION HERE, but only when multithreading /////////////////////////////////////////////////////
GetLocationRes := XtractImage.GetLocation(Buf, sizeof(Buf), Priority, Size, ImgColorDepth, Flags);
if (GetLocationRes = NOERROR) or (GetLocationRes = E_PENDING) then begin
if GetLocationRes = E_PENDING then begin
{ if QI for IRunnableTask succeed, we can use RunnableTask
interface pointer later to kill running extraction process.
We could spawn a new thread here to extract image. }
if S_OK <> XtractImage.QueryInterface(IRunnableTask, RunnableTask) then
RunnableTask := nil;
end;
Bmp := TBitmap.Create;
try
// This could consume a long time.
// If RunnableTask is available then calling Kill() method will immediately abort the process.
OleCheck(XtractImage.Extract(BmpHandle));
Bmp.Handle := BmpHandle;
Result := True;
except
on E: EOleSysError do begin
//-------------
OutputDebugString(PChar(string(E.ClassName) + ': ' + E.Message));
//-------------
FreeAndNilBitmap;
Result := False;
end else begin
FreeAndNilBitmap;
raise;
end;
end; { try/except }
end;
end;
procedure GetShellFolderItfPtr(const FolderName: string; Malloc: IMalloc; out TargetFolder: IShellFolder);
var
DesktopFolder: IShellFolder;
ItemIDList: PItemIDList;
begin
OleCheck(SHGetDesktopFolder(DesktopFolder));
ShellFolderParseDisplayName(DesktopFolder, FolderName, ItemIDList);
try
ShellFolderBindToObject(DesktopFolder, ItemIDList, IShellFolder, TargetFolder);
finally
Malloc.Free(ItemIDList);
end;
end;
end.
The actual question(s):
Why is the image extraction working without multithreading, but failing when using a worker thread?
How can i make this work?
I already started studying this post for another solution, but i am not yet sure how to do this.
Useful Informations:
Source for helper unit code: How to retrieve the file previews used by windows explorer in Windows vista and seven?
Multithreading example: https://lazarus-ccr.sourceforge.io/docs/rtl/classes/tthread.executeinthread.html
Activating PDF preview: open Adobe Acrobat Reader -> Edit -> Preferences -> General -> check "Enable PDF thumbnail previews"
I am using Lazarus v2.0.10 r63526 on Windows 10 Pro 64 bit.
Thanks to the comment from #IInspectable, that's the hint i needed.
Solution:
Add CoInitialize before calling GetExtractImageItfPtr and add CoUninitialize after receiving the file preview, but still within the worker thread.
Ensure that CoUninitialize is called even if exceptions occur by using try and finally`.
Working source code for main unit with worker thread:
type
TThreadedImageInfo = record
fileName: String;
width: integer;
height: integer;
icon: TIcon;
image: TImage;
bmp: TBitmap;
infoOut: String;
memo: TMemo;
end;
PThreadedImageInfo = ^TThreadedImageInfo;
procedure loadThumbnailImageFromFile(aData: Pointer);
var
XtractImage: IExtractImage;
ColorDepth: integer;
Flags: DWORD;
RT: IRunnableTask;
FileName: string;
pThreadInfo: PThreadedImageInfo;
begin
pThreadInfo := PThreadedImageInfo(aData);
if assigned(pThreadInfo) then begin
FileName := pThreadInfo^.fileName;
ColorDepth := 32;
Flags := IEIFLAG_ASPECT or IEIFLAG_QUALITY or IEIFLAG_ORIGSIZE; // = 580
if FileExists(FileName) then begin
CoInitialize(nil);
try
if GetExtractImageItfPtr(FileName, XTractImage) then begin
if ExtractImageGetFileThumbnail(XtractImage, pthreadinfo^.Image.Width,
pthreadinfo^.Image.Height, ColorDepth, Flags, RT, pthreadinfo^.Bmp) then begin
if (Flags and IEIFLAG_CACHE) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not cache the thumbnail.' + #13;
if (Flags and IEIFLAG_GLEAM) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'The image has a gleam.' + #13;
if (Flags and IEIFLAG_NOSTAMP) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an icon stamp on the thumbnail.' + #13;
if (Flags and IEIFLAG_NOBORDER) <> 0 then
pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an a border around the thumbnail.' + #13;
end else if GetFileLargeIcon(FileName, pThreadInfo^.icon) then begin
pThreadInfo^.infoOut := 'Thumbnail is not available. Default icon displayed.';
end;
end else begin
pThreadInfo^.infoOut := 'Error loading IExtractImage.';
end;
finally
CoUninitialize;
end;
end else begin
pThreadInfo^.infoOut := 'Error: File does not exist.';
end;
end;
end;
procedure threadDone(Sender: TObject; aData: Pointer);
var
pThreadInfo: PThreadedImageInfo;
begin
pthreadInfo := PThreadedImageInfo(aData);
if assigned(pThreadInfo) then begin
if assigned(pthreadInfo^.Bmp) then begin
pthreadinfo^.Image.Picture.Assign(pthreadInfo^.Bmp);
end else if assigned(pthreadInfo^.icon) then begin
pthreadinfo^.Image.Picture.Assign(pthreadInfo^.icon);
end else begin
pThreadInfo^.Image.Picture.Assign(nil);
end;
if assigned(pThreadInfo^.memo) then
pThreadInfo^.memo.Lines.Text := pThreadInfo^.infoOut;
if assigned(pthreadInfo^.icon) then
pthreadInfo^.icon.free();
if assigned(pthreadInfo^.bmp) then
pthreadInfo^.bmp.free();
end;
dispose(pthreadinfo);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
pThreadInfo: PThreadedImageInfo;
begin
new(pThreadInfo);
pThreadInfo^.fileName := Edit1.Text;
pThreadInfo^.image := Image1;
pThreadInfo^.memo := Memo1;
pThreadInfo^.icon := nil;
pThreadInfo^.bmp := nil;
pThreadInfo^.infoOut := '';
TThread.ExecuteInThread(#loadThumbnailImageFromFile, pThreadInfo, #threadDone);
end;
I am using this code: Inno Setup - How to add cancel button to decompressing page? (answer of Martin Prikryl) to decompress an arc file with Inno Setup.
I want to have the possibility of decompress more than one arc file to install files from components selection (for example). But still show on overall progress bar for all extractions. whole Is this possible?
This is modification of my answer to Inno Setup - How to add cancel button to decompressing page?
Prerequisities are the same, refer to the other answer.
In the ExtractArc, call AddArchive for each archive you want to extract.
[Files]
Source: unarc.dll; Flags: dontcopy
[Code]
const
ArcCancelCode = -10;
function FreeArcExtract(
Callback: LongWord;
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: PAnsiChar): Integer;
external 'FreeArcExtract#files:unarc.dll cdecl';
const
CP_UTF8 = 65001;
function WideCharToMultiByte(CodePage: UINT; dwFlags: DWORD;
lpWideCharStr: string; cchWideChar: Integer; lpMultiByteStr: AnsiString;
cchMultiByte: Integer; lpDefaultCharFake: Integer;
lpUsedDefaultCharFake: Integer): Integer;
external 'WideCharToMultiByte#kernel32.dll stdcall';
function GetStringAsUtf8(S: string): AnsiString;
var
Len: Integer;
begin
Len := WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, 0, 0, 0);
SetLength(Result, Len);
WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, Len, 0, 0);
end;
var
ArcTotalSize: Integer;
ArcTotalExtracted: Integer;
ArcExtracted: Integer;
ArcCancel: Boolean;
ArcProgressPage: TOutputProgressWizardPage;
function FreeArcCallback(
AWhat: PAnsiChar; Int1, Int2: Integer; Str: PAnsiChar): Integer;
var
What: string;
begin
What := AWhat;
if What = 'origsize' then
begin
Log(Format('Adding archive with files with total size %d MB', [Int1]));
ArcTotalSize := ArcTotalSize + Int1;
end
else
if What = 'write' then
begin
if ArcTotalSize > 0 then
begin
ArcProgressPage.SetProgress(ArcTotalExtracted + Int1, ArcTotalSize);
end;
ArcExtracted := Int1;
end
else
begin
// Just to pump message queue more often (particularly for 'read' callbacks),
// to get more smooth progress bar
if (ArcExtracted > 0) and (ArcTotalSize > 0) then
begin
ArcProgressPage.SetProgress(ArcTotalExtracted + ArcExtracted, ArcTotalSize);
end;
end;
if ArcCancel then Result := ArcCancelCode
else Result := 0;
end;
procedure FreeArcCmd(
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: string);
var
ArcResult: Integer;
begin
ArcCancel := False;
ArcResult :=
FreeArcExtract(
CreateCallback(#FreeArcCallback),
GetStringAsUtf8(Cmd1), GetStringAsUtf8(Cmd2), GetStringAsUtf8(Cmd3),
GetStringAsUtf8(Cmd4), GetStringAsUtf8(Cmd5), GetStringAsUtf8(Cmd6),
GetStringAsUtf8(Cmd7), GetStringAsUtf8(Cmd8), GetStringAsUtf8(Cmd9),
GetStringAsUtf8(Cmd10));
if ArcCancel then
begin
RaiseException('Extraction cancelled');
end
else
if ArcResult <> 0 then
begin
RaiseException(Format('Extraction failed with code %d', [ArcResult]));
end;
end;
var
ArcArchives: array of string;
procedure AddArchive(ArchivePath: string);
begin
SetArrayLength(ArcArchives, GetArrayLength(ArcArchives) + 1);
ArcArchives[GetArrayLength(ArcArchives) - 1] := ArchivePath;
FreeArcCmd('l', '--', ArchivePath, '', '', '', '', '', '', '');
end;
procedure UnPackArchives(DestPath: string);
var
I: Integer;
ArchivePath: string;
begin
Log(Format('Total size of files to be extracted is %d MB', [ArcTotalSize]));
ArcTotalExtracted := 0;
for I := 0 to GetArrayLength(ArcArchives) - 1 do
begin
ArcExtracted := 0;
ArchivePath := ArcArchives[I];
Log(Format('Extracting %s', [ArchivePath]));
FreeArcCmd('x', '-o+', '-dp' + DestPath, '-w' + DestPath, '--', ArchivePath,
'', '', '', '');
ArcTotalExtracted := ArcTotalExtracted + ArcExtracted;
end;
end;
procedure UnpackCancelButtonClick(Sender: TObject);
begin
ArcCancel := True;
end;
procedure ExtractArc;
var
PrevCancelButtonClick: TNotifyEvent;
begin
ArcProgressPage :=
CreateOutputProgressPage('Decompression', 'Decompressing archive...');
ArcProgressPage.SetProgress(0, 100);
ArcProgressPage.Show;
try
WizardForm.CancelButton.Visible := True;
WizardForm.CancelButton.Enabled := True;
PrevCancelButtonClick := WizardForm.CancelButton.OnClick;
WizardForm.CancelButton.OnClick := #UnpackCancelButtonClick;
try
AddArchive(ExpandConstant('{src}\test1.arc'));
AddArchive(ExpandConstant('{src}\test2.arc'));
Log('Arc extraction starting');
UnPackArchives(ExpandConstant('{app}'));
except
MsgBox(GetExceptionMessage(), mbError, MB_OK);
end;
finally
Log('Arc extraction done');
ArcProgressPage.Hide;
WizardForm.CancelButton.OnClick := PrevCancelButtonClick;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
ExtractArc;
end;
end;
For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library.
I am using this code: How to add .arc decompression to Inno Setup? (answer of Martin Prikryl). I want to add a cancel button at decompressing page and active this page for others functions (when the decompression is active, this page is inactive and, for example, i can not press on/off button of my music implementation).
How to add a cancel button to decompression page? and how to active this page for others functions?
I have reimplemented the solution from How to add .arc decompression to Inno Setup? using unarc.dll (from FreeArc+InnoSetup package ISFreeArcExtract v.4.0.rar).
It greatly simplifies the code and also makes it easier to add the ability to cancel the decompression.
#define ArcArchive "test.arc"
[Files]
Source: unarc.dll; Flags: dontcopy
[Code]
const
ArcCancelCode = -10;
function FreeArcExtract(
Callback: LongWord;
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: PAnsiChar
): Integer;
external 'FreeArcExtract#files:unarc.dll cdecl';
const
CP_UTF8 = 65001;
function WideCharToMultiByte(CodePage: UINT; dwFlags: DWORD;
lpWideCharStr: string; cchWideChar: Integer; lpMultiByteStr: AnsiString;
cchMultiByte: Integer; lpDefaultCharFake: Integer;
lpUsedDefaultCharFake: Integer): Integer;
external 'WideCharToMultiByte#kernel32.dll stdcall';
function GetStringAsUtf8(S: string): AnsiString;
var
Len: Integer;
begin
Len := WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, 0, 0, 0);
SetLength(Result, Len);
WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, Len, 0, 0);
end;
var
ArcTotalSize: Integer;
ArcExtracted: Integer;
ArcCancel: Boolean;
ArcProgressPage: TOutputProgressWizardPage;
function FreeArcCallback(
AWhat: PAnsiChar; Int1, Int2: Integer; Str: PAnsiChar): Integer;
var
What: string;
begin
What := AWhat;
if What = 'origsize' then
begin
ArcTotalSize := Int1;
Log(Format('Total size of files to be extracted is %d MB', [ArcTotalSize]));
end
else
if What = 'write' then
begin
if ArcTotalSize > 0 then
begin
ArcProgressPage.SetProgress(Int1, ArcTotalSize);
end;
ArcExtracted := Int1;
end
else
begin
// Just to pump message queue more often (particularly for 'read' callbacks),
// to get more smooth progress bar
if (ArcExtracted > 0) and (ArcTotalSize > 0) then
begin
ArcProgressPage.SetProgress(ArcExtracted, ArcTotalSize);
end;
end;
if ArcCancel then Result := ArcCancelCode
else Result := 0;
end;
function FreeArcCmd(
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: string): Integer;
begin
ArcCancel := False;
try
Result :=
FreeArcExtract(
CreateCallback(#FreeArcCallback),
GetStringAsUtf8(Cmd1), GetStringAsUtf8(Cmd2), GetStringAsUtf8(Cmd3),
GetStringAsUtf8(Cmd4), GetStringAsUtf8(Cmd5), GetStringAsUtf8(Cmd6),
GetStringAsUtf8(Cmd7), GetStringAsUtf8(Cmd8), GetStringAsUtf8(Cmd9),
GetStringAsUtf8(Cmd10));
Log(Format('Arc command "%s" result %d', [Cmd1, Result]));
except
Result := -63;
end;
end;
function UnPackArchive(ArchivePath: string; DestPath: string): Integer;
begin
{ Find out length of files to be extracted - origsize }
Result := FreeArcCmd('l', '--', ArchivePath, '', '', '', '', '', '', '');
if Result = 0 then
begin
// Actually extract
Result :=
FreeArcCmd('x', '-o+', '-dp' + DestPath, '-w' + DestPath, '--', ArchivePath,
'', '', '', '');
end;
end;
procedure UnpackCancelButtonClick(Sender: TObject);
begin
ArcCancel := True;
end;
procedure ExtractArc;
var
ArcArchivePath: string;
UnpackResult: Integer;
PrevCancelButtonClick: TNotifyEvent;
Error: string;
begin
ArcProgressPage :=
CreateOutputProgressPage('Decompression', 'Decompressing archive...');
ArcProgressPage.SetProgress(0, 100);
ArcProgressPage.Show;
try
WizardForm.CancelButton.Visible := True;
WizardForm.CancelButton.Enabled := True;
PrevCancelButtonClick := WizardForm.CancelButton.OnClick;
WizardForm.CancelButton.OnClick := #UnpackCancelButtonClick;
ArcArchivePath := ExpandConstant('{src}\{#ArcArchive}');
Log(Format('Arc extraction starting - %s', [ArcArchivePath]));
ArcExtracted := 0;
UnpackResult := UnPackArchive(ArcArchivePath, ExpandConstant('{app}'));
if UnpackResult <> 0 then
begin
if ArcCancel then
begin
Error := 'Extraction cancelled';
end
else
begin
Error := Format('Extraction failed with code %d', [UnpackResult]);
end;
MsgBox(Error, mbError, MB_OK);
end;
finally
Log('Arc extraction cleanup');
ArcProgressPage.Hide;
WizardForm.CancelButton.OnClick := PrevCancelButtonClick;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
ExtractArc;
end;
end;
For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library.
The code extracts a separate .arc file. If you want to embed the archive to the installer, you can use
[Files]
Source: {#ArcArchive}; DestDir: "{tmp}"; Flags: nocompression deleteafterinstall
And extract the archive from the {tmp}:
ArcArchivePath := ExpandConstant('{tmp}\{#ArcArchive}');
Note that the unarc.dll from ISFreeArcExtract v.4.0.rar does not seem to support password protected archives. The version from ISFreeArcExtract v.4.2.rar does, but I'm not aware of trustworthy download link.
If you want to extract multiple archives, see Inno Setup - How to add multiple arc files to decompress?
All you need is this
http://fileforums.com/showthread.php?t=96619
This program have the latest compatibility with inno setup and also supports Password based file with multiple extensions.
I am building an install with Inno Setup and I am using the components section to allow the end user to select optional items to install.
Some of these items need a longer description in order for the user to have enough information to intelligently select them.
Is there a way to add more in-depth descriptions somewhere?
This solution uses only Inno Setup proper (not the obsolete 3rd party build of the Inno Setup of suspicious origin).
The solution is partially based on my answer to Inno Setup: OnHover event.
Adjust the HoverComponentChanged procedure to your needs.
[Code]
var
LastMouse: TPoint;
CompLabel: TLabel;
function GetCursorPos(var lpPoint: TPoint): BOOL;
external 'GetCursorPos#user32.dll stdcall';
function SetTimer(
hWnd: longword; nIDEvent, uElapse: LongWord; lpTimerFunc: LongWord): LongWord;
external 'SetTimer#user32.dll stdcall';
function ScreenToClient(hWnd: HWND; var lpPoint: TPoint): BOOL;
external 'ScreenToClient#user32.dll stdcall';
function ClientToScreen(hWnd: HWND; var lpPoint: TPoint): BOOL;
external 'ClientToScreen#user32.dll stdcall';
function ListBox_GetItemRect(
const hWnd: HWND; const Msg: Integer; Index: LongInt; var Rect: TRect): LongInt;
external 'SendMessageW#user32.dll stdcall';
const
LB_GETITEMRECT = $0198;
LB_GETTOPINDEX = $018E;
function FindControl(Parent: TWinControl; P: TPoint): TControl;
var
Control: TControl;
WinControl: TWinControl;
I: Integer;
P2: TPoint;
begin
for I := 0 to Parent.ControlCount - 1 do
begin
Control := Parent.Controls[I];
if Control.Visible and
(Control.Left <= P.X) and (P.X < Control.Left + Control.Width) and
(Control.Top <= P.Y) and (P.Y < Control.Top + Control.Height) then
begin
if Control is TWinControl then
begin
P2 := P;
ClientToScreen(Parent.Handle, P2);
WinControl := TWinControl(Control);
ScreenToClient(WinControl.Handle, P2);
Result := FindControl(WinControl, P2);
if Result <> nil then Exit;
end;
Result := Control;
Exit;
end;
end;
Result := nil;
end;
function PointInRect(const Rect: TRect; const Point: TPoint): Boolean;
begin
Result :=
(Point.X >= Rect.Left) and (Point.X <= Rect.Right) and
(Point.Y >= Rect.Top) and (Point.Y <= Rect.Bottom);
end;
function ListBoxItemAtPos(ListBox: TCustomListBox; Pos: TPoint): Integer;
var
Count: Integer;
ItemRect: TRect;
begin
Result := SendMessage(ListBox.Handle, LB_GETTOPINDEX, 0, 0);
Count := ListBox.Items.Count;
while Result < Count do
begin
ListBox_GetItemRect(ListBox.Handle, LB_GETITEMRECT, Result, ItemRect);
if PointInRect(ItemRect, Pos) then Exit;
Inc(Result);
end;
Result := -1;
end;
procedure HoverComponentChanged(Index: Integer);
var
Description: string;
begin
case Index of
0: Description := 'This is the description of Main Files';
1: Description := 'This is the description of Additional Files';
2: Description := 'This is the description of Help Files';
else
Description := 'Move your mouse over a component to see its description.';
end;
CompLabel.Caption := Description;
end;
procedure HoverTimerProc(
H: LongWord; Msg: LongWord; IdEvent: LongWord; Time: LongWord);
var
P: TPoint;
Control: TControl;
Index: Integer;
begin
GetCursorPos(P);
if P <> LastMouse then { just optimization }
begin
LastMouse := P;
ScreenToClient(WizardForm.Handle, P);
if (P.X < 0) or (P.Y < 0) or
(P.X > WizardForm.ClientWidth) or (P.Y > WizardForm.ClientHeight) then
begin
Control := nil;
end
else
begin
Control := FindControl(WizardForm, P);
end;
Index := -1;
if (Control = WizardForm.ComponentsList) and
(not WizardForm.TypesCombo.DroppedDown) then
begin
P := LastMouse;
ScreenToClient(WizardForm.ComponentsList.Handle, P);
Index := ListBoxItemAtPos(WizardForm.ComponentsList, P);
end;
HoverComponentChanged(Index);
end;
end;
procedure InitializeWizard();
begin
SetTimer(0, 0, 50, CreateCallback(#HoverTimerProc));
CompLabel := TLabel.Create(WizardForm);
CompLabel.Parent := WizardForm.SelectComponentsPage;
CompLabel.Left := WizardForm.ComponentsList.Left;
CompLabel.Width := WizardForm.ComponentsList.Width;
CompLabel.Height := ScaleY(32);
CompLabel.Top :=
WizardForm.ComponentsList.Top + WizardForm.ComponentsList.Height -
CompLabel.Height;
CompLabel.AutoSize := False;
CompLabel.WordWrap := True;
WizardForm.ComponentsList.Height :=
WizardForm.ComponentsList.Height - CompLabel.Height - ScaleY(8);
end;
For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library. Use the Unicode version of Inno Setup 5.
To make this work correctly in resized/resizable/modern wizard, you will need some adjustments. See Inno Setup - how to center an animated gif in resized wizard and Long components descriptions in enlarged Inno Setup wizard.
Use this advanced compiler (download link is somewhere below).
It supports more classes and events than the standard compiler. You can access the property "OnItemMouseMove". Using this you can store a description for every item that is shown by a label. Here is an example:
var
CompLabel: TLabel;
procedure OnItemMouseMove(Sender: TObject; X, Y: Integer; Index: Integer; Area: TItemArea);
begin
case Index of
0: CompLabel.Caption := 'This is the description of Component 1';
1: CompLabel.Caption := 'This is the description of Component 2';
2: CompLabel.Caption := 'This is the description of Component 3';
3: CompLabel.Caption := 'This is the description of Component 4'
else
CompLabel.Caption := 'Move your mouse over a component to see its description.';
end;
end;
procedure OnMouseLeave(Sender: TObject);
begin
CompLabel.Caption := 'Move your mouse over a component to see its description.';
end;
procedure InitializeWizard();
begin
CompLabel := TLabel.Create(WizardForm);
CompLabel.Parent := WizardForm.SelectComponentsPage;
CompLabel.SetBounds(WizardForm.ComponentsList.Left,180,WizardForm.ComponentsList.Width,200);
CompLabel.Caption := 'Move your mouse over a component to see its description.';
WizardForm.ComponentsList.OnItemMouseMove := #OnItemMouseMove;
WizardForm.ComponentsList.OnMouseLeave := #OnMouseLeave;
WizardForm.ComponentsList.Height := WizardForm.ComponentsList.Height - 40;
end;