...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

D言語でMicrosoft Component Object Model (COM) を使う (その10)

D言語 Advent Calendar 2025 14日目の記事です。

その1でCOMインタフェースを使うために必要な宣言や定義の準備ができて、 その2でCOMの初期化と解放ができるようになりました。 その3では実際にファイルを開くダイアログを表示することができ、その4では選択したファイルのパスを取得できました。 その5ではQueryInterfaceを使って別のCOMインタフェースを取得して、ファイル作成日時を取得できました。 その6ではCOMインタフェースをアップキャストもダウンキャストもできないことを説明しました。

その7ではCOMオブジェクトとCOMインタフェースを自作するための準備をしました。 その8とその9でCOMサーバーとして自作COMファクトリを登録して自作COMオブジェクトと自作COMインタフェースを取得できました。

これでFilePicker COMクラスが完成したのですが・・・。

COMオブジェクトを拡張したい

FilePicker COMクラスのpickFileメソッドにpathとtimeをそれぞれ指定する書き方をやめて、 これらをひとまとまりにした構造体で受け渡ししたいと思ったとします。

struct PickedFile
{
    LPWSTR path;
    SYSTEMTIME time;
}
HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time) { ... } // こうじゃなくて
HRESULT pickFile(PickedFile* file) { ... } // こう書きたい

でも、IFilePicker COMインタフェースはもう公開してしまったから 好き勝手に変更すると利用者への弊害が大きいし、どうしようか。

こういうことは、実際の開発では頻繁に起こります。

では、どうするか。

新しいCOMインタフェースを作る

従来のCOMインタフェースを変更しないで、 新しいCOMインタフェースであるIFilePIcker2を作って、 FilePicker COMクラスに実装を追加します。

extern(C) const IID IID_IFilePicker2 = guidFromUUID("98afd9b2-cb71-41db-9155-c689512c6531");

// uuid("98afd9b2-cb71-41db-9155-c689512c6531");
interface IFilePicker2 : IUnknown
{
extern (Windows):
    HRESULT pickFile(PickedFile* file);
}

まず、新しいCOMインタフェースを作るので、 Windows SDKのuuidgen.exeを使ってuuidを生成し、 guidに変換して、IID_IFilePicker2を作成します。

IFilePicker2 COMインタフェースには、次のメソッドだけを定義します。

HRESULT pickFile(PickedFile file);

IFilePicker2 COMインタフェースをFilePicker COMクラスに実装する

次に、FilePicker COMクラスでIFilePicker2 COMインタフェースを実装します。

まずは、基底クラス(インタフェース)にIFilePicker2を追加します。

class FilePicker : ComObject, IFilePicker, IFilePicker2

続いて、QueryInterfaceメソッドでIID_FilePicker2を受け取ったときにも対応させます。

override HRESULT QueryInterface(const(IID)* riid, void** ppv)
{
    if (*riid == IID_IFilePicker)
    {
        *ppv = cast(void*)cast(IFilePicker)this;
        AddRef();
        return S_OK;
    }
    else if (*riid == IID_IFilePicker2)
    {
        *ppv = cast(void*)cast(IFilePicker2)this;
        AddRef();
        return S_OK;
    }
    else
    {
        return super.QueryInterface(riid, ppv);
    }
}

pickFileメソッドの引数がPickedFile構造体のポインタに変わっただけで、 実際の処理は元々のpickFileメソッドにすべて書かれているので、 PickedFile構造体のメンバ変数をそれぞれ従来のpickFileメソッドに与えるだけでいいでしょう。

class FilePicker : ComObject, IFilePicker, IFilePicker2
{
extern (Windows):
    // ...
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time) { ... }
    HRESULT pickFile(PickedFile* file)
    {
        return pickFile(&file.path, &file.time);
    }
}

ここまでのまとめ

ここまでをまとめると次のとおりです。

extern(C) const IID IID_IFilePicker2 = guidFromUUID("98afd9b2-cb71-41db-9155-c689512c6531");

// uuid("98afd9b2-cb71-41db-9155-c689512c6531");
interface IFilePicker2 : IUnknown
{
extern (Windows):
    HRESULT pickFile(PickedFile* file);
}

struct PickedFile
{
    LPWSTR path;
    SYSTEMTIME time;
}

extern(C) const CLSID CLSID_FilePicker = guidFromUUID("0b425ee1-5991-4dbd-89e7-a12bf13fbaef");

// uuid("0b425ee1-5991-4dbd-89e7-a12bf13fbaef");
class FilePicker : ComObject, IFilePicker, IFilePicker2
{
extern (Windows):
    override HRESULT QueryInterface(const(IID)* riid, void** ppv)
    {
        if (*riid == IID_IFilePicker)
        {
            *ppv = cast(void*)cast(IFilePicker)this;
            AddRef();
            return S_OK;
        }
        else if (*riid == IID_IFilePicker2)
        {
            *ppv = cast(void*)cast(IFilePicker2)this;
            AddRef();
            return S_OK;
        }
        else
        {
            return super.QueryInterface(riid, ppv);
        }
    }
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time)
    {
        HRESULT hr;

        IFileOpenDialog pDialog;
        hr = CoCreateInstance(&CLSID_FileOpenDialog, null, CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, cast(void**)&pDialog);
        if (FAILED(hr)) return hr;
        scope(exit) pDialog.Release();

        hr = pDialog.Show(null);
        if (FAILED(hr)) return hr;

        IShellItem pItem;
        hr = pDialog.GetResult(&pItem);
        if (FAILED(hr)) return hr;
        scope(exit) pItem.Release();

        LPWSTR outPath;
        hr = pItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &outPath);
        if (FAILED(hr)) return hr;
        scope(exit) CoTaskMemFree(outPath);

        // import std.conv;
        // *path = outPath.to!string;
        import core.stdc.wchar_;
        wcscpy(*path, outPath);

        IShellItem2 pItem2;
        hr = pItem.QueryInterface(&IID_IShellItem2, cast(void**)&pItem2);
        if (FAILED(hr)) return hr;
        scope(exit) pItem2.Release();

        PROPERTYKEY prop = PKEY_DateCreated;
        FILETIME filetime;
        pItem2.GetFileTime(&prop, &filetime);
        FileTimeToSystemTime(&filetime, time);

        return S_OK;
    }
    HRESULT pickFile(PickedFile* file)
    {
        return pickFile(&file.path, &file.time);
    }
}

まだファクトリクラスのFilePickerFactoryの修正も必要ですが、次の記事に回します。