- Enabling Drag and Drop
- Supporting Custom Drag Types
- Clipboard Handling
Supporting Custom Drag Types
In the examples so far, we have relied on QMimeData's support for common MIME types. Thus, we called QMimeData::setText() to create a text drag, and we used QMimeData:urls() to retrieve the contents of a text/uri-list drag. If we want to drag plain text, HTML text, images, URLs, or colors, we can use QMimeData without formality. But if we want to drag custom data, we must choose among the following alternatives:
- We can provide arbitrary data as a QByteArray using QMimeData::setData() and extract it later using QMimeData::data().
- We can subclass QMimeData and reimplement formats() and retrieveData() to handle our custom data types.
- For drag and drop operations within a single application, we can subclass QMimeData and store the data using any data structure we want.
The first approach does not involve any subclassing, but does have some drawbacks: We need to convert our data structure to a QByteArray even if the drag is not ultimately accepted, and if we want to provide several MIME types to interact nicely with a wide range of applications, we need to store the data several times (once per MIME type). If the data is large, this can slow down the application needlessly. The second and third approaches can avoid or minimize these problems. They give us complete control and can be used together.
To show how these approaches work, we will show how to add drag and drop capabilities to a QTableWidget. The drag will support the following MIME types: text/plain, text/html, and text/csv. Using the first approach, starting a drag looks like this:
void MyTableWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) performDrag(); } QTableWidget::mouseMoveEvent(event); } void MyTableWidget::performDrag() { QString plainText = selectionAsPlainText(); if (plainText.isEmpty()) return; QMimeData *mimeData = new QMimeData; mimeData->setText(plainText); mimeData->setHtml(toHtml(plainText)); mimeData->setData("text/csv", toCsv(plainText).toUtf8()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); if (drag->exec(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction) deleteSelection(); }
The performDrag() private function is called from mouseMoveEvent() to start dragging a rectangular selection. We set the text/plain and text/html MIME types using setText() and setHtml(), and we set the text/csv type using setData(), which takes an arbitrary MIME type and a QByteArray. The code for the selectionAsString() is more or less the same as the Spreadsheet::copy() function from Chapter 4 (p. 87).
QString MyTableWidget::toCsv(const QString &plainText) { QString result = plainText; result.replace("\\", "\\\\"); result.replace("\"", "\\\""); result.replace("\t", "\", \""); result.replace("\n", "\"\n\""); result.prepend("\""); result.append("\""); return result; } QString MyTableWidget::toHtml(const QString &plainText) { QString result = Qt::escape(plainText); result.replace("\t", "<td>"); result.replace("\n", "\n<tr><td>"); result.prepend("<table>\n<tr><td>"); result.append("\n</table>"); return result; }
The toCsv() and toHtml() functions convert a "tabs and newlines" string into a CSV (comma-separated values) or an HTML string. For example, the data
Red Green Blue Cyan Yellow Magenta
is converted to
"Red", "Green", "Blue" "Cyan", "Yellow", "Magenta"
or to
<table> <tr><td>Red<td>Green<td>Blue <tr><td>Cyan<td>Yellow<td>Magenta </table>
The conversion is performed in the simplest way possible, using QString::replace(). To escape HTML special characters, we use Qt::escape().
void MyTableWidget::dropEvent(QDropEvent *event) { if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); ... event->acceptProposedAction(); } }
Although we provide the data in three different formats, we accept only two of them in dropEvent(). If the user drags cells from a QTableWidget to an HTML editor, we want the cells to be converted into an HTML table. But if the user drags arbitrary HTML into a QTableWidget, we don't want to accept it.
To make this example work, we also need to call setAcceptDrops(true) and setSelectionMode(ContiguousSelection) in the MyTableWidget constructor.
We will now redo the example, but this time we will subclass QMimeData to postpone or avoid the (potentially expensive) conversions between QTableWidgetItems and QByteArray. Here's the definition of our subclass:
class TableMimeData : public QMimeData { Q_OBJECT public: TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range); const QTableWidget *tableWidget() const { return myTableWidget; } QTableWidgetSelectionRange range() const { return myRange; } QStringList formats() const; protected: QVariant retrieveData(const QString &format, QVariant::Type preferredType) const; private: static QString toHtml(const QString &plainText); static QString toCsv(const QString &plainText); QString text(int row, int column) const; QString rangeAsPlainText() const; const QTableWidget *myTableWidget; QTableWidgetSelectionRange myRange; QStringList myFormats; };
Instead of storing actual data, we store a QTableWidgetSelectionRange that specifies which cells are being dragged and keep a pointer to the QTableWidget. The formats() and retrieveData() functions are reimplemented from QMimeData.
TableMimeData::TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range) { myTableWidget = tableWidget; myRange = range; myFormats << "text/csv" << "text/html" << "text/plain"; }
In the constructor, we initialize the private variables.
QStringList TableMimeData::formats() const { return myFormats; }
The formats() function returns a list of MIME types provided by the MIME data object. The precise order of the formats is usually irrelevant, but it's good practice to put the "best" formats first. Applications that support many formats will sometimes use the first one that matches.
QVariant TableMimeData::retrieveData(const QString &format, QVariant::Type preferredType) const { if (format == "text/plain") { return rangeAsPlainText(); } else if (format == "text/csv") { return toCsv(rangeAsPlainText()); } else if (format == "text/html") { return toHtml(rangeAsPlainText()); } else { return QMimeData::retrieveData(format, preferredType); } }
The retrieveData() function returns the data for a given MIME type as a QVariant. The value of the format parameter is normally one of the strings returned by formats(), but we cannot assume that, since not all applications check the MIME type against formats(). The getter functions text(), html(), urls(), imageData(), colorData(), and data() provided by QMimeData are implemented in terms of retrieveData().
The preferredType parameter gives us a hint about which type we should put in the QVariant. Here, we ignore it and trust QMimeData to convert the return value into the desired type, if necessary.
void MyTableWidget::dropEvent(QDropEvent *event) { const TableMimeData *tableData = qobject_cast<const TableMimeData *>(event->mimeData()); if (tableData) { const QTableWidget *otherTable = tableData->tableWidget(); QTableWidgetSelectionRange otherRange = tableData->range(); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); ... event->acceptProposedAction(); } QTableWidget::mouseMoveEvent(event); }
The dropEvent() function is similar to the one we had earlier in this section, but this time we optimize it by first checking whether we can safely cast the QMimeData object to a TableMimeData. If the qobject_cast<T>() works, this means the drag was originated by a MyTableWidget in the same application, and we can directly access the table data instead of going through QMimeData's API. If the cast fails, we extract the data the standard way.
In this example, we encoded the CSV text using the UTF-8 encoding. If we want to be certain of using the right encoding, we could use the charset parameter of the text/plain MIME type to specify an explicit encoding. Here are a few examples:
text/plain;charset=US-ASCII text/plain;charset=ISO-8859-1 text/plain;charset=Shift_JIS text/plain;charset=UTF-8