2019年7月10日水曜日

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


概要

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

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

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

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

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

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

ソースコード


GetOpenDirNameのソースファイル
#include "./util.h"
#include <assert.h>
#include <shlobj.h>
#include <wchar.h>
#include <windows.h>

static int CALLBACK BrowseCallbackProc(HWND hWnd, UINT uMsg, LPARAM lParam,
                                       LPARAM lpData) {
  (void)hWnd;
  (void)lpData;

  wchar_t dir[MAX_PATH] = {0};
  ITEMIDLIST *pidl = NULL;

  switch (uMsg) {
    case BFFM_INITIALIZED:
      // The dialog box has finished initializing.
      break;
    case BFFM_IUNKNOWN:
      // An IUnknown interface is available to the dialog box.
      break;
    case BFFM_VALIDATEFAILED:
      // The user typed an invalid name into the dialog's edit box.
      // A nonexistent folder is considered an invalid name.
      MessageBox(hWnd, L"Invalid folder name!", L"Error", MB_OK);
      return 1;
    case BFFM_SELCHANGED:
      // The selection has changed in the dialog box.
#ifdef DEBUG
      pidl = (ITEMIDLIST *)lParam;
      SHGetPathFromIDList(pidl, dir);
      fwprintf(stderr, L"Select changed:%ls\n", dir);
      fwprintf(stderr, L"\n");
#endif
      break;
  }
  return 0;
}

bool GetDirectoryName(HWND hwnd, const wchar_t *title, const wchar_t *root_dir,
                      wchar_t *selected_dir) {
  assert(title);
  assert(selected_dir);

  PIDLIST_ABSOLUTE pidlRoot = NULL;

  // When root directory path is specified, the input path is converted to the
  // id list.
  if (root_dir != NULL) {
    // Root directory to item ID list.
    HRESULT hr = SHParseDisplayName((PCWSTR)root_dir, NULL, &pidlRoot, 0, NULL);
    if ((hr != S_OK) || (pidlRoot == NULL)) {
#ifdef DEBUG
      fwprintf(stderr, L"Invalid root directory\n");
#endif
      return false;
    }
  }

  // Set information about the folder dialog.
  BROWSEINFO bi;
  memset(&bi, 0, sizeof(BROWSEINFO));
  bi.hwndOwner = hwnd;
  bi.pidlRoot = pidlRoot;
  bi.pszDisplayName = (LPWSTR)selected_dir;
  bi.lpszTitle = title;
  bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_VALIDATE | BIF_USENEWUI;
  bi.lpfn = BrowseCallbackProc;
  bi.lParam = (LPARAM)root_dir;

  // Displays a dialog box that enables you to select a shell folder.
  LPITEMIDLIST pIDList = SHBrowseForFolder(&bi);
  if (pIDList == NULL) {
    selected_dir[0] = L'\0';
    return false;
  }

  //  Converts acquired item ID list to a file system path.
  if (!SHGetPathFromIDList(pIDList, selected_dir)) {
    CoTaskMemFree(pIDList);
    return false;
  }

  // Free the ID list allocated by SHBrowseForFolder.
  CoTaskMemFree(pIDList);
  return true;
}



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

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

    root_dirをNULLにした場合。


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

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

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

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

GetOpenDirNameのヘッダファイル

#ifndef _UTIL_H_
#define _UTIL_H_

#include <wchar.h>
#include <windows.h>

bool GetDirectoryName(HWND hwnd, const wchar_t *title, const wchar_t *root_dir,
                      wchar_t *selected_dir);
#endif  // _UTIL_H_


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

サンプルのメインソースファイル
#include <shlobj.h>
#include <stdio.h>
#include <wchar.h>
#include <windows.h>
#include <windowsx.h>
#include "./resource.h"
#include "./util.h"

namespace {
constexpr wchar_t WINDOW_NAME[] = L"Template window";
constexpr wchar_t CLASS_NAME[] = L"Template class";
}  // namespace

BOOL Cls_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) {
  (void)hWnd;
  (void)lpCreateStruct;
  return TRUE;
}

void Cls_OnDestroy(HWND hWnd) {
  (void)hWnd;
  PostQuitMessage(0);
}

void Cls_OnClose(HWND hWnd) { DestroyWindow(hWnd); }

void Cls_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
  (void)hWnd;
  (void)id;
  (void)hWndCtl;
  (void)codeNotify;
}

