How to get the location of the user’s “My Documents” folder

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 GetDocumentsPathmethod 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 with static as well. I have no clue as to why. The compiler simply demands it. Funny quirk that.
    I have always found that class 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 as virtual or dynamic are understood to be static.
    So why do class methods in a record need to be explicitly declared as static? It’s the default after all? And last time I checked records do not support inheritance, so virtual and dynamic binding is not even in the picture for them?

  • I follow the “always initialize your variables” rule.
    May seem strange then that the GetFolder method is lacking a Result := ''; 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.

Posted in Software Development
Tags: , , , , , , , , ,
One comment on “How to get the location of the user’s “My Documents” folder
  1. “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.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Show Buttons
Hide Buttons