9.2. Поддержка нестандартных типов данных при перетаскивании.

До сих пор мы имели дело с предопределенными типами перетаскиваемых объектов. Например, мы использовали QUriDrag, для перетаскивания файлов, и QTextDrag -- для текста. Оба этих класса являются наследниками QDragObject, который служит базой для всех перемещаемых объектов. В свою очредь, класс QDragObject наследует свойства абстрактного класса QMimeSource, предназначенного для хранения данных различных типов.

Если вы пожелаете перемещать объекты с текстовой информацией, с изображениями, с именами файлов или с информацией о цвете, то можно использовать предопределенные классы Qt: QTextDrag, QImageDrag, QUriDrag и QColorDrag. Но если вам необходимо перемещать нестандартные типы данных, то у вас есть два пути:

Класс QStoredDrag может хранить любые двоичные данные, что позволяет использовать его для любых типов MIME. Например, если вам потребуется перетащить некоторые данные, хранящиеся в файле формата (фиктивного) ASDF, то можно рекомендовать примерно такой код: void MyWidget::startDrag() { QByteArray data = toAsdf(); if (!data.isEmpty()) { QStoredDrag *drag = new QStoredDrag("octet-stream/x-asdf", this); drag->setEncodedData(data); drag->setPixmap(QPixmap::fromMimeSource("asdf.png")); drag->drag(); } } Однако, QStoredDrag имеет ряд неудобств. Одно из них заключается в том, что он может хранить только один MIME тип. Если мы предполагаем использовать механизм "drag and drop" только в пределах одного приложения, то это не является большой проблемой. Но когда необходимо реализовать взаимодействие между различными приложениями, то одного MIME типа, как правило бывает недостаточно.

Другое неудобство состоит в необходимости преобразования данных в QByteArray, даже если приемник не может принимать данные этого типа. При достаточно большом объеме данных, это может привести к неоправданной потере производительности. Было бы намного удобнее, если бы преобразование выполнялось в момент сброса перетаскиваемого объекта.

Решение этих двух проблем заключается в создании дочернего класса от QDragObject и реализации двух виртуальных методов format() и encodedData(), используемых Qt для получения сведений о перетаскиваемых объектах. Чтобы показать -- как это можно сделать, мы создадим класс CellDrag, который будет хранить данные из одной или нескольких ячеек таблицы QTable.

class CellDrag : public QDragObject { public: CellDrag(const QString &text, QWidget *parent = 0, const char *name = 0); const char *format(int index) const; QByteArray encodedData(const char *format) const; static bool canDecode(const QMimeSource *source); static bool decode(const QMimeSource *source, QString &str); private: QString toCsv() const; QString toHtml() const; QString plainText; }; Класс CellDrag порожден от класса QDragObject. В нем только две функции имеют прямое отношение к механизму "drag and drop" -- это format() и encodedData(). Дополнительно, только лишь для удобства, он предоставляет в распоряжение программиста статические функции canDecode() и decode(), которые извлекают данные в момент сброса. CellDrag::CellDrag(const QString &text, QWidget *parent, const char *name) : QDragObject(parent, name) { plainText = text; } Конструктору передается строка в текстовом виде, которая будет перемещаться. Это обычный текст, который может содержать символы табуляции и перевода строки. Этот текстовый тип мы использовали в Главе 4, когда добавляли в приложение Spreadsheet поддержку буфера обмена (см. раздел Реализация меню Edit). const char *CellDrag::format(int index) const { switch (index) { case 0: return "text/csv"; case 1: return "text/html"; case 2: return "text/plain"; default: return 0; } } Функция format() перекрывает метод родительского класса QMimeSource и возвращает различные MIME типы, поддерживаемые объектом при перетаскивании. В нашем примере поддерживаются три типа данных: CSV (от англ. Comma-Separated Values -- Данные, Разделенные Запятыми), HTML и простой текст.

Когда Qt пытается определить -- какой MIME тип поддерживается перетаскиваемым объектом, она вызывает format() с аргументом index, равным 0, 1, 2... и так до тех пор, пока format() не вернет пустой указатель. Типы MIME для CSV и HTML были взяты из официального списка, который вы найдете по адресу: http://www.iana.org/assignments/media-types/ .