void Cls_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y,
                       UINT keyFlags) {
  (void)hwnd;
  (void)fDoubleClick;
  (void)x;
  (void)y;
  (void)keyFlags;

  // Show folder dialog with default root path.
  wchar_t selected_dir[MAX_PATH] = {0};
  if (!GetDirectoryName(hwnd, L"Desktop", NULL, selected_dir)) {
#ifdef DEBUG
    fwprintf(stderr, L"Error on folder select\n");
#endif
  }
#ifdef DEBUG
  fwprintf(stderr, L"WM_LBUTTONDOWN\n");
  fwprintf(stderr, L"%ls\n", selected_dir);
  fwprintf(stderr, L"\n");
#endif
}

void Cls_OnRButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y,
                       UINT keyFlags) {
  (void)hwnd;
  (void)fDoubleClick;
  (void)x;
  (void)y;
  (void)keyFlags;

  // Show folder dialog with current directory.
  wchar_t root_dir[MAX_PATH] = {0};
  GetCurrentDirectory(MAX_PATH, root_dir);

  wchar_t selected_dir[MAX_PATH] = {0};
  if (!GetDirectoryName(hwnd, root_dir, root_dir, selected_dir)) {
#ifdef DEBUG
    fwprintf(stderr, L"Error on folder select\n");
#endif
  }
#ifdef DEBUG
  fwprintf(stderr, L"WM_RBUTTONDOWN\n");
  fwprintf(stderr, L"%ls\n", selected_dir);
  fwprintf(stderr, L"\n");
#endif
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
                         LPARAM lParam) {
  switch (message) {
    HANDLE_MSG(hWnd, WM_CREATE, Cls_OnCreate);
    HANDLE_MSG(hWnd, WM_DESTROY, Cls_OnDestroy);
    HANDLE_MSG(hWnd, WM_COMMAND, Cls_OnCommand);
    HANDLE_MSG(hWnd, WM_CLOSE, Cls_OnClose);
    HANDLE_MSG(hWnd, WM_LBUTTONDOWN, Cls_OnLButtonDown);
    HANDLE_MSG(hWnd, WM_RBUTTONDOWN, Cls_OnRButtonDown);
    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
  }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPTSTR lpsCmdLine, int nCmdShow) {
  (void)hPrevInstance;
  (void)lpsCmdLine;

  // Initialization of COM.
  CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

#ifdef DEBUG
  FILE* fp = nullptr;
  AllocConsole();
  _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
#endif

#ifdef DEBUG
  fwprintf(stdout, L"Hello world to stdout!\n");
  fwprintf(stderr, L"Hello world to stderr!\n");
#endif

  WNDCLASS wc;
  wc.style = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc = WndProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.lpszMenuName = 0;
  wc.lpszClassName = CLASS_NAME;
  if (!RegisterClass(&wc)) {
    return FALSE;
  }

  HWND hWnd = CreateWindow(CLASS_NAME, WINDOW_NAME, WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                           CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
  if (hWnd == NULL) {
    return FALSE;
  }
  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

#ifdef DEBUG
  FreeConsole();
#endif

  CoUninitialize();
  return 0;
}



ポイント

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


メイクファイル
TARGET = main.exe
PDB = main.pdb
MAP = main.map
RES = resource.res
SRC = main.cc util.cc
TMPDIR = build
OBJ = $(TMPDIR)\main.obj $(TMPDIR)\util.obj

CC = "$(VCINSTALLDIR)\cl.exe"
LINK = "$(VCINSTALLDIR)\link.exe"

CPPFLAGS = /nologo /W4 /Zi /O2 /MT /EHsc /Fd"$(TMPDIR)/" /DUNICODE /D_UNICODE \
      /DDEBUG
LFLAGS = "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" \
     "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" \
     "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" "libcmt.lib" \
     /NOLOGO /SUBSYSTEM:WINDOWS /DEBUG

ALL: $(TARGET)

$(TARGET): $(OBJ) $(RES)
 $(LINK) $(LFLAGS) /OUT:$(TARGET) /PDB:"$(PDB)" /MAP:"$(MAP)" \
 $(OBJ) $(RES)

.cc{$(TMPDIR)}.obj:
 @[ -d $(TMPDIR) ] || mkdir $(TMPDIR)
 $(CC) $(CPPFLAGS) /Fo"$(TMPDIR)\\" /c $<

clean:
 rm $(TARGET) $(OBJ) *.map *.pdb *.ilk *.obj *.lib


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

備考

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

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




0 件のコメント:

コメントを投稿

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