2012年2月3日金曜日

( Qt C++ )カスタムドラッグタイプを使用する②


はいそれでは表題の件やっていきます。

QMimeDataのサポートするものだけでプログラムを組んでいければ楽この上ないですが、しかしサポート外の独自データを含めたい場合があるかと思います。そんな時にこの記事は役に立つかと思います。

前回の第1回ではsetDataに任意のデータをQByteArrayに変換して格納していましたが、これは簡単な反面、ドラッグを開始した段階で変換を始めるため、ドラッグを拒否されたときこの変換動作は無駄となります。さらに変換データが多いときなどはアプリケーションの速度低下の原因となるという欠点があるそうです。
今回のやり方はドラッグ開始前の変換作業が必要なく、QMimeData::text(),data()など呼ばれた段階で各タイプに変換したデータを返せるので、欠点を最小限に抑えられます。

今回はC++ GUI Programming with Qt4 211ページの"Supporting Custom Drag Types"のところの2,3番目の項目をやっていく形となります。

2. We can subclass QMimeData and reimplement formats() and retrieveData()
to handle our custom data types.
(訳)
我々は、サブクラスとしてQMimeDataのformats()とretrieveData()再実装し、カスタムデータ型を処理できる。

3. For drag and drop operations within a single application, we can subclass
QMimeData and store the data using any data structure we want.
(訳)
単一のアプリケーション内でドラッグ&ドロップ操作では、我々は、QMimeDataをサブクラス化することができます。そして我々が望む任意のデータ構造を使用してデータを格納できます。

の部分です。

サンプルはC++ GUI Programming with Qt4 213ページからのものを参考にかなり変更して書いていきます。

