言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

Qt その4 ドラッグアンドドロップ

dndmodel

  • いろいろな色の名前が書いてあるリストがある。
  • ドラッグドロップによって順番を変える。

こういうのを作ります。Qt 標準の QStringListModel を使えば数行で出来てしまうのですが、Qt の設計を味わうためにわざわざモデルを作り直してみます。

メインプログラムはこんな感じ。これからモデルの MyModel を追加します。

// main.cpp
#include "MyModel.h"

int main(int argc, char **argv)
{
  QApplication app(argc, argv);

  // 色の名前の文字列リストを作り、MyModel に入れる。
  QStringList colors;
  colors << "Red" << "Orange" << "Yellow" << "Green" << "Blue";
  MyModel model(colors);

  // リストと、参考までにテーブルを作って QSplitter の中に並べる。
  QSplitter widget;
  QListView list(&widget);
  QTableView table(&widget);

  // リストとテーブルにモデルを登録する。
  list.setModel(&model);
  table.setModel(&model);

  // リストをドラッグアンドドロップ可能にする。
  list.setSelectionMode(QAbstractItemView::ExtendedSelection);
  list.setDragEnabled(true);
  list.setAcceptDrops(true);
  list.setDropIndicatorShown(true);
  list.setDragDropMode(QAbstractItemView::InternalMove);

  // 表示
  widget.show();
  return app.exec();
}

MyModel に実装するメソッドは次のヘッダファイルのようになります。

// MyModel.h
#include <QtGui>

// QAbstractListModel のサブクラスを定義する。
class MyModel : public QAbstractListModel
{
  // おまじない: Qt 拡張のシグナルやスロットが使えるようになる。
  Q_OBJECT
    
public:
  // コンストラクタは QStringList を受け取る。
  MyModel(const QStringList &strings, QObject *parent = 0);

  // 表示に必要なメソッド。
  int rowCount(const QModelIndex &parent = QModelIndex()) const;
  QVariant data(const QModelIndex &index, int role) const;

  // ドラッグアンドドロップに必要なメソッド。
  Qt::ItemFlags flags(const QModelIndex &index) const;
  bool setData(const QModelIndex &index, const QVariant &value,
               int role = Qt::EditRole);
  bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
  bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
  
private:
  QStringList _list;
};

特に、ドラッグアンドドロップに最低限必要なのは flags(), setData(), insertRows(), removeRows() の四つです。マニュアルには dropMimeData() を実装する事と書いてあるのですが、アプリ内でデータを動かすのには大げさです。Qt のソースを読むと、デフォルトの実装でドラッグ時にモデルの removeRows()、ドロップ時に insertRows() と setData() が呼ばれるようになっているので、アプリ内ドラッグアンドドロップにはこれらのメソッドを定義すると良いと分かります。

QAbstractListModel の実装を読むと、

  • insertRows() と setData() は QAbstractItemModel::decodeData() の中で、
  • removeRows() は QAbstractItemView::startDrag() の中で

呼ばれています。decodeData() から setData() を読む実装が面白くて、

bool QAbstractItemModel::setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles)
{
    bool b = true;
    for (QMap<int, QVariant>::ConstIterator it = roles.begin(); it != roles.end(); ++it)
        b = b && setData(index, it.value(), it.key());
    return b;
}

のようにモデルの中に存在する role を全部舐めてセットするようになっています。ここで、role というのはリスト要素の名前やアイコンなどの属性を表すEnum で、モデルに rule とインデックスを渡す事で属性の値を取って来れるようになっています。ここで例えば Javascript なんかだとオブジェクト自体を辞書として使えるので、このようにモデルによってあったり無かったりする属性を表現するのに最適なのですが、あいにく C++ のオブジェクトは柔軟ではないので Enum をキーとして使っています。

#include "MyModel.h"

MyModel::MyModel(const QStringList &strings, QObject *parent)
  : QAbstractListModel(parent)
{
  _list = strings;
}

// columnCount はデフォルトの実装があるので rowCount のみ実装します。
int MyModel::rowCount(const QModelIndex &) const
{
  return _list.count();
}

// DecorationRole(アイコン) EditRole(編集) DisplayRole(表示) の属性を返す。
QVariant MyModel::data(const QModelIndex &index, int role) const
{
  QString str  = _list.at(index.row());
  QColor color = QColor(str);

  if (index.column() == 0 && role == Qt::DecorationRole) return color;
  if (index.column() == 0 && role == Qt::EditRole)       return str;
  if (index.column() == 0 && role == Qt::DisplayRole)    return str;

  return QVariant();
}

// 編集可能かどうかを答える。
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
  return QAbstractItemModel::flags(index)
    | Qt::ItemIsEditable
    | Qt::ItemIsDragEnabled
    | Qt::ItemIsDropEnabled;
}

// Qt::EditRole か Qt::DisplayRole の時にリスト更新
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
  qDebug() << role << ":" << value;
  if (role == Qt::EditRole || role == Qt::DisplayRole) {
    _list.replace(index.row(), value.toString());
    emit dataChanged(index, index);
    return true;
  }
  return true;
}

bool MyModel::insertRows(int row, int count, const QModelIndex &parent)
{
    beginInsertRows(QModelIndex(), row, row + count - 1);
    for (int r = 0; r < count; ++r) _list.insert(row, QString());
    endInsertRows();

    return true;
}

bool MyModel::removeRows(int row, int count, const QModelIndex &parent)
{
    beginRemoveRows(QModelIndex(), row, row + count - 1);
    for (int r = 0; r < count; ++r) _list.removeAt(row);
    endRemoveRows();

    return true;
}

ソースコード: https://gist.github.com/1061993

Qt は web 上でソースが読めるので大変便利です。