...ing logging 4.0

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

HBITMAP(DDB)からBITMAPINFO(DIB)を得る

GetDIBits()を使う、以上。

で終わる話なのですが、BITMAPINFOを作成するために必要なメモリサイズを算出するのに手こずったので、結果だけ置いておきます。

なお、このコードで作成されるのは、スクリーンショットを撮ったときに得られるビットマップデータに合わせてbiCompressionがBI_BITFIELDSで、カラーマスクあり、カラーパレットなしの32ビットカラーDIBビットマップです。

BITMAPINFO* createBitmapInfo(HBITMAP hBitmap)
{
    BITMAP bitmap;
    GetObject(hBitmap, BITMAP.sizeof, &bitmap);
    HDC hdc = GetDC(null);

    // Allocates memory of BITMAPINFO
    const uint bitsPerPixel = bitmap.bmPlanes * 32; // 32 bits color
    const uint colorMaskBytes = {
        // Contains color mask when biCompression is BI_BITFIELDS.
        return 4 * 3; // 4 bytes * 3 masks(R,G,B)
    }();
    const uint numPallet = 0; // Wants that no color palette.
    const uint widthBytes = (bitmap.bmWidth * bitsPerPixel + 31) / 32 * 4;
    const uint bitmapBodySize = widthBytes * bitmap.bmHeight;

    BITMAPINFO* pbi = cast(BITMAPINFO*)new ubyte[
        BITMAPINFOHEADER.sizeof + colorMaskBytes + RGBQUAD.sizeof * numPallet + bitmapBodySize];

    pbi.bmiHeader.biSize = BITMAPINFOHEADER.sizeof; // First 6 members
    pbi.bmiHeader.biWidth = bitmap.bmWidth;
    pbi.bmiHeader.biHeight = bitmap.bmHeight;
    pbi.bmiHeader.biPlanes = bitmap.bmPlanes;
    pbi.bmiHeader.biBitCount = 32; // 32 bits color
    pbi.bmiHeader.biCompression = BI_BITFIELDS; // Contains color mask
    if (0 == core.sys.windows.wingdi.GetDIBits(
        hdc, hBitmap, 0, bitmap.bmHeight,
        cast(ubyte*)pbi + BITMAPINFOHEADER.sizeof + colorMaskBytes + RGBQUAD.sizeof * numPallet, pbi, DIB_RGB_COLORS))
    {
        throw new Exception("createBitmapInfo failure");
    }

    ReleaseDC(null, hdc);
    return pbi;
}

BITMAPINFO(DIB)からHBITMAP(DDB)を得る

CreateDIBitmap()を使う、以上。

で終わる話ではあるのですが、パックされたビットマップの場合、BITMAPINFOの先頭アドレスとBITMAPINFOHEADERの内容から、後に続くカラーマスクとカラーパレットのバイト数を算出して、ピクセルデータ列の先頭アドレスを求める必要があり、これがなかなか大変だったので、結果だけ置いておきます。

HBITMAP createHBitmap(BITMAPINFO* pbi)
{
    const uint bitsPerPixel = pbi.bmiHeader.biPlanes * pbi.bmiHeader.biBitCount;
    const uint colorMaskBytes = {
        if (pbi.bmiHeader.biCompression == BI_BITFIELDS)
            return 4 * 3; // 4 bytes * 3 masks(R,G,B)
        else
            return 0;
    }();
    const uint numPallet = {
        if (bitsPerPixel <= 8) // 1, 4, 8 bits color
        {
            if (pbi.bmiHeader.biClrUsed == 0)
                return 2 ^^ bitsPerPixel;
            else
                return pbi.bmiHeader.biClrUsed;
        }
        else if (bitsPerPixel <= 32) // 16, 24, 32 bits color
            return pbi.bmiHeader.biClrUsed;
        else
            throw new Exception("Illegal color bits bitmap");
    }();
    HDC hdc = GetDC(null);
    HBITMAP hBitmap = CreateDIBitmap(
        hdc, &pbi.bmiHeader, CBM_INIT,
        cast(ubyte*)pbi + BITMAPINFOHEADER.sizeof + colorMaskBytes + RGBQUAD.sizeof * numPallet, pbi, DIB_RGB_COLORS);
    ReleaseDC(null, hdc);
    return hBitmap;
}

BITMAPINFOから画像データ全体のバイナリ列を得る

かなり手こずったので結果だけ置いておきます。