そして、いつものようにQtCreaterの使用を前提とします。(QtCreaterなどの使い方は ”Qtをはじめよう" を見てください。)

ではさっそくコードを

(listmimedata.h)
#include <QListWidget>
#include <QMimeData>
#include <QStringList>

class ListMimeData : public QMimeData
{
    Q_OBJECT//マクロ
    
public:
    explicit ListMimeData(const QListWidgetItem *listwidgetitem);//コンストラクタ
    const QListWidgetItem *listWidgetItem() const { return myListWidgetItem; }
    QStringList formats() const { return myFormats; }//オーバーライド

protected:
    //オーバーライド
    QVariant retrieveData(const QString &mimetype, QVariant::Type preferredType) const;

private:
    const QListWidgetItem* myListWidgetItem;
    QStringList myFormats;
};

(listmimedata.cpp)
#include "listmimedata.h"

ListMimeData::ListMimeData(const QListWidgetItem *listwidgetitem)
{
    myListWidgetItem = listwidgetitem;
    myFormats << "text/csv" << "text/html" << "text/plain";
}

QVariant ListMimeData::retrieveData(const QString &mimetype,
                                    QVariant::Type preferredType) const
{
    if(mimetype == "text/plain")
    {
        return QString("%1%2").arg("text/plain!! : ").arg(myListWidgetItem->text());
    }
    else if(mimetype == "text/html")
    {
        return QString("%1%2").arg("text/html!! : ").arg(myListWidgetItem->text());
    }
    else if(mimetype == "text/csv")
    {
        return QString("%1%2").arg("text/csv!! : ").arg(myListWidgetItem->text());
    }
    else
    {
        return QMimeData::retrieveData(mimetype, preferredType);
    }
}

はい簡単ですね。見てのとおり独自データ処理・格納を行うためのQMimeDataのサブクラスです。

まずヘッダ部から説明します。
コンストラクタ、フィールドはいいでしょう。見たまんまです。重要なのはQMimeDataのformats()とretrieveData()をオーバーライドしているというところだけです。

次にcppを説明します。
まずコンストラクタですが、引数としてQListWidgetItemへのポインタを指定するようにしています。このポインタはプライベートフィールドmyListWidgetItemへセットされます。これはドラッグ&ドロップの際、直接ポインタから値を取り出すことができて効率がいいからだそうです。(QByteArrayに変換するよりは効率的。)
また、コンストラクタ内ではmyFormatsにこのカスタムMimeDataで使用できるmimeタイプをセットしています。

次にretrieveData()ですがここで引数に指定されたmimetypeに対する処理を返します。ここで独自の値を返すことができます。サンプルではQListWidgetItemにセットされているtextに呼ばれたときに格納されているmimetypeを文字列として追加し返しています。myFormatsに設定していないmimeタイプの場合はQMimeDataの通常の処理を返すようにしています。
このretrieveDataはprotectedなので外から呼び出すことはできません。ではどう使われるのかというとlistMimeData->text()とかlistMimeData->data("text/plain")だとかを呼び出すとlistMimeDataの内部で呼び出され、カスタムデータが返ってくるという形になります。
(QMimeDataのリファレンス)

次はこのカスタムMimeDataを利用するカスタムQListWidget側のコードです。

(custumlist.h)
#include <QListWidget>
#include <QMouseEvent>
#include <QApplication>
#include <QTextDocument>
#include "listmimedata.h"

class CustumList : public QListWidget
{
    Q_OBJECT//マクロ

public:
    explicit CustumList(QWidget *parent = 0);//コンストラクタ

protected:
    void mousePressEvent(QMouseEvent *e);//マウス左をクリックしたら呼ばれる
    void mouseMoveEvent(QMouseEvent *e);//マウスカーソルを移動したら呼ばれる
    void dragEnterEvent(QDragEnterEvent *e);//ドラッグされたものが来たら呼ばれる
    void dragMoveEvent(QDragMoveEvent *e);//ドラッグ&ドロップが操作されている間呼ばれる
    void dropEvent(QDropEvent *e);//ドロップの動作

private:
    void startDrag();//自分のウィジットでドラッグを開始した場合
    QPoint startPos;//マウスの位置
};

(custumlist.cpp)
#include "custumlist.h"

CustumList::CustumList(QWidget *parent) :
    QListWidget(parent)//コンストラクタ
{
    //ドラッグ&ドロップを受け付ける
    //trueで許可。
    setAcceptDrops(true);
}

void CustumList::mousePressEvent(QMouseEvent *e)
{
    //マウス左ボタンをクリックしたら
    if(e->button() == Qt::LeftButton)
    {
        //そのマウスの位置を保存
        startPos = e->pos();
    }

    //デフォルトの動作へ
    QListWidget::mousePressEvent(e);
}

void CustumList::mouseMoveEvent(QMouseEvent *e)
{
    ////マウス左ボタンをクリック中なら
    if(e->buttons() & Qt::LeftButton)
    {
        //保存していたマウスの位置と現在のマウスの位置の差を求める
        //manhattanLengthは絶対値を求めるもの
        int distance = (e->pos() - startPos).manhattanLength();

        //上で求めた差分とアプリで設定してあるドラッグ&ドロップを開始する差分
        //を比べ、それ以上ならドラッグを開始する。
        //なおstartDragDistance()はデフォルトでは4が返る
        //変更はsetStartDragDistance(int)
        if(distance >= QApplication::startDragDistance())
        {
            startDrag();
        }

        QListWidget::mouseMoveEvent(e);
    }
}

void CustumList::startDrag()
{
    ListMimeData *mimeData = new ListMimeData(currentItem());

    QDrag *drag = new QDrag(this);
    drag->setMimeData(mimeData);

    if (drag->start(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction)
        delete currentItem();
}

void CustumList::dragEnterEvent(QDragEnterEvent *e)
{
    CustumList *source =
            qobject_cast<CustumList *>(e->source());

    //自分意外からのドラック&ドロップなら
    if (source && source != this)
    {
        e->setDropAction(Qt::MoveAction);
        e->accept();
    }
}

void CustumList::dragMoveEvent(QDragMoveEvent *e)
{
    CustumList *source =
            qobject_cast<CustumList *>(e->source());

    //自分意外からのドラック&ドロップなら
    if (source && source != this)
    {
        e->setDropAction(Qt::MoveAction);
        e->accept();
    }
}

void CustumList::dropEvent(QDropEvent *e)
{
    CustumList *source =
            qobject_cast<CustumList *>(e->source());

    //自分意外からのドラック&ドロップなら
    if (source && source != this)
    {
        const ListMimeData *listMimeData =
            qobject_cast<const ListMimeData *>(e->mimeData());
        //自分のアイテムへドロップされたものを追加
        if(listMimeData)
        {
            addItem(QString("userData : %1").arg(listMimeData->listWidgetItem()->text()));
            addItem(QString("text/plain : %1").arg(listMimeData->text()));
            addItem(QString("text/csv : %1").arg((QString)listMimeData->data("text/csv")));
            addItem(QString("text/html : %1").arg(listMimeData->html()));
            e->setDropAction(Qt::MoveAction);
            e->accept();
        }
    }
}

はい長いですね。しかし、実は簡単です。見るべきところはstartDrag()とdropEvent()です。あとは( Qt C++ )カスタムウィジットでドラック&ドロップ(QListWidget派生)とその前後の記事と同じなのでそちらを見てください。

ヘッダ部は説明を省略します。
cpp部はstartDrag()とdropEvent()だけ説明します。

まずstartDrag()です。QByteArrayや各タイプへの変換作業と格納作業がないためかなりシンプルなコードになっています。
最初にカスタムMIMEDataであるListMimeDataを作成します。引数には現在リストで選択中のQListWidetItemをセットしていします。(currentItem()で選択中のQListWidetItemが返る)
あとはQDragを作成し、作成したListMimeDataをセットしてドラッグ&ドロップを開始するだけです。

次はdropEvent()です。
まず、同ウィジットで自分以外からのドラック&ドロップであるかを確認するためにCustumListへのキャストを行いthis(自身)と比較しています。自分以外からであれば処理を進めます。
次にeのmimeData()をListMimeDataにキャストします。キャスト成功なら処理を進めaddItemで各データをセットしていきます。


これらを実行すると以下のようになります。


(ドラッグ&ドロップ実行前)


(ドラッグ&ドロップ実行後)

以上です。

※なおアイテムの追加は以下のような感じでセットしました。
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)//uiにはGUI部品を記述(CustumListもこの中に記述)
{
    ui->setupUi(this);//uiのGUI部品の初期化

    //ui->listWidgetはCustumListです。
    ui->listWidget->addItem(new QListWidgetItem("Banana"));
}