Поддержка пользовательских типов переносимых объектов
Поддержка пользовательских типов переносимых объектов
До сих пор в представленных примерах мы полагались на поддержку QMimeData распространенных типов MIME. Так, мы вызывали QMimeData::setText() для создания объекта переноса текста и использовали QMimeData:urls() для получения содержимого объекта переноса типа text/uri-list. Если мы хотим перетаскивать обычный текст, текст в формате HTML, изображения, адреса URL или цвета, мы можем спокойно использовать класс QMimeData. Но если мы хотим перетаскивать пользовательские данные, необходимо сделать выбор между следующими альтернативами:
1. Мы можем обеспечить произвольные данные в виде массива QByteArray, используя функцию QMimeData::setData(), и извлекать их позже, используя функцию QMimeData::data().
2. Мы можем создать подкласс QMimeData и переопределить функции formats() и retrieveData() для обработки наших пользовательских типов данных.
3. Для выполнения операций механизма «drag-and-drop» в рамках одного приложения мы можем создать подкласс QMimeData и хранить данные в любых структурах данных.
Первый подход не требует никаких подклассов, но имеет некоторые недостатки: нам необходимо преобразбвать наши структуры данных в тип QByteArray, даже если переносимый объект не принимается, а если требуется обеспечить несколько MIME—типов, чтобы можно было хорошо взаимодействовать с самыми разными приложениями, нам придется сохранять несколько копий данных (по одной на каждый тип MIME). Если данные имеют большой размер, это может излишне замедлять работу приложения. При использовании второго и третьего подходов можно избежать или свести к минимуму эти проблемы. В этом случае мы получаем полное управление и можем использовать эти два подхода совместно.
Для демонстрации этих подходов мы покажем, как можно добавить возможности технологии «drag-and-drop» в виджет QTableWidget. Будет поддерживаться перенос следующих типов MIME: text/plain, text/html и text/csv. При применении первого подхода инициирование переноса выглядит следующим образом:
01 void MyTableWidget::mouseMoveEvent(QMouseEvent *event)
02 {
03 if (event->buttons() & Qt::LeftButton) {
04 int distance = (event->pos() - startPos).manhattanLength();
05 if(distance >= QApplication::startDragDistance())
06 startDrag();
07 }
08 QTableWidget::mouseMoveEvent(event);
09 }
10 void MyTableWidget::startDrag()
11 {
12 QString plainText= selectionAsPlainText();
13 if (plainText.isEmpty())
14 return;
15 QMimeData *mimeData = new QMimeData;
16 mimeData->setText(plainText);
17 mimeData->setHtml(toHtml(plainText));
18 mimeData->setData("text/csv", toCsv(plainText).toUtf8());
19 QDrag *drag = new QDrag(this);
20 drag->setMimeData(mimeData);
21 if (drag->start(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction)
22 deleteSelection();
23 }
Закрытая функция startDrag() вызывается из mouseMoveEvent() для инициирования переноса выделенной прямоугольной области. Мы устанавливаем типы MIME text/plain и text/html, используя функции setText() и setHtml(), а тип text/csv мы устанавливаем функцией setData(), которая принимает произвольный тип MIME и массив QByteArray. Программный код для функции selectionAsString() более или менее совпадает с кодом функции Spreadsheet::copy(), рассмотренной в главе 4.
01 QString MyTableWidget::toCsv(const QString &plainText)
02 {
03 QString result = plainText;
04 result.replace("\", "\\");
05 result.replace(""", "\"");
06 result.replace(" ", "", "")
07 result.replace(" ", "" "");
08 result.prepend(""");
09 result.append(""");
10 return result;
11 }
12 QString MyTableWidget::toHtml(const QString &plainText)
13 {
14 QString result = Qt::escape(plainText);
15 result.replace(" ", "<td>");
16 result.replace(" ", " <tr><td>");
17 result.prepend("<table> <tr><td>");
18 result.append(" </table>");
19 return result;
20 }
Функции toCsv() и toHtml() преобразуют строку со знаками табуляции и конца строки в формат CSV (comma—separated values — значения, разделенные запятыми) и 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 мы используем функцию Qt::escape().
01 void MyTableWidget::dropEvent(QDropEvent *event)
02 {
03 if (event->mimeData()->hasFormat("text/csv")) {
04 QByteArray csvData = event->mimeData()->data("text/csv");
05 QString csvText = QString::fromUtf8(csvData);
06 …
07 event->acceptProposedAction();
08 } else if (event->mimeData()->hasFormat("text/plain")) {
09 QString plainText = event->mimeData()->text();
10 …
11 event->acceptProposedAction();
12 }
13 }
Хотя мы предоставляем данные в трех разных форматах, мы принимаем в dropEvent() только два из них. Если подьзователь переносит ячейки из таблицы QTableWidget в редактор HTML, нам нужно, чтобы ячейки были преобразованы в таблицу HTML. Но если пользователь переносит произвольный текст HTML в таблицу QTableWidget, мы не станем его принимать.
Для того чтобы этот пример заработал, нам потребуется также вызвать setAcceptDrops(true) и setSelectionMode(ContiguousSelection) в конструкторе MyTableWidget.
Теперь мы переделаем этот пример, но на этот раз мы создадим подкласс QMimeData, чтобы отложить или избежать (потенциально затратных) преобразований между элементами QTableWidgetltem и массивом QByteArray. Ниже приводится определение нашего подкласса:
01 class TableMimeData : public QMimeData
02 {
03 Q_OBJECT
04 public:
05 TableMimeData(const QTableWidget *tableWidget,
06 const QTableWidgetSelectionRange &range);
07 const QTableWidget *tableWidget() const
08 { return myTableWidget; }
09 QTableWidgetSelectionRange range() const { return myRange; }
10 QStringList formats() const;
11 protected:
12 QVariant retrieveData(const QString &format,
13 QVariant::Type preferredType) const;
14 private:
15 static QString toHtml(const QString &plainText);
16 static QString toCsv(const QString &plainText);
17 QString text(int row, int column) const;
18 QString rangeAsPlainText() const;
19 const QTableWidget *myTableWidget;
20 QTableWidgetSelectionRange myRange;
21 QStringList myFormats;
22 };
Вместо реальных данных мы храним объект QTableWidgetSelectionRange, который определяет область переносимых ячеек и сохраняет указатель на QTableWidget. Функции formats() и retrieveData() класса QMimeData переопределяются.
01 TableMimeData::TableMimeData(const QTableWidget *tableWidget,
02 const QTableWidgetSelectionRange &range)
03 {
04 myTableWidget = tableWidget;
05 myRange = range;
06 myFormats << "text/csv" << "text/html" << "text/plain";
07 }
В конструкторе мы инициализируем закрытые переменные.
01 QStringList TableMimeData::formats() const
02 {
03 return myFormats;
04 }
Функция formats() возвращает список MIME—типов, находящихся в объекте MIME—данных. Последовательность форматов обычно несущественна, однако на практике желательно первыми указывать «лучшие» форматы. Приложения, поддерживающие несколько форматов, иногда будут использовать первый подходящий.
01 QVariant TableMimeData::retrieveData(const QString &format,
02 QVariant::Type preferredType) const
03 {
04 if (format == "text/plain") {
05 return rangeAsPlainText();
06 } else if (format =="text/csv") {
07 return toCsv(rangeAsPlainText()); }
08 else if (format == "text/html") {
09 return toHtml(rangeAsPlainText());
10 } else {
11 return QMimeData::retrieveData(format, preferredType);
12 }
13 }
Функция retrieveData() возвращает данные для заданного MIME—типа в виде объекта QVariant. Параметр format обычно содержит одну из строк, возвращенных функцией formats(), однако нам не следует на это рассчитывать, поскольку не все приложения проверяют MIME—тип на соответствие форматам функции formats(). Предусмотренные в классе QMimeData функции получения данных text(), html(), urls(), imageData(), colorData() и data() реализуются с помощью функции retrieveData().
Параметр preferredType определяет тип, который следует поместить в объект QVariant. Здесь мы его игнорируем и рассчитываем на то, что QMimeData преобразует при необходимости возвращенное значение в требуемый тип.
01 void MyTableWidget::dropEvent(QDropEvent *event)
02 {
03 const TableMimeData *tableData =
04 qobject_cast<const TableMimeData *>(event->mimeData());
05 if (tableData) {
06 const QTableWidget *otherTable = tableData->tableWidget();
07 QTableWidgetSelectionRange otherRange = tableData->range();
08 …
09 event->acceptProposedAction();
10 } else if (event->mimeData()->hasFormat("text/csv")) {
11 QByteArray csvData = event->mimeData()->data("text/csv");
12 QString csvText = QString::fromUtf8(csVData);
13 …
14 event->acceptProposedAction();
15 } else if (event->mimeData()->hasFormat("text/plain")) {
16 QString plainText = event->mimeData()->text();
17 …
18 event->acceptProposedAction();
19 }
20 QTableWidget::mouseMoveEvent(event);
21 }
Функция dropEvent() аналогична функции с тем же названием, которую мы рассматривали ранее в данном разделе, но на этот раз мы ее оптимизируем, делая вначале проверку возможности приведения типа QMimeData в тип TableMimeData. Если qobject_cast<T>() срабатывает, это значит, что перенос был инициирован виджетом MyTableWidget, расположенным в том же самом приложении, и мы можем получить непосредственный доступ к данным таблицы вместо того, чтобы пробираться сквозь программный интерфейс класса QMimeData. Если приведение типов оказывается неудачным, мы извлекаем данные стандартным способом.
В этом примере мы кодировали CSV—текст, используя кодировку UTF-8. Если бы мы хотели быть уверенными в применении правильной кодировки, мы могли бы использовать параметр charset в MIME—типе text/plain для явного задания типа кодировки. Ниже приводится несколько примеров:
text/plain; charset=US-ASCII
text/plain; charset=ISO-8859-1
text/plain; charset=Shift_JIS
text/plain; charset=UTF-8