ubyte[] getBitmapBuffer(BITMAPINFO* pbi)
{
    assert(pbi);
    const uint bitsPerPixel = pbi.bmiHeader.biPlanes * pbi.bmiHeader.biBitCount;
    const uint colorMaskBytes = {
        if (pbi.bmiHeader.biCompression == BI_BITFIELDS)
            return 4 * 3; // 4 bytes * 3 masks(R,G,B)
        else
            return 0;
    }();
    const uint numPallet = {
        if (bitsPerPixel <= 8) // 1, 4, 8 bits color
        {
            if (pbi.bmiHeader.biClrUsed == 0)
                return 2 ^^ bitsPerPixel;
            else
                return pbi.bmiHeader.biClrUsed;
        }
        else if (bitsPerPixel <= 32) // 16, 24, 32 bits color
            return pbi.bmiHeader.biClrUsed;
        else
            throw new Exception("Illegal color bits bitmap");
    }();
    const uint widthBytes = (pbi.bmiHeader.biWidth * bitsPerPixel + 31) / 32 * 4;
    const uint pixelBufSize = widthBytes * pbi.bmiHeader.biHeight;
    return (cast(ubyte*)pbi)[0 .. BITMAPINFOHEADER.sizeof + colorMaskBytes + RGBQUAD.sizeof * numPallet + pixelBufSize];
}

DFL: PictureBoxのサンプルコード

画像ファイルを簡単に表示することができるPictureBoxクラスのサンプルです。

Bitmapクラスで読み込める形式の画像ファイルなら表示できるようで、bmp、jpgは表示できました。 未確認ながらgifも可能かもしれませんが、pngはダメっぽいです。

WinFormsにあるPictureBoxSizeMode.ZOOMが不足していたので、ついでに追加しています。

上段左上から右に向かって1,2,3,下段左から右に向かって4,5とすると、以下の表示モードになります。

  • 1=NORMAL
  • 2=STRECH_IMAGE
  • 3=ZOOM
  • 4=CENTER_IMAGE
  • 5=AUTO_SIZE

DFLのダウンロード

github.com

参考

DFL: ClippingFormのサンプルコード

透過する部分を白色にしたビットマップを与えて、矩形でないウィンドウを作れるClippingFormのサンプルコードです。

ウィンドウタイトル(キャプション)がなく、かつ、常に最前面に表示されるウィンドウが作成されるようになっています。

閉じるボタンがないので、サンプルコードでは、クリックしたら終了するようにしてあります。 また、ドラッグしてウィンドウの位置を変更できるようにするには、自分でその処理を書く必要があります。

import dfl;

class MainForm : ClippingForm
{
    public this()
    {
        this.text = "Clipping Form example";
        this.size = Size(300, 200);
        this.clipping = new Bitmap(r".\image\clipping.bmp"); // 白色が透過される
        this.click ~= (Control c, EventArgs e) { // クリックしたら終了する
            this.close();
        };
    }
}

static this()
{
    Application.enableVisualStyles();
}

void main()
{
    Application.run(new MainForm());
}

DFLのダウンロード

github.com

DFL: Clipboardのサンプルコード

Clipboardクラスを使ってコピー・アンド・ペーストをする処理のサンプルコードです。

簡単な使い方

長くなるのでここにサンプルコードを転記するのは止めておきますが、簡単に使い方を示します。 いつもどおり、基本的にWinFormsのそれと大差ありません。

// UTF-8文字列をクリップボードにコピー
string text = "hoge";
Clipboard.setString(text);

// クリップボードにUTF-8文字列がコピーされているか確認
if (Clipboard.containsString())
{
    // クリップボードからUTF-8文字列を取得
    string utf8Str = Clipboard.getString();
    if (utf8Str !is null)
        msgBox(utf8Str);
}

上記は、いくつかのデータ型に対してのみ定義されている簡単呼び出し版のメソッド群を使った方法です。 この方法では、DataクラスやDataObjectクラスを目にすることがありません。 WinFormsには同様のメソッドがもっとたくさん定義されていますが、現在DFLが対応しているのは、以下のとおりです。 括弧内は、その実体を表す代表的なデータ型です。

  • UTF-8文字列(string)
  • Unicode文字列(wstring)
  • Ansi文字列(ubyte[])
  • DDBビットマップ(HBITMAP)
  • DIBビットマップ(BITMAPINFO)
  • ファイルドロップリスト(HDROP)

データ型の自動変換

