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.
Related
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.
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).
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.
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.
My program exits with RunError(5), which would suggest that it can't access the file, which it should be able to. I have checked and the file is used as it should be, the file isn't read-only, etc. What the program does is, it creates a .dat file if one doesn't exists and uses it for saving stuff. If I run the program and the file doesn't exist, the file is created, but after that, in the same execution, the program won't access the file. This ONLY happens if the file was created in the current execution.
This is the way in which the procedures are called(the code is quite long but I am giving you the first few lines, where the error occurs):
fileName := 'labSave.dat';
CreateFile;
assign(labyrinthFile,fileName);
writeln(CheckFileSize);
and then there is each of the procedures:
procedure Initialize;
begin
fileName := 'labSave.dat';
assign(labyrinthFile,fileName);
end;
procedure CreateFile;
begin
if not FileExists(fileName) then FileCreate(fileName);
end;
function CheckFileSize: integer;
begin
reset(labyrinthFile);
CheckFileSize := FileSize(labyrinthFile);
close(labyrinthFile);
end;
According to Lazarus forum (http://forum.lazarus.freepascal.org/index.php?topic=4936.0):
Runtime Error 5 means Access denied. The file maybe readonly and you
use the wrong (default) filemode, or you try to re-open the file with
a new filehandle without having closed it before (somewhere in the
while and repeat loops possibly you assignfile more then once, then
the reset fails?).
If I recall correctly now, the workflow should be as follows for create:
AssignFile(f, filename); Rewrite(f); CloseFile(f);
and for existing file:
AssignFile(f, filename); Reset(f); CloseFile(f);
Seeing other mistakes found in your code through questions in comments, I strongly suggest you to devote more time to debugging and when such errors happen - strip out ALL of the irrelevant code and check your code design for cases like above (assigning file before creating it, etc.).