...ing logging 4.0

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

DFL: ドラッグアンドドロップ(3)~IDropSource編~

前回、IDropTarget インタフェースを使って、自アプリへのドロップを実装しました。

受け渡すデータに対応する IDataObject インタフェースを実装したクラス、ドロップ先に対応する IDropTarget インタフェースを実装したクラス、ドラッグ元に対応する IDragSource インタフェースを実装したクラスがあれば、OLE DnD が一通り使えます。

今回は、IDropSource インタフェースを使って、自アプリからのドラッグを実装します。

DnD で受け渡せるデータの定義方法はほかにもありますが、DFL では IDataObject インタフェースを実装した DataObject クラスにデータを格納して受け渡す方法だけが用意されているようです。

IDragSource インタフェースが持つメソッドはこの2つです。

  • giveFeedBack (IDropSource::GiveFeedback)
  • queryContinueDrag (IDropSource::QueryContinueDrag)

ドロップ先のテストとしては、色々なデータを受け取ってくれるワードパッドを使います。

OLE ドラッグの実装

import dfl;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    private Label _label;

    public this()
    {
        this.text = "OLE-Drag example";
        this.size = Size(600, 300);

        _label = new Label();
        _label.parent = this;
        _label.location = Point(0,0);
        _label.dock = DockStyle.FILL;
        _label.font = new Font("Meiryo UI", 14f);

        _label.text = "これはOLEドラッグ対応のラベルです。\n" ~ 
                      "このエリアからワードパッドへドラッグしてください。\n";

        // マウスボタンを押したらドラッグ開始する
        _label.mouseDown ~= (Control sender, MouseEventArgs e) {
            // 受け渡すデータの入れ物を用意する
            DataObject dataObj = new DataObject();
            // 今回はラベルの文字列を受け渡すデータとして用意した
            string txt = _label.text;
            // DataFormats.stringFormatはドロップターゲットが文字列を受け取るときに指定する
            // Dataクラスのコンストラクタは色々な型に対応している(string[]もOK)
            dataObj.setData(DataFormats.stringFormat, new Data(txt));
            // ドラッグ開始する
            // 今回はDragDropEffects.COPYモードのドラッグだけ許可する
            DragDropEffects effect = _label.doDragDrop(dataObj, DragDropEffects.COPY);
        };
        // ドロップ先にマウスカーソルが表示されるときの見た目を変える
        _label.giveFeedback ~= (Control sender, GiveFeedbackEventArgs e) {
            e.useDefaultCursors = true; // デフォルト
            // ほかのカーソルを使うときは...
            //
            // e.useDefaultCursors = false;
            // if ((e.effect & DragDropEffects.COPY) == DragDropEffects.COPY)
            // Cursor.current = ...; // カーソルの画像リソースを用意する必要あり
            // else ...
        };
        // ドラッグ中の出来事に対応する(ドラッグをキャンセルするなど)
        _label.queryContinueDrag ~= (Control sender, QueryContinueDragEventArgs e) {
            if(e.escapePressed)
            {
                e.action = DragAction.CANCEL; // ESCキーを押したらキャンセル
            }
            else if ((e.keyState & DragDropKeyStates.RIGHT_MOUSE_BUTTON) == DragDropKeyStates.RIGHT_MOUSE_BUTTON)
            {
                e.action = DragAction.CANCEL; // 右クリックを押したらキャンセル
            }
        };
    }
}

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

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

これで、OLE によるドラッグアンドドロップできました。

いつもどおり WInForms とほとんど同じですので、詳しい説明はコード中のコメントに任せます。

なお、DnD で受け渡すあらゆるデータを保持するために Data クラスが用意されているのは、前回説明したとおりです。

GitHub のサンプルコード

GitHub の DFL に同梱されたサンプルコードの方には、ListBox を OLE DnD に対応させた DragDropListBox を実装して公開しています。 本家 WinForms の ListView にはあって ListBox にはない、便利な itemDrag イベントハンドラを定義してあります。 DragDropListBox は、その辺りを考慮して、ウィンドウプロシージャでマウスの位置とボタンの状態を監視して状態遷移処理をしているので流れが複雑ですが、まあまあ直感的な操作感にできていると思います。 本当なら、ListBox のアイテム状態には、解除状態と選択状態のほかに、ListView にある解除待ち状態も欲しいところです。

リストボックスのドラッグアンドドロップ問題

