Inno Setup: Overwrite existing installation or show dir prompt - installation

It would be nice if I have this in my setup:
If there is no previous installation, then an edit field for the destination directory should be shown.
If there is a previous installation, than the user should should be asked if he wants to overwrite the existing installation (no directory
prompt should be visible) or if he wants to install this version in a
different directory as a separate installation (two entries in the uninstall list). If this option is chosen, than the edit field
for the destination directory should be shown.
Is it possible to use existing Inno Setup options to achieve this? Or do I have to build a custom dialog page?

At the beginning (InitializeSetup event function), check if the application is installed already (see GetUninstallString in the code below). If it is, ask user, what to do (see MsgBox use in the code and the first screenshot). If user chooses to update the existing installation, proceed normally. Inno Setup by default does not allow changing installation path of an existing installation (see DisableDirPage).
If uses chooses to install another copy, set AppId to a new unique value (GetAppId function in the code). This will make Inno Setup treat the installation as new, so it will prompt for the installation path. Update also UninstallDisplayName, so that the user can distinguish the installations when choosing which copy to uninstall (see GetAppIdentification and the third screenshot). Also update DefaultDirName to a new unique path (see GetAppIdentification and the third screenshot).
#define AppName "My Program"
#define AppVersion "1.5"
[Setup]
AppId={code:GetAppId}
AppName={#AppName}
AppVersion={#AppVersion}
UninstallDisplayName={#AppName} {#AppVersion}{code:GetAppIdentification}
UsePreviousLanguage=no # Needed when AppId is dynamic
DefaultDirName={autopf}\My Program{code:GetAppIdentification}
[Code]
var
Instance: string;
function GetAppId(Param: string): string;
begin
Result := '{#AppName}' + Instance;
end;
function GetAppIdentification(Param: string): string;
begin
if Instance <> '' then Result := ' (' + Instance + ')';
end;
function GetUninstallString(): string;
var
UninstallKey: string;
begin
UninstallKey :=
'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + GetAppId('') + '_is1';
RegQueryStringValue(HKA, UninstallKey, 'UninstallString', Result);
Log(Result)
end;
function InitializeSetup(): Boolean;
var
Message: string;
Answer: Integer;
begin
Result := True;
if GetUninstallString() = '' then
begin
Log('Application is not installed yed, installing the first copy');
end
else
begin
Log('Application is installed already, asking what to do');
Message :=
'This program is installed already, ' +
'do you want to update the existing installation? ' +
'Press No to install another copy of the program';
Answer := MsgBox(Message, mbConfirmation, MB_YESNOCANCEL);
if Answer = IDYES then
begin
Log('User chose to update the installation');
end
else
if Answer = IDNO then
begin
Log('User chose to install another copy');
Instance := '2';
end
else
begin
Log('User chose to abort the installation');
Result := False;
end;
end;
end;
Now the question is what to do if there are already two installations. To make a third (or more), it's easy, just loop, increasing the value in Instance, until GetUninstallString returns an empty string. But had you wanted the user to be able to choose what copy to update, it would be more difficult. That's too much for one question.
What you want to do is quite complicated. If you want to keep the flexibility, I think that the easiest solution is to treat every new version as a separate software. In addition, when starting the installation, as a courtesy to those who want to keep the latest version only, offer to uninstall the previous (latest) installation automatically. If the user already has multiple installations, do nothing specific (or just inform the user).

I solved this problem by putting the responsibility on the user (the one installing the application) to explicitly specify that they want to install a separate instance of the application by specifying a /instancename parameter on the installer's command line (the AppId directive uses a scripted constant).

Related

How do I save a picture from a open picture dialogue to a file

Hi I'm currently using Delphi 2010.
I basically have a form where a user has to enter information about themselves and upload a picture. I have an Image component on my form. I did some research and many of the websites I looked at said to use a OpenPictureDialogue to allow the user to select an image and display it in the Image component.
My question is, how can I save this image to a file on my computer? Keeping in mind I will have multiple users adding their picture and that I will have to use the picture later on again, basically I want to use the LoadFromFile procedure to display the picture in my program later on.
I also read many websites saying to use the SavePictureDialogue, but that allows the user to select the file they want the image to be saved to and I don't want that, I want it to save to a file that only I can access.
I have this so far, I know it is very limited.
if opdAcc.Execute then
begin
if opdAcc.FileName <> '' then
begin
imgAccImage.Picture.LoadFromFile(opdAcc.FileName);
end;
end;
I am a student and my knowledge is quite limited and I would appreciate any help. :)
First of all, there is no place on the hard drive that only you can access. But you can create a folder to store your files and copy users' pictures there. This reduces the likelihood that the user will have access to these files. The usual folder for storing such files is the AppData folder. It is better to create a folder with the same name as your application in AppData and store such files there.
Suppose the GetPicturesDirectoryPath function generates the address of such a folder and ensures that this folder has already been created or will be created. The next step is to generate a unique name for the file you want to store. Note that multiple users may select files with the same name. In this case, after copying the picture selected by the second user, the image file will be written over the previous user's file. If a unique identifier is assigned to each user, this identifier is the best choice for the picture file name. But you can use the GetGUIDFileName function to create a unique address. Make sure the generated address is kept with the rest of the user information, or the connection between the copied file and the user will be lost. The implementation of all these will be something like the following:
uses IOUtils;
function GetAppDataDirectoryPath: string;
begin
...
end;
function GetPicturesDirectoryPath: string;
begin
Result := TPath.Combine(GetAppDataDirectoryPath, 'MyApp');
TDirectory.CreateDirectory(Result);
end;
function GetUniqueFilePath(const Extension: string): string;
begin
Result := TPath.ChangeExtension(
TPath.Combine(GetPicturesDirectoryPath, TPath.GetGUIDFileName),
Extension);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
DestPath: string;
begin
OpenPictureDialog1.Options := OpenPictureDialog1.Options +
[ofFileMustExist]; // make sure that selected file exists
if OpenPictureDialog1.Execute then
begin
DestPath := GetUniqueFilePath(TPath.GetExtension(OpenPictureDialog1.FileName));
TFile.Copy(OpenPictureDialog1.FileName, DestPath);
if TFile.Exists(DestPath) then
Image1.Picture.LoadFromFile(DestPath)
else
ShowMessage('Well, something went wrong!');
end;
end;
Read this to implement GetAppDataDirectoryPath.

select more than one checkbox to execute condition

I want a text file to show contents in memo1 once I have selected 2 checkboxes.
How would I do this?
I tried the code below but I can't seem to get it right.
if CheckBox1.Checked and CheckBox2.Checked then
begin
memo1.lines.LoadFromFile('files\RS.txt');
end;
I also want to be able to select the checkboxes individually Like:
(pointing this out in case combining them prevents checking them individually)
Checkbox1:
procedure TForm1.CheckBox1Change(Sender: TObject);
begin
memo1.lines.LoadFromFile('files\R.txt');
end;
Checkbox2:
procedure TForm1.CheckBox2Change(Sender: TObject);
begin
memo1.lines.LoadFromFile('files\S.txt');
end;
Any suggestions/Improvements will be appreciated.
Running Lazarus IDE v1.6.4
Windows 10 x64
I'm assuming your objective is to generate a filename which depends on the
particular combination of the boolean states of the two checkboxes -
see example code below. The point of doing this is that it helps separate
the definition of what you want the file name to be from what you want to do
with it.
procedure TForm1.Button1Click(Sender: TObject);
begin
Memo1.Lines.LoadFromFile(GetFileName);
end;
function TForm1.GetFileName: String;
begin
// Return empty string if neither checkbox is checked
Result := '';
if Checkbox1.Checked and Checkbox2.Checked then
Result := 'files\RS.txt'
else // if we reach here only one of the checkboxes, or neither, is checked
if Checkbox1.Checked then
Result := 'files\R.txt'
else
if Checkbox2.Checked then
Result := 'files\S.txt'
end;
I've assigned an empty string to the Result of the function at the outset to ensure that the Result is always defined.
Important You'll notice that the above does not use the Change events of the Checkboxes. The reason is that you may not get the result you need (or are expecting) if the Change events are never triggered - for example if one CheckBox is set to Checked in the IDE but the other isn't, and you want to get the right file name regardless of whether the user has actually clicked either one of them.
As far as I understood you want the following behaviour:
There are two check boxes
There is one memo field
Depending on the state of the two check boxes the text in the memo field shall change
If this understanding is correct:
I typically don't use Pascal but your problem seems to be independent of the programming language used. I would do it like this:
The two procedures TForm1.CheckBox1Change and TForm1.CheckBox2Change are called whenever the corresponding check box'es state changes.
I would write a third procedure and call this third procedure from both procedures. I would do nothing else than calling this third procedure in the other two procedures.
In the third procedure I would evaluate what to do - depending on the state of both check boxes.
A separate checkboxchange proocedure per event is automatically generated by the designer if you double click the event. However that is not a rigid decision.
If you have the initial codefragment in e.g. checkbox1change, you can simply point the onchange of checkbox2 to that existing checkbox1change by using the dropdown of the onchange of checkbox2

SelectDirectory does not include drives on some machines

The following code gets different results on different machines. One machine just gives the desktop folder (not desired) the other gives the desktop folder and Computer, mapped drives (desired).
procedure TForm1.Button1Click(Sender: TObject);
var
Directory : String;
begin
FileCtrl.SelectDirectory('Caption', 'Desktop', Directory, [sdNewUI, sdShowEdit]);
end;
One one machine it gives:
On another it gives:
This feels like a windows setting, but I am not sure where to start. Using Delphi XE, Windows 10.
Any thoughts are appreciated. Thanks for your time.
Workaround
Use a TFileOpenDialog instead*.
Set FileOpenDialog1.Options:= [fdoPickFolders,fdoPathMustExist]
Now you have a dialog that:
Always works.
Allows copy paste
*) Not to be confused with the TOpenDialog, which does not allow you to only select folders.
Solution for Windows XP
Note that the new TFileOpenDialog only works for Vista and above.
Your program will not work on XP if you include this control.
If you start the dialog on XP it will generate an EPlatformVersionException.
You may want to use the following code instead if you want to be backward compatible:
uses JclSysInfo; //because you have XE use JCL.
...
var
WinMajorVer: Integer;
Directory: string;
FileDialog: TFileOpenDialog;
begin
WinMajorVer:= GetWindowsMajorVersionNumber;
if WinMajorVer < 6 then begin //pre-vista
//To show the root Desktop namespace, you should be setting the Root parameter to an empty string ('') instead of 'Desktop'
FileCtrl.SelectDirectory('Caption', '', Directory, [sdNewUI, sdShowEdit]);
end else begin
FileDialog:= TFileOpenDialog.Create(self);
try
FileDialog.Options:= [fdoPickFolders,fdoPathMustExist];
if FileDialog.Execute then Directory:= FileOpenDialog1.FileName;
finally
FileDialog.Free;
end;
end;
Result:= Directory;
end;
Recommended reading:
detect windows version
EDIT
FileCtrl.SelectDirectory('Caption', 'Desktop', Directory, [sdNewUI, sdShowEdit]);
The 'Desktop' goes into the Root parameter, which is handled like so:
...
SHGetDesktopFolder(IDesktopFolder);
IDesktopFolder.ParseDisplayName(Application.Handle, nil,
Root, Eaten, RootItemIDList, Flags);
...
Here's what MSDN for IDesktopFolder.ParseDisplayName has to say:
pszDisplayName [in]
Type: LPWSTR
A null-terminated Unicode string with the display name. Because each Shell folder defines its own parsing syntax, the form this string can take may vary. The desktop folder, for instance, accepts paths such as "C:\My Docs\My File.txt". It also will accept references to items in the namespace that have a GUID associated with them using the "::{GUID}" syntax.
Note that the documentation states that the desktop folder will accept paths and guids. It does not accept 'Desktop'. Because that's neither.
The fact that 'Desktop' as a root works on one system but not another is some undocumented fix made in an older/newer version of the IDesktopFolder interface.
Technical solution
Use '' as a 'root' as shown in my code above.
Obviously SelectDirectory is a really bad design by Microsoft that should never be used. It just sucks in so many ways. I recommend it not be used whenever possible.

How to set folder display name with e.g. "SHGetSetFolderCustomSettings()"?

Looks like SHGetSetFolderCustomSettings allows you to set an icon, a tooltip, a web view template and stuff, but I could not find how to set the LocalizedResourceName in the associated desktop.ini (see SHFOLDERCUSTOMSETTINGS structure).
Therefore I am currently writing to desktop.ini directly, however this comes with a caveat:
Explorer does not properly update its views even when you tell it to refresh with F5 or Ctrl+R.
This is what I want to write, using Python (though non-Python code should be less of an issue):
[.ShellClassInfo]
LocalizedResourceName=My Folder Name
InfoTip=A customized folder
Any ideas how to set the folder name and have Explorer properly update it ?
I have tried with SHChangeNotify(SHCNE_ALLEVENTS, SHCNF_PATH, path, path), but this does not seem to update the display name (and also with SHCNE_RENAMEFOLDER, SHCNE_RENAMEITEM, SHCNE_UPDATEDIR, SHCNE_UPDATEITEM).
(The worst approach would probably be to edit the desktop.ini twice... once directly, then with that API function... rather not what I want).
About the why (I guess at least one of you will ask):
I am storing project data using GUIDs as folder names.
The user should however see a friendly name that can also be used for sorting (and maybe even be able to edit it without interfering with the internal name).
Furthermore, the low-level file system layout should be backwards-compatible with older versions of the software.
Use simple call of IShellFolder.SetNameOf:
procedure UpdateLocalizedResourceName(const ADirectory, ANewResourceName: UnicodeString);
var
Desktop: IShellFolder;
Eaten: DWORD;
DirIDList1, Child, NewChild: PItemIDList;
Attr: DWORD;
Folder: IShellFolder;
begin
OleCheck(SHGetDesktopFolder(Desktop));
try
Attr := 0;
OleCheck(Desktop.ParseDisplayName(0, nil, PWideChar(ADirectory), Eaten, DirIDList1, Attr));
try
OleCheck(SHBindToParent(DirIDList1, IShellFolder, Pointer(Folder), Child));
try
OleCheck(Folder.SetNameOf(0, Child, PWideChar(ANewResourceName), SHGDN_INFOLDER, NewChild));
CoTaskMemFree(NewChild);
finally
Folder := nil;
end;
finally
CoTaskMemFree(DirIDList1);
end;
finally
Desktop := nil;
end;
end;
UPDATE
Important notice! LocalizedResourceName parameter must exists in desktop.ini before you call UpdateLocalizedResourceName. Otherwise SetNameOf function fails.

CopyFileEx and 8.3 file names

Suppose you have 2 files in the same directory:
New File Name.txt and
NewFil~1.txt
If you use CopyFileEx to copy both files to the same destination, maintaining the same names, you will end up having only ONE file (the second one replaces the first one) which can be sometimes not a good thing. Any workaround for this behavior?
This happens at file system level so you there is no much you can do if you don't want to disable SFN generation at all.
The way I use to handle this problem is to:
1) Before the file is copied, I check if the file name exists.
2) If there is a collision then I first rename the existing file top some temporary name
3) Then I copy the file
4) rename the first file back.
To detect the collision do something like this:
function IsCollition(const Source, Destination: string; var ExistingName: string): boolean;
var
DesFD: TSearchRec;
Found: boolean;
ShortSource, FoundName: string;
begin
ShortSource:= ExtractFileName(SourceName);
Found:= false;
FoundName:= WS_NIL;
if (FindFirst(DestinationName, faAnyFile, DesFD) = 0) then
begin
Found:= true;
FoundName:= DesFD.Name;
SysUtils.FindClose(DesFD);
end;
if (not Found) or (SameFileName(ShortSource, FoundName)) then
begin
Result:= false;
end else
begin
// There iis a collision:
// A file exists AND it's long name is not equal to the original name
Result:= true;
end;
ExistingName:= FoundName;
end;
There's not a great solution for the automatic generation of short filename aliases. If your application will have adequate privileges, you might be able to use the SetFileShortName() API. Another (heavy handed) alternative might be to disable short name alias generation altogether, though I'd be reluctant to require this of your users. See
http://support.microsoft.com/kb/210638/EN-US/
http://technet.microsoft.com/en-us/library/cc778996.aspx
http://blogs.msdn.com/adioltean/archive/2005/01/27/362105.aspx
for more details.

Resources