2019年7月10日水曜日

キャプチャソフト開発 その1 フォルダの選択ダイアログを表示する


概要

フォルダ選択ダイアログなんで簡単だろうと思ったら、意外と面倒くさかった。

「ファイル」選択ダイアログには、それ専用の一つの関数がある。
GetOpenFileNameだ。

しかし、Windows APIにはフォルダ選択専用の一つの関数が存在しないのだ。
ファイルの選択のように、一つの関数で呼び出せれば便利なのだが...

そこで、自前で関数を実装し、テストで動くことを確認した。
関数の名前はGetOpenFileNameにならい「GetOpenDirName」とした。

主に以下のページを参考とした。

細かい仕組みとか、フォルダの扱い方などの解説はこのページではしない。
作った関数の使い方、サンプルの動作、関数の改造方法などについて述べようと思う。

ソースコード


GetOpenDirNameのソースファイル
  1. #include "./util.h"
  2. #include <assert.h>
  3. #include <shlobj.h>
  4. #include <wchar.h>
  5. #include <windows.h>
  6. static int CALLBACK BrowseCallbackProc(HWND hWnd, UINT uMsg, LPARAM lParam,
  7. LPARAM lpData) {
  8. (void)hWnd;
  9. (void)lpData;
  10. wchar_t dir[MAX_PATH] = {0};
  11. ITEMIDLIST *pidl = NULL;
  12. switch (uMsg) {
  13. case BFFM_INITIALIZED:
  14. // The dialog box has finished initializing.
  15. break;
  16. case BFFM_IUNKNOWN:
  17. // An IUnknown interface is available to the dialog box.
  18. break;
  19. case BFFM_VALIDATEFAILED:
  20. // The user typed an invalid name into the dialog's edit box.
  21. // A nonexistent folder is considered an invalid name.
  22. MessageBox(hWnd, L"Invalid folder name!", L"Error", MB_OK);
  23. return 1;
  24. case BFFM_SELCHANGED:
  25. // The selection has changed in the dialog box.
  26. #ifdef DEBUG
  27. pidl = (ITEMIDLIST *)lParam;
  28. SHGetPathFromIDList(pidl, dir);
  29. fwprintf(stderr, L"Select changed:%ls\n", dir);
  30. fwprintf(stderr, L"\n");
  31. #endif
  32. break;
  33. }
  34. return 0;
  35. }
  36. bool GetDirectoryName(HWND hwnd, const wchar_t *title, const wchar_t *root_dir,
  37. wchar_t *selected_dir) {
  38. assert(title);
  39. assert(selected_dir);
  40. PIDLIST_ABSOLUTE pidlRoot = NULL;
  41. // When root directory path is specified, the input path is converted to the
  42. // id list.
  43. if (root_dir != NULL) {
  44. // Root directory to item ID list.
  45. HRESULT hr = SHParseDisplayName((PCWSTR)root_dir, NULL, &pidlRoot, 0, NULL);
  46. if ((hr != S_OK) || (pidlRoot == NULL)) {
  47. #ifdef DEBUG
  48. fwprintf(stderr, L"Invalid root directory\n");
  49. #endif
  50. return false;
  51. }
  52. }
  53. // Set information about the folder dialog.
  54. BROWSEINFO bi;
  55. memset(&bi, 0, sizeof(BROWSEINFO));
  56. bi.hwndOwner = hwnd;
  57. bi.pidlRoot = pidlRoot;
  58. bi.pszDisplayName = (LPWSTR)selected_dir;
  59. bi.lpszTitle = title;
  60. bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_VALIDATE | BIF_USENEWUI;
  61. bi.lpfn = BrowseCallbackProc;
  62. bi.lParam = (LPARAM)root_dir;
  63. // Displays a dialog box that enables you to select a shell folder.
  64. LPITEMIDLIST pIDList = SHBrowseForFolder(&bi);
  65. if (pIDList == NULL) {
  66. selected_dir[0] = L'\0';
  67. return false;
  68. }
  69. // Converts acquired item ID list to a file system path.
  70. if (!SHGetPathFromIDList(pIDList, selected_dir)) {
  71. CoTaskMemFree(pIDList);
  72. return false;
  73. }
  74. // Free the ID list allocated by SHBrowseForFolder.
  75. CoTaskMemFree(pIDList);
  76. return true;
  77. }


  • GetDirectoryName
    今回実装した、フォルダ選択ダイアログを呼び出す専用の関数である。
    GetOpenFileNameと同じ感覚で使用できる。

    第一引数 hwnd は呼び出し元のウィンドウのウィンドウハンドルを入れる。
    第二引数 title は表示するダイアログのタイトル文字列を入れる。
    第三引数 root_dir はルートのパスを入れる(NULLならデスクトップ)。
    第四引数 selected_dir は、最終的に選択されたフォルダのフルパスを入れるバッファを指定する。

    root_dirをNULLにした場合。


    root_dirを指定した場合。
  • SHGetPathFromIDList
    この関数を呼ぶことで、ファイル選択ダイアログを呼ぶことができる。
    引数としてBROWSEINFO構造体を渡す必要がある。
  • BROWSEINFO
    ダイアログの細かい動作やコールバック関数、結果の格納先のバッファなど、いろいろ指定するための構造体。

    ちょっとハマったのは、ulFlagsメンバの設定。どうもフラグの組み合わせにより、zipファイルまで表示&選択できてしまう問題があるらしい。
    SHBrowseForFolderでzipファイルも表示されてしまう

    このソースコードの組み合わせでは不具合は出ていないが(正しくは、不具合が出ないような組み合わせを探したのだが)、書きかえる際には注意が必要だろう。
  • BrowseCallbackProc
    フォルダ選択ダイアログのメッセージプロシージャである。
    エラー処理などの細かい挙動を書きかえたい場合は、この関数をいじる。

    本実装のTODOとして、ダブルクオーテーションの問題がある。
    ダブルクオーテーションで囲われたフォルダのパスをエディットボックスに入れる場合などは、ダブルクオーテーションを取り外す処理の実装が必要になる。
    そういった例外的なケースに対する処置も、ここに記述することができるだろう。