先行研究1先行研究2によれば、ListBox を複数選択モードにしてドラッグに対応させるのは鬼門のようです。 selectedItems() と selectedIndices() での選択中項目の取得がうまくいかなかったり、例外が出てしまうそうですが、原因としていくつか可能性らしきものは掴んだので、今度、書こうかなと思います。 大元は、ドラッグ開始時に発生する WM_LBUTTONDOWN を ListBox のデフォルトウィンドウプロシージャに与えてはいけないのだと推測しています。

DFL のダウンロード

github.com

参考文献

DFL: ドラッグアンドドロップ(2)~IDropTarget編~

前回、DragAcceptFiles() を使い、自アプリへのファイルのドロップを簡単に実装できましたが、この方法ではできないことが色々あります。

  1. ファイル以外のドロップができない(OLE DnD が必要)。
  2. 自ウィンドウからドラッグを開始できない(OLE DnD の IDropSource インタフェースの実装が必要)。
  3. キー入力(shift, ctrl, alt)と組み合わせてドロップのモード(move, copy, link など)を選択できない(OLE DnD の IDropTarget インタフェースの実装が必要)。

そこで、allowDrop プロパティで OLE DnD を有効にして、OLE でのドロップを使います。 これにより、ファイル以外のドロップができ、キー入力と組み合わせてドロップのモードを変更することができます。

なお、まだ OLE Drag は含まれないので、自ウィンドウからドラッグ開始はできません。

DFL の Control クラスは COM の IDropTarget インタフェースを実装しているので、それを継承している Form やその他のコントロールも同様にドロップ先になれます。

Control クラスには、IDropTarget インタフェースのメソッドにそれぞれ対応するイベントが用意されています。

  • dragEnter (IDropTarget::DragEnter)
  • dragOver (IDropTarget::DragOver)
  • dragLeave (IDropTarget::DragLeave)
  • drapDrop (IDropTarget::Drop)

OLE ドロップの実装

import dfl;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    private Label _label;

    public this()
    {
        this.text = "Drag-and-Drop example";
        this.size = Size(600, 300);

        this.allowDrop = true; // フォームがドロップを受け付けるように設定する
        
        // フォームエリアにドラッグして入ってきたとき
        this.dragEnter ~= (Control sender, DragEventArgs e) {
            // 何もすることなし
        };
        // フォームエリア内でドラッグ中のとき
        this.dragOver ~= (Control sender, DragEventArgs e) {
            // ドラッグ中のオブジェクトに応じた処理を書く
            // fileDropを与えて呼び出すとファイルをドロップできるフォームになる
            // ドロップしたいオブジェクトによって適宜変える
            if (e.data.getDataPresent(DataFormats.fileDrop))
            {
                if ((e.keyState & DragDropKeyStates.SHIFT_KEY) == DragDropKeyStates.SHIFT_KEY)
                {
                    // ドラッグ中にSHIFTキーを押したら移動アイコンにする
                    e.effect = DragDropEffects.MOVE;
                }
                else if ((e.keyState & DragDropKeyStates.ALT_KEY) == DragDropKeyStates.ALT_KEY)
                {
                    // ドラッグ中にALTキーを押したらリンクアイコンにする
                    e.effect = DragDropEffects.LINK;
                }
                else
                {
                    // ドラッグ中にCTRLキーを押したらコピーアイコンにする
                    e.effect = DragDropEffects.COPY;
                }
                assert((e.allowedEffect & e.effect) != 0);
            }
            else
            {
                // 受け取れないものをドラッグしているときはドロップ不可アイコンにする
                e.effect = DragDropEffects.NONE;
            }
        };
        // ドロップしたとき
        this.dragDrop ~= (Control sender, DragEventArgs e) {
            // ドロップしたファイル一覧を得る
            string[] files = e.data.getData(DataFormats.fileDrop, false).getStrings;
            _label.text = "";
            foreach (string fileName; files)
            {
                _label.text = _label.text ~ fileName ~ "\n";
            }
        };
        // ドラッグ中にフォームエリアから出たとき
        this.dragLeave ~= (Control sender, EventArgs e) {
            // 何もすることなし
        };

        _label = new Label();
        _label.location = Point(0,0);
        _label.autoSize = true;
        _label.dock = DockStyle.FILL;
        _label.font = new Font("Meiryo UI", 14f);
        _label.text = "Drop files to this form.";
        _label.parent = this;
    }
}

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

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

いつもどおり基本的には WinForms と同じです。

完全に作りかけだったのか一時期は動いていたのか分かりませんが、どうも OLE DnD のコードは完全に壊れていました。 キャストしてコンパイルが通るようにしただけだったというか、なんかすごいことになっていました。

