Tips13 OLEによるDnD

前のTips


 ViViビルド#0125でやっとドラッグ&ドロップ(以下DnDと略す)をOLEにより実装しました。 それ以前は、マウス関係のハンドラで自前で処理していましたが、ViVi内の異なるウィンドウ間や、 別のアプリケーションとの間でDnDできないので、 OLEによる実装に変更して欲しいという要望に応えたものです。
 MFC 4.x ではOLEのためのクラスが定義されており、COMインタフェース等を完全に包み隠しているので、 OLEを完全に理解していなくても、簡単にプログラムが記述できます。 しかし実際に実装してみると、やっぱり(本質と関係無いところで)結構苦労しました。 以下はその苦労の記録でもあります。これからOLEのプログラムを組んでみようという方の参考になれば幸いです。

■ OLEによるDnDの概要

 DnDのための汚れ仕事(?)はほとんどMFCがやってくれます。 プログラマはドロップターゲットになるビューでドロップのためのハンドラ等を定義し、 ドロップソースのビューでドラッグを開始するメソッドを呼ぶだけです。
ドロップソース側:            ドロップターゲット側:
┏━━━━━━━━━┓          ┏━━━━━━━━━┓
┃CView       ┃          ┃CView       ┃←─┐
┃┌───────┐┃          ┃         ┃  │ハンドラメソッドを
┃│OnLButtonDown │┃          ┃┌───────┐┃  │ コール
┃│  ハンドラ  │┃          ┃│COleDropTarget├╂──┘
┃└───────┘┃          ┃└───────┘┃
┃    │    ┃          ┃    ↑    ┃
┗━━━━┿━━━━┛          ┗━━━━┿━━━━┛
     │・インスタンス化            │
     │・データを設定             │
     ↓・DoDragDrop をコール          │
 ┌───────┐            ┌───────┐
 │COleDataSource│───────────→│COleDataObject│
 └───────┘  COMによる通信  └───────┘

 このようにOLEによるDnD実装はオブジェクト指向化されており、 ソースとターゲットで独立にコードを記述するのが特徴的です。 他の部分との依存関係をあまり考えなくてもよいので、コードの記述はかなり楽ですが、 ソースとターゲットのビューが同じだったり、同じスプリットウィンドウの異なるペインだったり、 同じドキュメントの異なるビューの場合は、マルチスレッドの場合の同期処理のようなものが必要になります。

■ OLEの初期化

 今回のOLEの実装で一番時間がかかった(数時間を要した)のは、このOLEの初期化を行う部分でした。 OLEの初期化は CWinApp 派生クラスの InitInstance で、

    BOOL CViviApp::InitInstance()
    {
        if( !AfxOleInit() )             //  OLEの初期化
            return FALSE;
        .....
    }
とします。このような簡単なコードを記述するのに、どうして数時間も要してしまったのか、 不思議に思われるかもしれませんが、最初は『OLEの初期化が必要だということが解らなかった』からです。!!!
 最初にテストのプロジェクトを作成し、必要最小限のコードで動作確認を行いましたが、ぜんぜん動作しません。 ヘルプを片っ端から読みましたがOLEをよく理解していないので、動作不良の原因がなかなか特定できません。 原因を調査してみると、後で説明するドロップターゲットのハンドラがまったくコールされていないことが解りました。 さらに調べると、ドロップターゲットへウィンドウを登録するのですが、ここで失敗していることが分かりました。 でも、何故???(この時はほんとに悩みました)
 そう、原因はOLEの初期化を行っていないことでした。  解ってしまえば、なんてことはないのですが、登録関数やドロップターゲットのハンドラに関するヘルプ部分には、 初期化が必要だという記述がどこにも無いのです。OLEのプログラムを一度でも組んでいれば常識かもしれませんが、 初体験の筆者にはなかなか思い付きませんでした。
 途方にくれた筆者は AppWizard でOLEオプションをONにして新たなプロジェクトを作成してみました。 するとドロップターゲットの登録処理がうまくいき、ハンドラもちゃんとコールされるではありませんか。 ここまで来ればしめたもので、動作するコードと動作しないコードの diff をとってみて、 OLEの初期化が必要なことが解った次第です。

 教訓:人は自分が知っていることしか知らない。

■ ドロップソース側の実装

 ドロップソース側の実装は簡単です。CViviView::OnLButtonDown で選択されている領域がクリックされた場合に、 COleDataSource のインスタンスを作成し、それにDnDするデータ(選択されているテキスト)を結合し、 COleDataSource::DoDragDrop をコールします。 このメソッドはDnD処理を行い、その結果、コピーかムーブかキャンセルか(またはリンク)を返します。 ムーブの場合はソース側で選択領域を削除します。 たったこれだけのコードで、後はMFC/COMが処理を引き受けてくれるので、 登録されているドロップターゲット(Word 等)に対しDnD操作を行うことが可能になります。らくちんですね。

    void CViviView::OnLButtonDown(UINT nFlags, CPoint point)
    {
        if( isSelectedMode() && isSelectedArea(point.x, point.y) ) {  //  選択領域をクリックした場合
            HGLOBAL hGlobal = GlobalAlloc(GHND,...);        //  DnDするデータ領域を作成
            hGlobal にデータを設定;
            COleDataSource dataSource;                      //  データソースオブジェクトをインスタンス化
            dataSource.CacheGlobalData(CF_TEXT, hGlobal);   //  作成したデータを設定
            DROPEFFECT result = dataSource.DoDragDrop(DROPEFFECT_COPY|DROPEFFECT_MOVE);
            if( result == DROPEFFECT_MOVE ) {
                選択されていた領域を削除する;
            }
            return;
        }
        .....
    }

 プログラムは上記のようになります。ここでは、プレーンテキスト(CF_TEXT)をデータとして設定しています。

■ ドロップターゲット側の実装

 ドロップターゲット側の実装もそれほど手間ではありません。作業は以下の3つだけです。

  1. COleDropTarget のメンバ変数を CView の派生クラスに追加する。
  2. CView の派生クラスの OnCreate でビューのウィンドウをドロップターゲットとして登録する。
  3. CView の派生クラスの、OnDragEnter, OnDragLeave, OnDragOver, OnDrop をオーバライドする。

 以下、簡単に説明します。

  1. COleDropTarget のメンバ変数を CView の派生クラスに追加する。

     以下の様にメンバ変数を追加します。

        class CViviView : public CView
        {
        protected:
            .....
            COleDropTarget  m_dropTarget;   //  OLE DnD のドロップターゲット
            .....
        };
    
  2. CView の派生クラスの OnCreate でビューのウィンドウをドロップターゲットとして登録する。

     クラスウィザードで OnCreate をオーバライドし、 以下のようにビューウィンドウをドロップターゲットとして(COMに?)登録します。 これで、ビューがDnDターゲットになった場合は、3のメソッドたちがコールされるようになります。

        int CViviView::OnCreate(LPCREATESTRUCT lpCreateStruct)
        {
            if (CView::OnCreate(lpCreateStruct) == -1)
                return -1;
    
            if( m_dropTarget.Register( this ) )     // ドロップターゲットへの登録
                return 0;
            else
                return -1;
        }
    
  3. CView の派生クラスの、OnDragEnter, OnDragLeave, OnDragOver, OnDrop をオーバライドする。

     OnDragEnter はDnD中にマウスカーソルがターゲットウィンドウに入ったときにコールされます。 OnDragLeave は逆にウィンドウから出ていったときです。 OnDragOver はウィンドウ内でのマウスカーソルが移動したときにコールされます。 座標を調べ、ドロップ可能かどうかをCOMを通じてドロップソース側に通知します。 OnDrop は実際にドロップ処理を行うメソッドです。
     DnDされるデータ形式はプレーンテキストだけではなく、ビットマップや、 ユーザが独自に定義した形式も可能です。したがって、 各メソッドではDnDされているデータの形式を調べ、それに従った処理を行います。
     それぞれの実装例を以下に示します。

        DROPEFFECT CViviView::OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
        {
            if( pDataObject->IsDataAvailable(CF_TEXT) ) {
                int line, offset, column;
                pointToPosition(point, line, offset, column);       //  マウス位置からカーソルを設定
                カーソルを表示;
                return (dwKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE;
                                                    //  コントロールの状態により、コピーがどうかを通知
            }
            .....                                   //  他のデータ形式の処理...
        }
    
        void CViviView::OnDragLeave()
        {
                                                    //  CF_TEXT では特にすることは無い
        }
    
        DROPEFFECT CViviView::OnDragOver(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
        {
            if( pDataObject->IsDataAvailable(CF_TEXT) ) {
                int line, offset, column;
                pointToPosition(point, line, offset, column);
                if( line != m_caretLine || column != m_caretColumn ) {
                    カーソル位置を更新・表示;
                }
                return (dwKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE;
            }
            .....                                   //  他のデータ形式の処理...
        }
    
        BOOL CViviView::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)
        {
            if( pDataObject->IsDataAvailable(CF_TEXT) ) {
                HGLOBAL hData = pDataObject->GetGlobalData(CF_TEXT);
                ASSERT(hData);
                const char *ptr = (const char *)GlobalLock(hData);          //  ドロップするデータを取得
                doInsertText(ptr);                                          //  データをドキュメントに挿入
                GlobalUnlock(hData);
    
                return TRUE;
            }
            .....                                   //  他のデータ形式の処理...
        }
    

■ ドロップ先のビューをアクティブにする

 OLEでDnDを行った場合、同一インスタンス内であれば、 DnD後にターゲットのビューをアクティブにしたいものです。この実装は実は簡単なのですが、 OLEの初期化の次に時間がかかって(やはり数時間)しまいました。
 ビューウィンドウをアクティベイトしてもダメで、CMDIChildWnd とスプリットウィンドウのペインをアクティベイトする必要があります。 コードは以下のような感じです。

    BOOL CViviView::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)
    {
        if( pDataObject->IsDataAvailable(CF_TEXT) ) {
            .....

            CSplitterWnd *spltWnd = (CSplitterWnd *)GetParent();
            spltWnd->SetActivePane(0, 0, this);
            ((CMDIChildWnd *)spltWnd->GetParent())->MDIActivate();      //  ターゲットのビューをアクティブに

            return TRUE;
        }
        .....                                   //  他のデータ形式の処理...
    }

前のTips 津田伸秀 のホームページに戻る。

Last Modified on 26-Jan-1997 21:42:27, Copyright (c) 1996 by Nobuhide Tsuda, All Right Reserved.
このホームページに関するご質問、ご要望、バグレポート等は  ntsuda@beam.or.jp  までメールをいただければ幸いです。