...ing logging 4.0

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

DFL: 既定プリンターの給紙方法を取得する

年末から、DFLに印刷機能を追加しようとしている。 GDI Print APIという古いAPIを使っているので日本語の資料は色々あるけど、先は長そうだ。 まずは、APIの使い方を調べるため、ページ設定ダイアログと印刷ダイアログを表示したり、印刷するには印刷できたが、 印刷スケールが滅茶苦茶だったりして、まだまともに動作していない。 とりあえず、WinFormsのPageSetupDialogクラスに相当する実装が動くようにしよう。

さて、ページ設定ダイアログにある給紙方法の選択コンボボックスでの選択結果を取得する方法がよく分からなくて詰まっていたが、 なんとか動くようになったので、一旦、ここにまとめておく。 DeviceCapabilities()にDC_BINSやDC_BINNAMESをセットして返ってくる給紙方法番号や給紙方法文字列が、めんどくさい仕様だった。

ページ設定ダイアログで選択された給紙方法番号は、DEVMODE.dmDefaultSourceで得られる。 この番号は、次のとおり定義されている(wingdi.dから引用)。

// DEVMODE.dmDefaultSource
enum : short {
    DMBIN_ONLYONE = 1,
    DMBIN_UPPER   = 1,
    DMBIN_LOWER,
    DMBIN_MIDDLE,
    DMBIN_MANUAL,
    DMBIN_ENVELOPE,
    DMBIN_ENVMANUAL,
    DMBIN_AUTO,
    DMBIN_TRACTOR,
    DMBIN_SMALLFMT,
    DMBIN_LARGEFMT,
    DMBIN_LARGECAPACITY, // = 11
    DMBIN_CASSETTE   = 14,
    DMBIN_FORMSOURCE,
}
enum : short {
    DMBIN_FIRST = DMBIN_UPPER,
    DMBIN_LAST = DMBIN_FORMSOURCE,
    DMBIN_USER = 256,
}

給紙方法番号は、上記のシステム定義のほか、DMBIN_USER(256)以上の番号で、プリンタごとに定義されている。 うちのプリンターの場合は、5種類の給紙方法のうち、4つがユーザー定義で、1つがシステム定義だった。

DeviceCapabilities()にDC_BINSとWORD型の配列バッファを与えると、配列バッファに給紙方法番号が得られる。 つまり、要素が給紙方法番号を表している

[276, 7, 277, 257]

のような配列が得られる。 また、配列の要素数は関数の戻り値で得られる。

DeviceCapabilities()にDC_BINNAMESと文字配列のバッファを与えると、配列バッファに給紙方法文字列が得られる。 この文字列の仕様がめんどくさい。 それぞれの文字列は24文字ごとに書き込まれていて、 1つの文字列は最大24文字までで、null終端されている。 ただし、1つの文字列が24文字のときは、null終端がない。 よって、文字列ごとにwstring型に分割して代入するために、null終端されているのが何文字目かを調べて、文字列長を修正する手間がかかった。

これで、給紙方法番号と給紙方法文字列の対応が得られた。 2つの配列の同じインデックス番号同士の要素が、それぞれ組になる。 値は嘘だが次のようなイメージ。

[276, 7, 277, 257]
["自動", "手差し", "上トレイ", "下トレイ"]

さらにめんどくさいことに、次は、dmDefaultSourceと同じ値(上記の277とか)を、 給紙方法番号配列から検索して、そのインデックス番号(上記なら2)を得る。

給紙方法文字列の配列をそのインデックス番号で引くと、 ページ設定ダイアログに表示されているものと同じ文字列が得られる。

ということで、とりあえず動いていそうなコードは以下のとおり。

PaperSource createPaperSource(HGLOBAL hDevMode, HGLOBAL hDevNames)
{
    DEVMODE* pDevMode = cast(DEVMODE*)GlobalLock(hDevMode);
    scope(exit)
        GlobalUnlock(pDevMode);
    DEVNAMES* pDevNames = cast(DEVNAMES*)GlobalLock(hDevNames);
    scope(exit)
        GlobalUnlock(pDevNames);
    
    // Get printer basic settings.
    string deviceName = fromUnicodez(pDevMode.dmDeviceName.ptr);
    string outputPort = fromUnicodez(cast(wchar*)(cast(ubyte*)pDevNames + pDevNames.wOutputOffset * wchar.sizeof));

    // Get default paper source kind.
    PaperSourceKind sourceKind = {
        if (pDevMode.dmDefaultSource <= DMBIN_LAST) // System defined paper source.
            return cast(PaperSourceKind)pDevMode.dmDefaultSource;
        else if (pDevMode.dmDefaultSource >= DMBIN_USER) // User defined paper source.
            return PaperSourceKind.CUSTOM;
        else
            assert(0);
    }();

    // Get number of paper sources.
    int sourceNum = DeviceCapabilities(toUnicodez(deviceName), "", DC_BINS, null, pDevMode);
    WORD[] sourceBuffer = new WORD[sourceNum];
    DeviceCapabilities(toUnicodez(deviceName), "", DC_BINS, cast(wchar*)sourceBuffer.ptr, pDevMode);
    WORD[] sourceList;
    for (int i = 0; i < sourceNum; i++)
        sourceList ~= sourceBuffer[i];

    // Get name of paper sources.
    enum BINNAME_MAX_LENGTH = 24;
    wchar[] sourceNamesBuffer = new wchar[BINNAME_MAX_LENGTH * sourceNum];
    DeviceCapabilities(toUnicodez(deviceName), toUnicodez(outputPort), DC_BINNAMES, sourceNamesBuffer.ptr, pDevMode);
    // Reference: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-devicecapabilitiesw
    // Value: DC_BINNAMES
    // Meaning: Retrieves the names of the printer's paper bins.
    //          The pOutput buffer receives an array of string buffers.
    //          Each string buffer is 24 characters long and contains the name of a paper bin.
    //          The return value indicates the number of entries in the array.
    //          The name strings are null-terminated unless the name is 24 characters long.
    //          If pOutput is NULL, the return value is the number of bin entries required.
    wstring[] sourceNameList;
    for (int i = 0; i < sourceNum; i++)
    {
        wchar* w = cast(wchar*)(cast(ubyte*)sourceNamesBuffer + i * BINNAME_MAX_LENGTH * wchar.sizeof);
        int end = -1;
        for (int j = 0; j < BINNAME_MAX_LENGTH; j++)
        {
            if (w[j] == '\0')
            {
                end = j;
                break;
            }
        }
        if (end == -1) // Null terminal is not found.
            sourceNameList ~= w[0..BINNAME_MAX_LENGTH].dup; // TODO: Is it correct?
        else
            sourceNameList ~= w[0..end].dup; // Contains null terminal.
    }

    // Get paper source name.
    // Search index of paper source.
    wstring sourceName = {
        int index = -1;
        for (int i = 0; i < sourceNum; i++)
        {
            if (sourceList[i] == pDevMode.dmDefaultSource)
            {
                index = i;
                break;
            }
        }
        if (index != -1)
            return sourceNameList[index];
        else
            return "no name"w;
    }();
    
    return new PaperSource(sourceKind, to!string(sourceName));
}

備考

  • toUnicodez()の実装は、ここには載っていない。DFLの関数なので、省略している。
  • hDevModeとhDevNamesは、上位の関数内で生成、開放すること。 生成は、ページ設定ダイアログを開いたらシステムがやってくれるので、実際には開放処理だけ書けばいい。