Порядок следования форматов не имеет значения, однако, хорошей практикой считается помещать наиболее предпочтительные форматы в начало. Приложения, которые поддерживают несколько форматов, зачастую останавливаются на первом подходящем формате.

QByteArray CellDrag::encodedData(const char *format) const { QByteArray data; QTextOStream out(data); if (qstrcmp(format, "text/csv") == 0) { out << toCsv(); } else if (qstrcmp(format, "text/html") == 0) { out << toHtml(); } else if (qstrcmp(format, "text/plain") == 0) { out << plainText; } return data; } Функция encodedData() возвращает данные в заказанном формате. Аргумент format, обычно содержит одну из строк, которую возвращает функция format(), но мы не можем безоговорочно утверждать это, поскольку не все приложения проверяют тип MIME вызовом format(). В приложениях Qt такая проверка обычно выполняется вызовом provides() внутри QDragEnterEvent и QDragMoveEvent (как мы это видели ранее).

Для преобразования QString в QByteArray, лучше использовать QTextStream.

QString CellDrag::toCsv() const { QString out = plainText; out.replace("\\", "\\\\"); out.replace("\"", "\\\""); out.replace("\t", "\", \""); out.replace("\n", "\"\n\""); out.prepend("\""); out.append("\""); return out; } QString CellDrag::toHtml() const { QString out = QStyleSheet::escape(plainText); out.replace("\t", "<td>"); out.replace("\n", "\n<tr><td>"); out.prepend("<table>\n<tr><td>"); out.append("\n</table>"); return out; } Функции toCsv() и toHtml() выполняют преобразование символов табуляции и перевода строки в соответствующие элементы формата CSV и HTML. Например, данные Red Green Blue Cyan Yellow Magenta будут преобразованы в "Red", "Green", "Blue" "Cyan", "Yellow", "Magenta" или в <table> <tr><td>Red<td>Green<td>Blue <tr><td>Cyan<td>Yellow<td>Magenta </table> Преобразование выполняется простой заменой одних символов другими, с помощью QString::replace(). Для экранирования специальных символов HTML используется статическая функция QStyleSheet::escape(). bool CellDrag::canDecode(const QMimeSource *source) { return source->provides("text/plain"); } Функция canDecode() возвращает true, если перетаскиваемые данные могут быть декодированы, в противном случае возвращается false.

Хотя мы и предусматриваем поддержку трех форматов для перетаскиваемых данных, мы будем принимать только данные в простом текстовом виде, поскольку для наших нужд этого будет более чем достаточно. Если пользователь попытается переместить ячейки из QTable в HTML-редактор, то данные будут преобразованы в HTML-таблицу. Но если пользователь попробует переместить произвольную HTML-таблицу (например, из браузера) в QTable, то эти данные не будут восприняты приложением.

bool CellDrag::decode(const QMimeSource *source, QString &str) { QByteArray data = source->encodedData("text/plain"); str = QString::fromLocal8Bit((const char *)data, data.size()); return !str.isEmpty(); } И, наконец, функция decode() преобразует text/plain данные в QString. Здесь мы предполагаем, что используется 8-ми битная кодировка символов.

Если вы пожелаете точно указывать кодировку символов, для перемещаемых данных, вы можете задать параметр charset формата text/plain, напимер:

text/plain;charset=US-ASCII text/plain;charset=ISO-8859-1 text/plain;charset=Shift_JIS Итак. Мы закончили описание реализации класса CellDrag. Нам осталось только интегрировать его с QTable. Оказывается, класс QTable уже выполняет почти все, что нам нужно. Единственное, что нам остается сделать -- это вызвать setDragEnabled(true) в конструкторе и перекрыть метод QTable::dragObject(), который будет возвращать CellDrag: QDragObject *MyTable::dragObject() { return new CellDrag(selectionAsString(), this); } Мы не приводим текст функции selectionAsString(), поскольку он почти полностью совпадает с текстом функции Spreadsheet::copy().

Чтобы добавить поддержку приема данных, сбрасываемых на таблицу, необходимо перекрыть методы contentsDragEnterEvent() и contentsDropEvent() точно так же, как мы это делали в приложении "Project Chooser".