GetOpenDirNameのヘッダファイル

  1. #ifndef _UTIL_H_
  2. #define _UTIL_H_
  3.  
  4. #include <wchar.h>
  5. #include <windows.h>
  6. bool GetDirectoryName(HWND hwnd, const wchar_t *title, const wchar_t *root_dir,
  7. wchar_t *selected_dir);
  8. #endif // _UTIL_H_

この関数一つだけなので寂しいが、ファイル分けしておくといろいろ便利。

サンプルのメインソースファイル
  1. #include <shlobj.h>
  2. #include <stdio.h>
  3. #include <wchar.h>
  4. #include <windows.h>
  5. #include <windowsx.h>
  6. #include "./resource.h"
  7. #include "./util.h"
  8. namespace {
  9. constexpr wchar_t WINDOW_NAME[] = L"Template window";
  10. constexpr wchar_t CLASS_NAME[] = L"Template class";
  11. } // namespace
  12. BOOL Cls_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) {
  13. (void)hWnd;
  14. (void)lpCreateStruct;
  15. return TRUE;
  16. }
  17. void Cls_OnDestroy(HWND hWnd) {
  18. (void)hWnd;
  19. PostQuitMessage(0);
  20. }
  21. void Cls_OnClose(HWND hWnd) { DestroyWindow(hWnd); }
  22. void Cls_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
  23. (void)hWnd;
  24. (void)id;
  25. (void)hWndCtl;
  26. (void)codeNotify;
  27. }
  28. void Cls_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y,
  29. UINT keyFlags) {
  30. (void)hwnd;
  31. (void)fDoubleClick;
  32. (void)x;
  33. (void)y;
  34. (void)keyFlags;
  35. // Show folder dialog with default root path.
  36. wchar_t selected_dir[MAX_PATH] = {0};
  37. if (!GetDirectoryName(hwnd, L"Desktop", NULL, selected_dir)) {
  38. #ifdef DEBUG
  39. fwprintf(stderr, L"Error on folder select\n");
  40. #endif
  41. }
  42. #ifdef DEBUG
  43. fwprintf(stderr, L"WM_LBUTTONDOWN\n");
  44. fwprintf(stderr, L"%ls\n", selected_dir);
  45. fwprintf(stderr, L"\n");
  46. #endif
  47. }
  48. void Cls_OnRButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y,
  49. UINT keyFlags) {
  50. (void)hwnd;
  51. (void)fDoubleClick;
  52. (void)x;
  53. (void)y;
  54. (void)keyFlags;
  55. // Show folder dialog with current directory.
  56. wchar_t root_dir[MAX_PATH] = {0};
  57. GetCurrentDirectory(MAX_PATH, root_dir);
  58. wchar_t selected_dir[MAX_PATH] = {0};
  59. if (!GetDirectoryName(hwnd, root_dir, root_dir, selected_dir)) {
  60. #ifdef DEBUG
  61. fwprintf(stderr, L"Error on folder select\n");
  62. #endif
  63. }
  64. #ifdef DEBUG
  65. fwprintf(stderr, L"WM_RBUTTONDOWN\n");
  66. fwprintf(stderr, L"%ls\n", selected_dir);
  67. fwprintf(stderr, L"\n");
  68. #endif
  69. }
  70. LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
  71. LPARAM lParam) {
  72. switch (message) {
  73. HANDLE_MSG(hWnd, WM_CREATE, Cls_OnCreate);
  74. HANDLE_MSG(hWnd, WM_DESTROY, Cls_OnDestroy);
  75. HANDLE_MSG(hWnd, WM_COMMAND, Cls_OnCommand);
  76. HANDLE_MSG(hWnd, WM_CLOSE, Cls_OnClose);
  77. HANDLE_MSG(hWnd, WM_LBUTTONDOWN, Cls_OnLButtonDown);
  78. HANDLE_MSG(hWnd, WM_RBUTTONDOWN, Cls_OnRButtonDown);
  79. default:
  80. return DefWindowProc(hWnd, message, wParam, lParam);
  81. }
  82. }
  83. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  84. LPTSTR lpsCmdLine, int nCmdShow) {
  85. (void)hPrevInstance;
  86. (void)lpsCmdLine;
  87. // Initialization of COM.
  88. CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  89. #ifdef DEBUG
  90. FILE* fp = nullptr;
  91. AllocConsole();
  92. _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  93. _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  94. _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
  95. #endif
  96. #ifdef DEBUG
  97. fwprintf(stdout, L"Hello world to stdout!\n");
  98. fwprintf(stderr, L"Hello world to stderr!\n");
  99. #endif
  100. WNDCLASS wc;
  101. wc.style = CS_HREDRAW | CS_VREDRAW;
  102. wc.lpfnWndProc = WndProc;
  103. wc.cbClsExtra = 0;
  104. wc.cbWndExtra = 0;
  105. wc.hInstance = hInstance;
  106. wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  107. wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  108. wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  109. wc.lpszMenuName = 0;
  110. wc.lpszClassName = CLASS_NAME;
  111. if (!RegisterClass(&wc)) {
  112. return FALSE;
  113. }
  114. HWND hWnd = CreateWindow(CLASS_NAME, WINDOW_NAME, WS_OVERLAPPEDWINDOW,
  115. CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
  116. CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
  117. if (hWnd == NULL) {
  118. return FALSE;
  119. }
  120. ShowWindow(hWnd, nCmdShow);
  121. UpdateWindow(hWnd);
  122. MSG msg;
  123. while (GetMessage(&msg, NULL, 0, 0) > 0) {
  124. TranslateMessage(&msg);
  125. DispatchMessage(&msg);
  126. }
  127. #ifdef DEBUG
  128. FreeConsole();
  129. #endif
  130. CoUninitialize();
  131. return 0;
  132. }


ポイント

  • CoInitializeExCoUninitialize
    フォルダ選択ダイアログはCOMの機能を使用する。
    COMの初期化と終了を忘れずに。
  • Cls_OnLButtonDown
    WM_LBUTTONDOWNで呼ばれる関数。
    左クリックでは、デスクトップをルートとするファイル選択ダイアログを表示する。
  • Cls_OnRButtonDown
    WM_RBUTTONDOWNで呼ばれる関数。
    右クリックでは、実行可能ファイルの存在するディレクトリ(カレントディレクトリ)をルートとするファイル選択ダイアログを表示する。


メイクファイル
  1. TARGET = main.exe
  2. PDB = main.pdb
  3. MAP = main.map
  4. RES = resource.res
  5. SRC = main.cc util.cc
  6. TMPDIR = build
  7. OBJ = $(TMPDIR)\main.obj $(TMPDIR)\util.obj
  8.  
  9. CC = "$(VCINSTALLDIR)\cl.exe"
  10. LINK = "$(VCINSTALLDIR)\link.exe"
  11.  
  12. CPPFLAGS = /nologo /W4 /Zi /O2 /MT /EHsc /Fd"$(TMPDIR)/" /DUNICODE /D_UNICODE \
  13. /DDEBUG
  14. LFLAGS = "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" \
  15. "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" \
  16. "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" "libcmt.lib" \
  17. /NOLOGO /SUBSYSTEM:WINDOWS /DEBUG
  18.  
  19. ALL: $(TARGET)
  20.  
  21. $(TARGET): $(OBJ) $(RES)
  22. $(LINK) $(LFLAGS) /OUT:$(TARGET) /PDB:"$(PDB)" /MAP:"$(MAP)" \
  23. $(OBJ) $(RES)
  24.  
  25. .cc{$(TMPDIR)}.obj:
  26. @[ -d $(TMPDIR) ] || mkdir $(TMPDIR)
  27. $(CC) $(CPPFLAGS) /Fo"$(TMPDIR)\\" /c $<
  28.  
  29. clean:
  30. rm $(TARGET) $(OBJ) *.map *.pdb *.ilk *.obj *.lib


テンプレートのSRCとOBJをいじっただけ。
複数ファイルのプロジェクトにも、このメイクファイルは対応できるのだ。

備考

アイコンとリソースファイルには変更がない。
テンプレートから引っ張ってくるか、自分で用意してほしい。

このサンプル一式については、GitHubにも上げておく




0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています