« C#でカラーカーソル/アニメーションカーソルを使用する | トップページ | OLEドラッグ&ドロップ対応にする方法(ファイル編) »

2011年1月26日 (水)

OLEドラッグ&ドロップ対応にする(テキスト編)

以前、Delphi5 にてOLEドラッグ&ドロップ対応をテスト的に行なった事がありますが、1から実装する必要があって、かなり大変だった記憶があります。しかし、.NET では Control クラスにて基本となるイベント等をサポートしていて、コントロールを使ってのOLEドラッグ&ドロップを実装するのは、結構簡単に出来ます。

今回は、基本的な部分の説明と、テキストのドラッグ&ドロップ記述例について書きます。

1.ドラッグ&ドロップに関連するプロパティ/メソッド/イベント
関連するプロパティ/メソッド/イベントを一覧形式で示しておきます。使用種別の「ターゲット側」と「ソース側」というのは、それぞれ「ドラッグ&ドロップを受け取る側」と「ドラッグ&ドロップを開始する側」を意味します。これ以降は、それぞれドロップターゲット、ドロップソースと表現します。

OLEドラッグ&ドロップに関連するプロパティ・メソッド・イベント
プロパティ
/メソッド
/イベント
使用種別説明
ターゲット側ソース側
AllowDrop × ドラッグしたデータを受け入れるかどうかを設定する。ドロップ受け入れ処理に必要なイベント処理を記述しても、この値が false の場合、ドロップ処理受け入れは拒否される。
DoDragDrop × このメソッドで、ドラッグを開始すると共に、ドロップ先に受け渡すデータを設定します。また戻り値に、最終的なドロップ時の効果を返します。またドラッグ&ドロップが途中キャンセルされた場合には、DragDropEffects.Noneが返ります。
DragDrop ドラッグ アンド ドロップ操作が完了したときに発生します。このイベントはドロップソース側では発生しません。
ターゲット側では、このイベントでドロップ時の具体的な処理を行ないます。
DragEnter オブジェクトがコントロールの境界内にドラッグされると発生します。このイベントはドロップソース側では発生しません。
ターゲット側では、このイベントでカーソル形状を変える処理を行ないます。但し、KeyState が変化したらカーソル形状を変更する場合、ここでの処理は実質的にあまり意味がありません(DragOver イベントもすぐに発生するので、こちらのイベント処理だけ行なってもよい)。
DragLeave × オブジェクトがコントロールの境界の外へドラッグされると発生します。また、QueryContinueDrag イベントで、DragAction.Cancel 設定直後にも発生します。このイベントはドロップソース側では発生しません。
DragOver オブジェクトがコントロールの境界を超えてドラッグされると発生します。このイベントはドロップソース側では発生しません。
カーソル形状を KeyState により変更する必要がある場合、このイベントでその処理を行なう必要があります。
GiveFeedback ドラッグ アンド ドロップ操作が開始されたときに発生します。このイベントはドロップターゲット側では発生しません。
QueryContinueDrag ドロップソース側のコントロールでドラッグ中に、マウスボタンあるいはキー(Shift, Ctrl, Alt, Esc)の状態が変化した場合に発生します。このイベントはドロップターゲット側では発生しません。
通常このイベントでは、KeyState 変更により、ドラッグ&ドロップを中止したい場合に記述します。また、マウスボタンアップも検出出来るので、ソース側における DragDrop に相当するイベント処理としても使用出来ます。

◎:ほぼ必須となる ○:処理内容により必要 ×:ほぼ不要 -:イベント自体が発生しない

2.TextBox 内のテキストドラッグ&ドロップ
それでは、TextBox コントロール内のテキスト全部をドラッグ&ドロップにより、別の TextBox へテキストを「移動」または「複写」するサンプルを記述してみます。本当なら、テキストでも選択中のものだけをドラッグ対象にしたいところなのですが、TextBox の場合、頑張ってもまず無理だと思います。どうしてもやりたい場合は、後述する RichTextBox の利用を考えた方が良いでしょう。

    2-1.ドロップターゲットとなる TextBox の処理
    テキストボックス(textBox1)が、ドロップターゲットとして機能する様に、記述してみます。まず、textBox1.AllowDrop は、true にする必要があります。常時ドロップ可能という前提の場合、設計時点で AllowDrop プロパティを true にしますが、ここではサンプルコードとして明示したいので、フォームの Load イベントでこれを設定する様にしています。
    そして、DragOver イベントでカーソル形状の設定処理を、DropDrop イベント処理で、受け取ったテキストを Text プロパティに設定する処理を行なっています。

    private void Form1_Load(object sender, System.EventArgs e)
    {
        // ドラッグ&ドロップを受入可能にする
        textBox1.AllowDrop = true;
    }
    
    private void textBox1_DragOver(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.UnicodeText))
        {
            if ((e.KeyState & 4) > 0)   // Shift キー押下
                e.Effect = DragDropEffects.Move;
            else
                e.Effect = DragDropEffects.Copy;
        }
    }
    
    private void textBox1_DragDrop(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.UnicodeText))
            textBox1.Text = e.Data.GetData(DataFormats.UnicodeText) as string;
    }
    

    この様な記述で、ドロップターゲットとして機能する様になります。ここで、DataFormat の指定には注意して下さい。DataFormat.Text は、ANSI文字列を指すので、特に GetData でこの指定にすると、Unicode 固有文字が入っていた場合に文字化けします。

    さて上記の様な記述では、ドロップ前にテキスト文字が存在すると、無条件で置換されてしまいます。これはちょっと不満です。本来だと、既存のテキストに対し、ドロップした位置にドラッグされたテキストを挿入するのが一般的だと思います。これは、C#2005 以降であれば以下のようにすれば実現出来ます。

    private void textBox1_DragDrop(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.UnicodeText))
        {
            string insertText = e.Data.GetData(DataFormats.UnicodeText) as string;
            int index = textBox1.GetCharIndexFromPosition(
                textBox1.PointToClient(new Point(e.X, e.Y)));
            textBox1.Text = textBox1.Text.Insert(index, insertText);
            textBox1.Select(index, insertText.Length);
        }
    }
    

    2-2.ドロップソースとなる TextBox の処理
    ドロップソース側では、TextBox に限らず、まずドラッグを開始したかという判断が重要になります。実は、このイベントは何故かありません(但し、一部のクラスには実装されています)。従って、まずドラッグ開始の判断処理を実装する必要があります。この判断処理ですが、DOBON.NETの「Drag&Dropを行う」にコード記述例があります。
    簡単に説明すると、マウスのボタンダウン位置を中心とする SystemInformation.DragSize の四角形範囲を越えて、ボタンダウンのまま移動した場合が、ドラッグ開始と判断します。
    それでは、テキストボックス(textBox2)をドロップソースとして、ドラッグ開始となる判断処理を記述してみます。若干、DOBON.NET の実装とは違いますが、私の場合以下の様にしています。

    private Rectangle dragRangeRectangle = Rectangle.Empty;
    private void textBox2_MouseDown(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)  // 左ボタンダウン
        {
            // ドラッグ開始判断となる四角形(dragRangeRectangle)の計算
            Size dragSize = SystemInformation.DragSize;
            dragRangeRectangle = new Rectangle(new Point(
                e.X - (dragSize.Width / 2),
                e.Y - (dragSize.Height / 2)), dragSize);
        }
    }
    
    private void textBox2_MouseMove(object sender, MouseEventArgs e)
    {
        // マウス左ボタン押下状態でなければ処理しない
        if (e.Button != MouseButtons.Left) return;
        // dragRangeRectangle 未設定なら処理しない
        if (dragRangeRectangle == Rectangle.Empty) return;
        // テキストがない場合はドラッグ処理しない
        if (textBox2.Text.Length == 0) return;
        // ドラッグ状態と判断する範囲を超えていない
        if (dragRangeRectangle.Contains(e.X, e.Y)) return;
        // ドラッグ開始の処理
        DragDropEffects dde = textBox2.DoDragDrop(textBox2.Text,
            DragDropEffects.All);
        // ドロップ後の処理
        if (dde == DragDropEffects.Move)    // 移動でドロップ処理された
            textBox2.Clear();               // テキストをクリア
        dragRangeRectangle = Rectangle.Empty;
    }
    
    private void textBox2_MouseUp(object sender, MouseEventArgs e)
    {
        dragRangeRectangle = Rectangle.Empty;
    }
    

    これで、すでにドラッグ&ドロップ出来る状態になっていますが、マウス右ボタンダウンで、ドラッグをキャンセルする様にする場合、QueryContinueDrag イベントで更に以下の記述を行ないます。なお、Esc キー入力でもドラッグキャンセルするのが標準ですが、こちらはデフォルトで行われる(DragAction.Cancel が既に設定された状態で、Esc キー入力による QueryContinueDrag イベントが発生する)ので、こちらは特に記述する必要はありません。

    private void textBox2_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
    {
        if ((e.KeyState & 2) > 0)   // マウス右ボタンダウン
            e.Action = DragAction.Cancel;
    }
    

    これで、とりあえずドラッグソースとしての記述は終了です。
    ここでは、自分のアプリ内でドロップソースとドロップターゲットの記述を行なっているので、ドラッグ&ドロップを自プロセスのみで完結出来ます。しかし、ここはOLEドラッグ&ドロップ対応なので、別プロセスの同じアプリケーションに対するドラッグ&ドロップや、テキストエディタ等全く別のアプリケーション等に対するドラッグ&ドロップも行なって、その動作を確認して下さい。

3.RichTextBox 内のテキストドラッグ&ドロップ
選択中テキストを対象にしたドラッグ&ドロップを行ないたい場合は、この RichTextBox を使用します。というのも、TextBox クラスでは、選択中のテキストがある状態でマウスのボタンダウンを行なうと、選択解除されてしまいます。また、元々ドラッグにより、テキストの選択処理が出来る機能があるので、テキストの選択を行なう為のドラッグか、OLEドラッグ&ドロップを開始する為のドラッグなのかを判断する必要も出て来ます。これらが解決出来ない限り無理です。

この様な理由から、TextBox での選択中テキストだけを対象にしたテキストドラッグ&ドロップは諦めた方が良いでしょう。しかも、RichTextBox では、C#2005 から何と EnableAutoDragDrop プロパティというものが存在しており、ドロップソース側としての記述はほとんど不要となります。
残念ながら C#2003 では、このプロパティがないので簡単には実装出来ません。また、GetCharIndexFromPosition プロパティも処理上は欲しいところですが、これもC#2003では存在しないので、かなりいろいろコードを書く必要があると思われます。
よって RichTextBox のドラッグ&ドロップは、C#2005 以降を前提として書きます。

話を元に戻して、RichTextBox では、何故かドラッグ&ドロップ関連のイベントが、プロパティウィンドウから設定出来ません。そこで、ここではフォームの Load イベント時に設定する様にしています。必要なドロップターゲットとしての処理は、TextBox とほぼ同じです。また、ドロップソースとしては、TextBox 同様右マウスボタンダウンで、ドラッグキャンセルする場合は、QueryContinueDrag イベントにて、同様の処理を行ないます。これらを実装した結果を示します。

private void Form1_Load(object sender, System.EventArgs e)
{
    // ドラッグ&ドロップを受入可能にする
    richTextBox1.AllowDrop = true;
    // ドロップソース側としてのドラッグ&ドロップ操作を有効にする
    richTextBox2.EnableAutoDragDrop = true;
    // ドロップターゲットとなる richTextBox1 のイベント設定
    richTextBox1.DragOver += new DragEventHandler(richTextBox1_DragOver);
    richTextBox1.DragDrop += new DragEventHandler(richTextBox1_DragDrop);
    // ドロップソースとなる richTextBox2 のイベント設定
    richTextBox2.QueryContinueDrag +=
        new QueryContinueDragEventHandler(richTextBox2_QueryContinueDrag);
}

private void richTextBox1_DragOver(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.UnicodeText))
    {
        if ((e.KeyState & 4) > 0)   // Shift キー押下
            e.Effect = DragDropEffects.Move;
        else
            e.Effect = DragDropEffects.Copy;
    }
}

private void richTextBox1_DragDrop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.UnicodeText))
    {
        string insertText = e.Data.GetData(DataFormats.UnicodeText) as string;
        int index = richTextBox1.GetCharIndexFromPosition(
            richTextBox1.PointToClient(new Point(e.X, e.Y)));
        richTextBox1.Text = richTextBox1.Text.Insert(index, insertText);
        richTextBox1.Select(index, insertText.Length);
    }
}

private void richTextBox2_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
    if ((e.KeyState & 2) > 0)   // マウス右ボタンダウン
        e.Action = DragAction.Cancel;
}

これだけで、TextBox とほぼ同等の機能を実現出来ます。もちろん、選択中テキストだけをドラッグ&ドロップ出来るとか、ドロップソース側は、実はドロップターゲットとしても機能する等の違いもありますが、実用上はこちらの方がいいので問題とはならないでしょう。
元々 TextBox と挙動が違う部分があったりするので、個人的には RichTextBox が嫌いだったのですが、見直しました。

そえから、C#2005から AllowDrop プロパティがプロパティウィンドウから設定出来なくなっていますが、どうも AllowDrop が false のままでも、EnableAutoDragDrop が true であれば、ドラッグ&ドロップを受け入れる様です。

4.DragDropEffects の設定
ここで、ドラッグ&ドロップ処理で指定する、DragDropEffects の指定について考えてみます。

まず、DoDragDrop で設定するパラメータは、DragDropEffects 効果として使用したいもの全てを指定する必要があります。ここで設定した内容が、ドロップターゲット側で発生する、 DragEnter / DragOver / DragDrop イベント中の DragEventArgs.AllowEffect に反映されます。つまり、DoDragDrop で指定していない DragDropEffects 効果は、後のイベントで 設定出来ません(設定しても無視されます)。ここで、DragDropEffects.All に、DragDropEffects.Link が含まれていない事に注意して下さい。今回はテキストを対象にしていますので、DragDropEffects.Link は使用していませんが、これが必要な場合は、

DoDragDrop(dataObject, DragDropEffects.All | DragDropEffects.Link);

といった記述をする必要があります。
次に、キー操作などで DragDropEffects 効果を変更する場合の指定ですが、これはドロップターゲットでしか指定出来ません。ドロップソース側では、GiveFeedback イベントにより、現在の Effect 効果を知る事は出来ますが、これは読み取り専用なので、変更は出来ません。
ただし、Effect 効果により、例えば自作カーソルを使い分けたりする様な指定は可能です。今回は、かなり長くなったので、このあたりは次回位にサンプルと作ってみようかと思います。

ここで、各 DragDropEffects のデフォルトカーソル形状を示します。

デフォルトカーソル形状一覧
DragDropEffects列挙値カーソル形状
None 0
Copy 1
Move 2
Link 4
Scroll -2147483648
(0x80000000)

ところで、DragDropEffects.Scroll ですが、私にはどうも意味合いが判りません。確かにドラッグでスクロールする動作はありふれた事ですが、別に DragDropEffects.Scroll を指定しないとスクロールしなくなる訳でもありません。@ITの.NET TIPS中に「ファイルやディレクトリをエクスプローラへドラッグ&ドロップするには?」で、DragDropEffects.Scroll について説明されている部分があります。しかし、どういう場合に使用するのか、ちょっとイメージがわかないです。使用価値はほぼないとみて、無視しててもいいものなのでしょうかね。

効果不明の DragDropEffects.Scroll の話はこれ位にして、以上でテキストのドラッグ&ドロップは終わりにしたいと思います。最後に、今回使用したサンプルソースを、ここでダウンロード出来るようにしておきます。
「DropText.cab」をダウンロード


« C#でカラーカーソル/アニメーションカーソルを使用する | トップページ | OLEドラッグ&ドロップ対応にする方法(ファイル編) »

C# Tips」カテゴリの記事

コメント

コメントを書く

コメントは記事投稿者が公開するまで表示されません。

(ウェブ上には掲載しません)

トラックバック


この記事へのトラックバック一覧です: OLEドラッグ&ドロップ対応にする(テキスト編):

« C#でカラーカーソル/アニメーションカーソルを使用する | トップページ | OLEドラッグ&ドロップ対応にする方法(ファイル編) »