ドロップするあらゆるデータを Data クラスが保持するようになっており、Data クラスは、データそのものと、そのデータの型を覚えています。 データそのものは、int とか Object とか string とか ubyte[] とかの中身です。 これを全部 void* で指しておいて、後で復元できるようにデータの型を別に記憶していたようですが、ぶっ壊れていました。

とりあえずここを直すために、void* で保持することをやめて、保持できるデータ型ごとに別のメンバ変数を設けてやっつけました。 うまくいけば、Phobos の SumType や Variant が綺麗に使えるところかもしれません。

DFLのダウンロード

github.com

参考文献

DFL: ドラッグアンドドロップ(1)~DragAcceptFiles編~

ソースはリンク切れのため今となっては詳細不明。

DFLドラッグアンドドロップがうまく動かないという話はずっと前に聞いていたのですが、自分で使ったことがなかったためそのままでしたので、今回どうなっているのか試してみました。

調べたところ、Windowsドラッグアンドドロップは、実装方法が2つあって、DragAcceptFiles() を使ってファイルのドロップだけに対応する簡単な方法と、あらゆるドラッグとドロップの両方に対応する OLE Drag & Drop (DnD) があるようです。

DFL は OLE DnD をサポートしているようですが、ファイルをウィンドウにドロップしても、正しいファイル名が取得できませんでした。 検証するためには COM の壁も超えないといけなくて調べることが多く大変なので、まずは簡単な方で実験しました。

下のウィンドウにエクスプローラーからファイルをドロップすると、ファイルパス一覧が描画されます。

import dfl;
import std.conv : to;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

import core.sys.windows.shellapi; // DragAcceptFiles, DragQueryFile, DragFinish, HDROP
import core.sys.windows.winuser; // WM_DROPFILES

class MainForm : Form
{
    private Label _label;

    public this()
    {
        this.text = "Drag-and-Drop example";
        this.size = Size(600, 300);

        // wndProc()でWM_DROPFILESメッセージをトラップできるようにする
        DragAcceptFiles(handle, true);

        _label = new Label();
        _label.location = Point(0,0);
        _label.autoSize = true;
        _label.dock = DockStyle.FILL;
        _label.font = new Font("Meiryo UI", 14f);
        _label.text = "Drop files to this form.";
        _label.parent = this;
    }

    protected override void wndProc(ref Message msg)
    {
        switch (msg.msg)
        {
            case WM_DROPFILES:
                HDROP hDrop = cast(HDROP)msg.wParam;
                
                // ドロップしたファイル数を得る
                uint numFiles = DragQueryFile(hDrop, -1, null, 0);
                if (numFiles == 0)
                {
                    _label.text = "Error";
                }
                else
                {
                    _label.text = "";
                    for (int i; i < numFiles; i++)
                    {
                        enum BUFFER_LENGTH = 260;
                        wchar[BUFFER_LENGTH] fileName;
                        // ドロップしたファイル1つずつのパスをメモリにコピーする
                        DragQueryFile(hDrop, i, fileName.ptr, BUFFER_LENGTH);
                        _label.text = _label.text ~ to!string(fileName);
                        _label.text = _label.text ~ "\n";
                    }
                }
                // ファイルパスを受け取るために確保されたメモリを解放する
                DragFinish(hDrop);
                break;
         default:
        }
        super.wndProc(msg);
    }
}

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

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

簡単な方法だけあって、特に難しいことはありませんでした。

後々分かったのですが、HDROP 型のハンドルには、受け取ったファイルパスが直列に並んで書き込まれているメモリ領域が保持されているようです。 "filepath\0filepath\0filepath\0\0" のように、最後は "\0\0" で終わります。 きっと、DragQueryFile() がファイルパス1つずつの切り出しをしているのでしょうね。

fileName が wchar[] なのは DragQueryFile() が wchar* を求めるから。 to!string(fileName) しているのは、Label.text() が string を求めるからです。 ansi/unicode の設定をちゃんとすれば自分で変換するのは不要かもしれません(手抜き)。

あと、dfl.internal.utf.dragQueryFile() という関数が用意されているのに気がつきました。こっちを使ってもいいかもしれません。

簡単な方法なので、このままだとできないことが色々ありそうです。

  1. ファイル以外のドロップができない(OLE DnD が必要)。
  2. 自ウィンドウからドラッグを開始できない(OLE DnD の IDropSource インタフェースの実装が必要)。
  3. キー入力(shift, ctrl, alt)と組み合わせてドロップのモード(move, copy, link など)を選択できない(OLE DnD の IDropTarget インタフェースの実装が必要)。