元々、Windowsではクリップボードにコピーされた特定のデータ型間は自動変換されるのですが、UTF-8文字列はDFLで定義した独自のデータ型なので、自動変換の対象外であるため、UTF-8/Unicode/Ansiの相互の自動変換を自前で実装しています。 自動変換されるので、UnicodeでセットされたデータをUTF-8でゲットすることが可能です。

また、ビットマップについては、システムがDDBビットマップ(HBITMAP)/DIB(BITMAPINFO)/DIBV5の相互変換をしてくれます。 なのですが、一体どういう形式のDDB/DIBに変換されたのかを解釈する必要があり、ビットマップのメモリ上のバイナリフォーマットを把握するのにかなり手こずりました。何とかDDB/DIBでセットされたデータをDDB/DIBのどちらでもゲットすることができるようになりました。

高度な使い方

簡単呼び出し版でない方法は、下記のとおりです。やっているのは上記コードと同じことです。

クリップボードフォーマットをDataFormats.stringFormat等で指定して、そのフォーマットに関連付けるデータをDataクラスのインスタンスに格納してセットします。

ゲットするときには、欲しいクリップボードフォーマットのデータがクリップボード上にあるかを確認したうえで、あった場合に、Dataクラスのインスタンス(data)を取得します。 dataに格納されている(はずの)データ型に対応したgetStringFormat()等のメソッドを呼び出して、データの実体を取り出します。

// UTF-8文字列をクリップボードにコピー
string text = "hoge";
Clipboard.setData(DataFormats.stringFormat, new Data(text));

// クリップボードにUTF-8文字列がコピーされているか確認
if (Clipboard.containsData(DataFormats.stringFormat))
{
    // クリップボードからUTF-8文字列を取得
    Data data = Clipboard.getData(DataFormats.stringFormat);
    string utf8str = data.getStringFormat();
    if (utf8Str !is null)
        msgBox(utf8Str);
}

さらに高度な使い方

Windowsクリップボードには複数のデータ型を同時にセットすることができます。 これにより、ユーザがビットマップデータを「コピー」して画像をクリップボードにセットしたように見えて、メモ帳で「ペースト」したらテキストデータがゲットされる、みたいなことができるようになっています。 メモ帳はクリップボードにテキストデータがあるか確認して、あった場合は、テキストデータの形式を要求するので、そのような挙動になります。

複数のデータ型を同時にセットするためには、DataObjectクラスのインスタンスを自分で作成し、それに複数セットします。 Clipboard.setData()を呼び出すとその度に新しいDataObjectが作られてしまうので、この用途では使えません。

// ビットマップとUTF-8文字列をクリップボードにコピー
Image bitmap = new Bitmap(r".\image\sample.bmp");
string text = "hoge";
dfl.data.IDataObject dataObj = new DataObject;
dataObj.setData(DataFormats.bitmap, new Data(bitmap));
dataObj.setData(DataFormats.stringFormat, new Data(text));
Clipboard.setDataObject(dataObj, false);
// クリップボードからビットマップとUTF-8文字列を取得
dfl.data.IDataObject dataObj = Clipboard.getDataObject();
if (dataObj.getDataPresent(DataFormats.bitmap))
{
    Data bmpData = dataObj.getData(DataFormats.bitmap);
    Image image = bmpData.getBitmap();
}
if (dataObj.getDataPresent(DataFormats.stringFormat))
{
    Data strData = dataObj.getData(DataFormats.stringFormat);
    string text = strData.getStringFormat();
}

GitHubにあるサンプルコードを実行し、コピーのボタンを押したあと、メモ帳やワードパッドでペーストすると、コピーしたデータ次第でちょっと面白い挙動が見られます。

ビットマップの解釈が大変だった

DataクラスとDataObjectクラスは、ドラッグアンドドロップの処理を見直したときにも大幅に修正しましたが、それに輪を掛けた大規模修正になりました。 DataObjectの実装も、DIBビットマップの構造を把握するのも大変でした。

特に、ビットマップのメモリ上のバイナリ形式には、カラーマスクがあったりなかったりするよ、カラーパレットもあったりなかったりするよ、という記事がいくつもありましたが、あったりなかったりする条件がよく分からず困りました。

そんな中でようやく下記の記事に出会うことができ、何とかDDB/DIBのバイナリを解釈できました。

クリップボードビューアがあってよかった

