Your application allows your users to create and save files. You help your users by remembering where the last file was stored they don’t have to navigate their entire folder structure every time they want to save a file.
procedure TMainForm.SaveButtonClick(Sender: TObject); begin SaveDialog1.InitialDir := UserSettings.LastUsedFolder; SaveDialog1.Execute(); UserSettings.LastUsedFolder := ExcludeTrailingPathDelimiter(ExtractFilePath(SaveDialog1.FileName)); end;
So far so good.
First time experience
To make you application even better, you want to improve the experience of saving something for the very first time after installation. Right now, as the value for the “last save location” is still an empty string, the save dialog takes you straight to the user’s “Documents” library. [1, 2, 3]
While not bad, what you really want is to send your users straight to the “Your Beautiful App” folder in their “My Documents” folder.
You have already changed your save code to look like:
procedure TMainForm.SaveButtonClick(Sender: TObject); begin if UserSettings.LastUsedFolder = '' then SaveDialog1.InitialDir := IncludeTrailingPathDelimiter({PathToMyDocuments}'') + 'Your Beautiful App' else SaveDialog1.InitialDir := UserSettings.LastUsedFolder; if SaveDialog1.Execute() then UserSettings.LastUsedFolder := ExcludeTrailingPathDelimiter(ExtractFilePath(SaveDialog1.FileName)); end;
All you need now is a way to find the correct path for someone’s “My Documents” folder.
So where is this thing?
Trouble ahead?
Well… that happens to depend on a the Windows version and localization that your application is running under. Different Windows versions have put the user’s “My Documents” folder in different locations. It has travelled from C:\Documents and Settings\Marjan\My Documents
(XP and earlier) to C:\Users\Marjan\My Documents
(Vista and up). [4]
Then there is localization. What you know as the My Documents
folder is the Mijn documenten
folder on my Dutch Windows.
And roaming profiles for network users where the entire user profile isn’t even on the C:
drive, but somewhere on their network.
Nah…
Luckily, you don’t need to worry about all this. Windows has it all covered for you. All you need to do is ask Windows for the path you need. Depending on the Delphi version you use, that is a breeze or a slightly stiffer wind.
Delphi XE5 and up
If use Delphi XE5 or later, you are home free. Getting the location of the user’s My Documents
folder is a simple matter of calling the GetDocumentsPath
method of the TPath
record in System.IOUtils
:
Memo1.Lines.Add('Home path (%USERPROFILE%): ' + TPath.GetHomePath); Memo1.Lines.Add('Documents (My Documents): ' + TPath.GetDocumentsPath); Memo1.Lines.Add('Pictures (My Pictures): ' + TPath.GetPicturesPath); Memo1.Lines.Add('Music (My Music): ' + TPath.GetMusicPath); Memo1.Lines.Add('Movies (My Videos): ' + TPath.GetMoviesPath); Memo1.Lines.Add('Temporary files (TEMP): ' + TPath.GetTempPath);
The TPath
record in XE5 and up has support for a whole slew of platform agnostic folders such as the “Home”, “Pictures”, “Music”, and “Temp” folders. On the Windows platform these are conceptually known as Known Folders. Quite nice stuff. You can even add custom folders to Windows’ default set.
The platform agnostic folders that TPath supports are documented on
Standard RTL Path Functions across the Supported Target Platforms
Pre XE5
If you are using an earlier Delphi version, then you will have to call the Windows API functions yourself.
On Windows 7 and up you can use SHGetKnownFolderPath. On earlier Windows versions you will need to use its predecessor SHGetFolderPath or SHGetSpecialFolderPath
But … SHGetFolderPath is marked deprecated on MSDN. And SHGetSpecialFolderPath is marked “not supported. Instead, use SHGetFolderPath”.
Eeeks. Now what?
Hang on to your hat. Deprecated is not the same as “gone”.
In fact, Delphi (XE6 in this case) doesn’t yet include an external function declaration for SHGetKnownFolderPath
. It only declares SHGetFolderPath
in the Winapi.SHFolder
unit. And indeed the TPath methods in XE5+ use that function as well..
What’s more Delphi 2009 doesn’t even have the SHGetFolderPath
declaration yet. It only has a declaration for SHGetSpecialFolderPath
. And that still works when used on Windows 7. [5]
If all your customers are on Windows 7 and up, you could go with SHGetKnownFolderPath
and ignore the older functions. If however some of your users are still on Vista, then you will have to use SHGetFolderPath
(as well) to support them.
I have opted to just go with SHGetFolderPath
as newer Windows versions still support it and it is preferred over SHGetSpecialFolderPath
in the MSDN documentation.
If you are on a Delphi version that does not provide an external declaration for SHGetFolderPath
it is easy enough for you to add it yourself. You need a declaration
function SHGetFolderPath(hwnd: HWND; csidl: Integer; hToken: THandle; dwFlags: DWord; pszPath: LPWSTR): HRESULT; stdcall;
and an “implementation”:
function SHGetFolderPath; external 'SHFolder.dll' name 'SHGetFolderPathW';
Instead of just adding these to some “utils” unit and calling them directly where I need to get a folder location, I prefer to isolate my code from API’s and third party libraries. So I have created a “helper” unit with a record to hide the nitty gritty of calling the Windows API. You could, of course, also opt for a “static” class (so you don’t have to instantiate it).
Benefits
- When Microsoft does decide to remove the
SHGetFolderPath
API, you only need to change a single unit. All your other code can remain unchanged. - You create code that is better unit testable.
As unit tests should be independent of outside influences, you would also have to use some mock, stub or shim to “fake” the dll that provides the API.
unit FolderHelper; interface type RFolderHelper = record strict private class function GetFolder(const aCSIDL: Integer): string; static; public class function GetMyDocumentsFolder: string; static; class function GetMyMusicFolder: string; static; // ... any other folders in which you are interested end; implementation uses ShlObj, // Needed for the CSIDL constants Windows; function SHGetFolderPath(hwnd: HWND; csidl: Integer; hToken: THandle; dwFlags: DWord; pszPath: LPWSTR): HRESULT; stdcall; forward; function SHGetFolderPath; external 'SHFolder.dll' name 'SHGetFolderPathW'; class function RFolderHelper.GetFolder(const aCSIDL: Integer): string; var FolderPath: array[0 .. MAX_PATH] of Char; begin SetLastError(ERROR_SUCCESS); if SHGetFolderPath(0, aCSIDL, 0, 0, @FolderPath) = S_OK then Result := FolderPath; end; class function RFolderHelper.GetMyDocumentsFolder: string; begin Result := GetFolder(CSIDL_PERSONAL); end; class function RFolderHelper.GetMyMusicFolder: string; begin Result := GetFolder(CSIDL_MYMUSIC); end; // ... similar implementations for the other folders end.
Just as with classes the fact that the GetMyDocumentsFolder
is declared as a class
method means you can call it without an instance variable:
SaveDialog1.InitialDir := IncludeTrailingPathDelimiter(RFolderHelper.GetMyDocumentsFolder) + 'Your Beautiful App'
Enjoy!
Notes on the FolderHelper
unit
- A convention my colleagues and I adopted some time ago with regard to record declarations is to prefix their names with an “R” rather than “T” as is the custom in Delphi. I find it very convenient that the type name makes it obvious that it is a record not a class. It means I don’t have to Ctrl-Click through to the declaration of that type to check whether or not the lack of calls to construct or free it is as it should be.
class
methods in records require that you mark them withstatic
as well. I have no clue as to why. The compiler simply demands it. Funny quirk that.
I have always found thatclass
methods in Delphi should not have been named “class methods” as it introduces a lot of confusion when talking about the methods of classes.
It would have been so nice if they had been named “static methods” just like other languages have done.
Unfortunately, in Delphi,static
was used to name the default binding method where the declared (compile-time) type of the class or object variable determines which implementation to activate. In other words, in Delphi, all methods that are not marked asvirtual
ordynamic
are understood to bestatic
.
So why do class methods in a record need to be explicitly declared asstatic
? It’s the default after all? And last time I checked records do not support inheritance, sovirtual
anddynamic
binding is not even in the picture for them?-
I follow the “always initialize your variables” rule.
May seem strange then that theGetFolder
method is lacking aResult := '';
statement.
But it isn’t. Not really.
The compiler does it.
The result is of type string.
A string is a managed type and managed types are always initialized by the compiler.
Notes
[1] This is the default on Windows 7. On earlier Windows versions this may have been different. Though I don’t know when the desktop became the default, IIRC it once defaulted to the current dir, which of course is the location of your application’s executable unless you start that through a short cut with a different “start in” location.
[2] When you run with debugging, opening the save dialog for the first time is incredibly slow. Probably because of the debug hooks and the number of dll’s being loaded to support the save dialog. The second time you open the dialog is is a lot faster than the first time, but to all intents and purposes it is still slow.
[3] When you run with debugging, the first time you open the save dialog it goes straight to the … Desktop ?! wtf? Interestingly, when you cancel the dialog and reopen it, it does go to the user’s “Documents” library… ?! But with a non blank value in InitialDir it keeps going to the desktop regardless of how many times it has been opened?!
[4] Even though Windows reports my “My Documents” folder as C:\Users\Marjan\Documents
, it actually lives on my D:
drive… 🙂 But Windows handles that in a completely transparent manner for all applications so you don’t need to worry about it.
[5] Interestingly, the ShlObj unit in D2009 does have a declaration for SHGetFolderLocation
(which gets the location of a folder relative to the desktop) and mentions SHGetFolderPath in a comment.
“class methods in records require that you mark them with static as well. I have no clue as to why.”
Because there is nothing that could get passed as Self. For a class method on a class that is not static the TClass gets passed as Self. But for records there is nothing like that.
“It would have been so nice if they had been named “static methods” just like other languages have done.”
No because a static method in other languages is not the same as a class method in Delphi which can be virtual because of the concept of class references (TClass). In fact a static method is a class method with the static keyword. Because then it does not get the Self parameter with the class reference.
And it also makes a difference if you have class procedure TFoo.Something; and then call TBar.Something (TBar inherits from TFoo) because then the Self argument is different than in a TFoo.Something call.
Thanks!
Saved me a lot of time. Now I am using Delphi 6 (ya very old school) and I had to change:
FolderPath: array[0 .. MAX_PATH] of Char;
to
FolderPath: array[0 .. MAX_PATH] of WideChar;
plus the strings to WideString
Glad to hear it was helpful.
Delphi’s IDE never was much without many 3rd party tools (at least until/when I stopped using Delphi), but Delphi 6… wow… I feel for you.
Hello There. I found your blog using msn. This is an extremely well written article. I will be sure to bookmark it and come back to read more of your useful information. Thanks for the post. I will definitely return.|