DFL のダウンロード

github.com

OLE DnD でのファイルドロップに対応したバージョンを既に公開しています。 今回のサンプルコードと似たようなサンプルも公開済みです。

参考文献

DFL: Splitter コントロールのサンプルコード

github.com

Splitter コントロールには最小幅を設定するプロパティがありますが、最大幅を設定するプロパティがないので、サンプルコードでは自前で実装しています。 方法は、Splitter の左又は上にあるパネルのサイズが変更されたときに、所定の最大幅を超えていたら、最大幅まで小さくするようにしています。 WInForms には最大幅を設定するプロパティがあるようですが、DFL には実装されていません(注釈とともにコメントアウントされている)。

使用上の注意としては、Splitter がどのコントロールにドッキングしているかが重要なので、コントロール(ここではパネル)をフォームに登録する順序に気をつけてください。

このサンプルコードでは、パネルに文字を描画するために、paint イベントハンドラに処理を書いています。 WinForms と同じ方法で文字などをコントロールに描画することができています。

import dfl;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    private Splitter _splitter1;
    private Splitter _splitter2;
    private Panel _panel1;
    private Panel _panel2;
    private Panel _panel3;

    this()
    {
        this.text = "Splitter example";
        this.size = Size(300, 300);

        _panel1 = new Panel();
        _panel1.dock = DockStyle.LEFT;
        _panel1.width = 100;
        _panel1.borderStyle = BorderStyle.FIXED_3D;
        _panel1.backColor = Color(255, 255, 255);
        _panel1.resize ~= (Control c, EventArgs e) {
            if (_panel1.width > 250)
            {
                _panel1.width = 250;
            }
        };
        _panel1.paint ~= (Control c, PaintEventArgs e) {
            Graphics g = e.graphics;
            string str = "min=25(default)\nmax=250";
            Font font = new Font("Meiryo UI", 10f);
            Color color = Color(0, 0, 0);
            Size size = g.measureText(str, font);
            g.drawText(str, font, color, Rect(0, 0, size.width, size.height));
        };
        _panel1.parent = this;

        _splitter1 = new Splitter();
        _splitter1.parent = this;

        _panel2 = new Panel();
        _panel2.dock = DockStyle.TOP;
        _panel2.height = 100;
        _panel2.borderStyle = BorderStyle.FIXED_3D;
        _panel2.backColor = Color(255, 255, 255);
        _panel2.resize ~= (Control c, EventArgs e) {
            if (_panel2.height > 120)
            {
                _panel2.height = 120;
            }
        };
        _panel2.paint ~= (Control c, PaintEventArgs e) {
            Graphics g = e.graphics;
            string str = "min=25(default)\nmax=120";
            Font font = new Font("Meiryo UI", 10f);
            Color color = Color(0, 0, 0);
            Size size = g.measureText(str, font);
            g.drawText(str, font, color, Rect(0, 0, size.width, size.height));
        };
        _panel2.parent = this;

        _splitter2 = new Splitter();
        _splitter2.dock = DockStyle.TOP;
        _splitter2.parent = this;

        _panel3 = new Panel();
        _panel3.dock = DockStyle.FILL;
        _panel3.borderStyle = BorderStyle.FIXED_3D;
        _panel3.backColor = Color(255, 255, 255);
        _panel3.parent = this;
    }
}

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

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

DFL: ListViewコントロールのサンプルコード

github.com

View.DETAILS:

View.LIST:

ListView コントロールのサンプルですが、アイコンを用意していないので、view プロパティに View.LARGE_ICON と View.SMALL_ICON を設定しても見栄えが悪いです。

あと、リストビューの詳細表示の1列目にはセンタリングなどのアライメントを設定できず、常時左寄せになる仕様らしいです。 末尾に挙げた参考文献を見て対策を試みましたが、どうもうまくいきませんでした。

サンプルコードでは、アライメント設定を遅らせることで設定が受け付けられたため、そのままにしてあります。