普段あまり使っていなかったので忘れていましたが、クリップボードの履歴を扱えるソフトCLCL (https://nakka.com/soft/clcl/) に、クリップボードビューア機能が付いていることに気がつきました。

これのおかげで、どのようなクリップボードフォーマットをセットしたときにどのようなフォーマットが自動変換されてセットされるのかが分かるようになり、また、そのバイナリデータも目視できるようになったことで、何が起こっているのかようやく分かるようになりました。

今後の課題

DFLのDataObjectはCOMのIDataObject実装です。

本来は、IDataObject実装が保持しているクリップボードフォーマットが何なのかを照会したとき、それ自身が提供できるフォーマットだけを列挙するのが作法のようですが、現在のDFLの実装ではそうなっていません。 システムが自動変換したフォーマットも一緒に列挙されるようになっています。

そのため、見かけ上は、DataObjectから取得しているのか、システムが自動変換したものを取得しているのか、区別が付かないようになっています。

それはそういう仕様と考えればいいのですが、WinFormsでは、自動変換を認めるか認めないかを選択できるようになっています。

DFLの各種メソッドは、WinFormsのメソッドのシグネチャを踏襲しているため、自動変換を認めるか認めないかを選択できるような引数があるものの、実際には機能していません。

また、その他の課題として、標準クリップボード形式はたくさんありますが、そのすべてに対応するのはかなり大変なので、無理そうです。

DFLのダウンロード

github.com

DFL: やることリスト

TODO

  • [-] application.d
  • [-] base.d
  • [x] button.d
  • [x] chart.d
    • [x] 表を描画するTableRendererクラスを作る
    • [x] 折れ線グラフを描画するLineGraphRendererクラスを作る
    • [x] タイムチャートを描画するTimeChartRendererクラスを作る
  • [x] clipboard.d
  • [x] clippingform.d
  • [-] collections.d
  • [x] colordialog.d
  • [-] com.d
  • [x] combobox.d
  • [-] commondialog.d
  • [-] control.d
  • [x] data.d
  • [x] drawing.d
  • [x] environment.d
  • [x] event.d
  • [x] filedialog.d
  • [x] folderdialog.d
  • [x] fontdialog.d
  • [ ] form.d
    • [ ] MessageFilter 周りが中途半端なので TextBox と一緒に見直す
  • [x] groupbox.d
  • [x] imagelist.d
  • [x] label.d
  • [x] listbox.d
  • [x] listview.d
  • [x] menu.d
  • [x] messagebox.d
  • [x] notifyicon.d
  • [x] panel.d
  • [x] picturebox.d
  • [x] printing.d
  • [x] progressbar.d
  • [ ] registry.d
    • [ ] サンプルコード追加する
  • [ ] resources.d
    • [ ] サンプルコード追加する
  • [x] richtextbox.d
  • [ ] sharedcontrol.d
    • [ ] どういうときに使うのかよく分からないので調べる
  • [ ] socket.d
    • [ ] サンプルコード追加する?(そもそもDFLにsocketライブラリいる?)
  • [x] splitter.d
  • [x] statusbar.d
  • [x] tabcontrol.d
  • [ ] textbox.d
    • [ ] キー入力イベント処理の見直し
  • [ ] textboxbase.d
    • [ ] キー入力イベント処理の見直し
  • [x] timer.d
  • [x] toolbar.d
  • [x] tooltip.d
  • [x] trackbar.d
  • [x] treeview.d
  • [-] usercontrol.d

その他

  • [ ] internal/ の windows ヘッダー関係を DFL 独自のものから core.sys.windows.* に置き換える
  • [ ] Stream を廃止して undeaD ライブラリへの依存をなくす
    • COM の IStream から Picture コントロールを作成するコードだけ削除すればよさそうなので deprecated にした
  • [ ] ReBar コントロール(ToolStrip コントロール)を実装する
    • Panel を使った非フローティングウィンドウタイプならすぐできそうだが、フローティングウィンドウタイプだと難しそう
  • [x] PrintDialog とかの印刷関係の実装
  • [x] フォーム備付けのスクロールバーのサンプルコードを書く
  • [-] getter を const メンバにする
    • Windows APIを使っているメソッドでは無理そうなので諦め

更新履歴

  • 2023/3/17 更新
  • 2023/3/18 更新
  • 2023/3/19 更新
  • 2023/3/20 更新
  • 2023/3/29 更新
  • 2023/5/5 更新
  • 2024/1/14 更新
  • 2024/4/3 更新
  • 2024/4/13 更新
  • 2024/4/14 更新