CSIDLではなく KNOWNFOLDERIDをつかう。GetSpecialFolderPathではなくGetKnownFolderPathをつかう

久しぶりにWindows規定のフォルダーを調べる必要があって、CSIDLの何だっけと調べてみた。

MicrosoftCSIDLに関するページをみると、最初に以下のように書かれている。

注意
Vista Windows、これらの値は KNOWNFOLDERID 値に置き換えられています。 新しい定数とそれに対応する CSIDL 値の一覧については、このトピックを参照してください。 便宜上、CSIDL 値ごとに対応する KNOWNFOLDERID 値もここに示されています。

CSIDL システムは、互換性上の理由Windows Vista でサポートされています。 ただし、新しい開発では 、CSIDL 値ではなく KNOWNFOLDERID 値を使用する必要があります。

少し日本語が怪しいがKNOWNFOLDERIDを使うようにしましょうとのことだ。

フォルダーを調べる関数SHGetSpecialFolderPathのページではトップに以下のように書かれている。

[SHGetSpecialFolderPath is not supported. Instead, use SHGetFolderPath.] 

SHGetSpecialFolderPathはサポートされていないのでSHGetFolderPathを使用してくださいとのこと。

じゃあ、SHGetFolderPathのページはというと、同じくトップに以下のように書かれている。

Note  As of Windows Vista, this function is merely a wrapper for SHGetKnownFolderPath. The CSIDL value is translated to its associated KNOWNFOLDERID and then SHGetKnownFolderPath is called. New applications should use the known folder system rather than the older CSIDL system, which is supported only for backward compatibility.

要は、SHGetFolderPathはSHGetKnownFolderPathのラッパーで、CSIDL値はKNOWNFOLDERIDに内部で変換して渡しているよとのこと。

Windows Vista以降であるならば、CSIDL値を使わずKNOWNFOLDERIDを使い、パスを得るにはSHGetSpecialFolderPathを使わずSHGetKnownFolderPathを使えと言うことらしい。
Windows11はテストをしていないので分からないが、現時点で手元のWindows10ではCSIDL+GetSpecialFolderPathの組み合わせでも期待通りの結果を得ることができる。

ざっくりとコードで比較してみる。なお下記のコードをコンパイルするにはどちらもshlobj.hをインクルードする必要がある。
まずCSIDL+SHGetSpecialFolderPathでのパターン

  wchar_t wPath[MAX_PATH];
  HRESULT hr = ::SHGetSpecialFolderPath( NULL, wPath, CSIDL_DESKTOP, FALSE );
  if( SUCCEEDED( hr ) )
  {
    // wPathを使って作業
  }

次にKNOWNFOLDERID+SHGetKnownFolderPathでのパターン

  wchar_t* pwPath;
  HRESULT hr = ::SHGetKnownFolderPath( FOLDERID_Desktop, 0, NULL, &pwPath );
  if( SUCCEEDED( hr ) )
  {
    // wPathを使って作業
  }
  ::CoTaskMemFree( pwPath );

SHGetKnownFolderPathの最初の引数は、REFKNOWNFOLDERID型となっておりKNOWNFOLDERIDのポインターを渡すようになっているが、定義済みのKNOWNFOLDERID定数はそのまま書ける(つまりポインターになっている)。

CSIDL+SHGetSpecialFolderPathでは、パスの配列はこちらで用意する必要がある。まずないとは思うがMAX_PATH以上のパス文字列の場合は失敗するか、メモリー領域の破壊が発生する。
一方、KNOWNFOLDERID+SHGetKnownFolderPathでは、パスの配列はSHGetKnownFolderPath側で用意されポインターを返す。
このため、使用後には必ず廃棄しなければならない(CoTaskMemFree関数)。

KNOWNFOLDERID+SHGetKnownFolderPathはVista以降でなければ動作しないが、これからのプログラムでXP以前をサポートすることもないだろうし、素直にKNOWNFOLDERID+SHGetKnownFolderPathでコーディングした方が良さそうだ。

2022-05-28 追記

リンクエラーで未解決の外部シンボルと言われる場合は、以下を追加すること。

#pragma comment(lib, "Shell32")