import dfl;
import std.conv;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    private ListView _listView;

    this()
    {
        this.text = "ListView example";
        this.size = Size(300, 300);

        // Create
        _listView = new ListView();
        _listView.parent = this;

        // Style
        _listView.dock = DockStyle.FILL;
        _listView.view = View.DETAILS;
        // _listView.view = View.LIST;
        // _listView.view = View.LARGE_ICON;
        // _listView.view = View.SMALL_ICON;
        _listView.gridLines = true;
        _listView.multiSelect = false;
        _listView.hideSelection = false;
        _listView.fullRowSelect = true;
        _listView.checkBoxes = true;

        // Header
        ColumnHeader colX = new ColumnHeader();
        colX.text = "X";
        colX.width = 70;

        ColumnHeader colY = new ColumnHeader();
        colY.text = "Y";
        colY.width = 70;

        ColumnHeader colXY = new ColumnHeader();
        colXY.text = "XY";
        colXY.width = 70;

        _listView.columns.addRange([colX, colY, colXY]);

        // Contents
        _listView.beginUpdate(); // Stop redraw.

        // Work around: The first column alignment setting is enabled after beginUpdate().
        colX.textAlign = HorizontalAlignment.CENTER;
        colY.textAlign = HorizontalAlignment.RIGHT;
        colXY.textAlign = HorizontalAlignment.LEFT;

        for (int x=1; x<=3; x++)
        {
            for (int y=1; y<=3; y++)
            {
                string xstr = to!string(x);
                string ystr = to!string(y);
                string xystr = to!string(x*y);
                ListViewItem item = new ListViewItem(xstr);
                _listView.items.add(item); // Add item to first column.
                item.subItems.add(ystr);   // Add sub item to second column.
                item.subItems.add(xystr);  // Add sub item to third column.
            }
        }

        _listView.endUpdate(); // Restart redraw.
    }
}

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

参考

DFL: ビジュアルスタイルの有効化

追記あり

続・追記あり


DFLに限らないけれども。

VisualStyle有効:

VisualStyle無効:

以下のコードをソースファイルに追加してdmdでビルドすれば、実行ファイルと同時にマニフェストファイルが生成される。 実行ファイルを起動すれば、ビジュアルスタイルが有効になっている。

でもビジュアルスタイルを有効にすると、なぜかタブストップのフォーカスの表示が見えなくなっている???

version(X86_64)
{
    pragma(linkerDirective, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"");
}
version(X86)
{
    pragma(linkerDirective, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"");
}

マニフェストと実行ファイルを別にしたくない場合は、リソースファイルを作ってリソースコンパイラコンパイルしてリンクするらしい。こっちはめんどくさいのでまだ試していない。

ところで最近のアプリケーションはコモンコントロールを使わないのが増えたなあ。

追記

DFL の場合、 Application.enableVisualStyles() を最初に呼び出せばいいだけだった。 でもやっぱりフォーカスが見えなくなってしまう。

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

続・追記

ビジュアルスタイルを有効にしたとき、フォーカスの点線枠が表示されていなかったが、実行する度に違う結果になる。 どうもコントロールを初めて作成するよりも早く Application.enableVisualStyles() が呼ばれるときと、その逆順になるときがあるように思える。

そこで、ビジュアルスタイル有効化のタイミングをもっと早めればいいのかも、と思って試してみたところ、static this() 内で呼び出すのが一番簡単そうだ。今のところ、安定している。

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

void main()
{
    // Don't call this here!!
    // Application.enableVisualStyles();

    Application.run(new MainForm());
}

参考

フォーカスを自前で描画する方法をついでに調べたときのメモ。結局、使うことはなかった。

DFL: StatusBarコントロールのサンプルコード

github.com

ウィンドウにステータスバーを表示し、3つのパネルを追加します。

各パネルは borderStyle プロパティに StatusBarPanelBorderStyle を設定することで3種類の見た目に変更できます。

ウィンドウ内を左クリックすると左端のステータスバーにクリックした回数がカウントされます。パネルのテキストを変更する実装例です。

import dfl;
import std.conv;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    private StatusBar _statusBar;

    this()
    {
        this.text = "StatusBar example";
        this.size = Size(300, 300);

        _statusBar = new StatusBar();

        StatusBarPanel panel1 = new StatusBarPanel("Click count:");
        StatusBarPanel panel2 = new StatusBarPanel("Second panel");
        StatusBarPanel panel3 = new StatusBarPanel("Third panel");

        panel1.borderStyle = StatusBarPanelBorderStyle.SUNKEN;
        panel2.borderStyle = StatusBarPanelBorderStyle.RAISED;
        panel3.borderStyle = StatusBarPanelBorderStyle.NONE;

        panel1.width = 100;

        _statusBar.panels.add(panel1);
        _statusBar.panels.add(panel2);
        _statusBar.panels.add(panel3);

        _statusBar.showPanels = true;
        _statusBar.parent = this;

        this.click ~= (Control c, EventArgs e) {
            static int counter;
            panel1.text = "Click count: " ~ to!string(++counter);
        };
    }
}

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