Chapter 9. Drag and Drop

Drag and Drop

Drag and drop is a modern and intuitive way of transferring information within an application or between different applications. It is often provided in addition to clipboard support for moving and copying data.

In this chapter, we will see how to add drag and drop support to an application and how to handle custom formats. Then we will show how to reuse the drag and drop code to add clipboard support. This code reuse is possible because both mechanisms are based on QMimeData, a class that can provide data in several formats.

Enabling Drag and Drop

Drag and drop involves two distinct actions: dragging and dropping. Qt widgets can serve as drag sites, as drop sites, or as both.

Our first example shows how to make a Qt application accept a drag initiated by another application. The Qt application is a main window with a QTextEdit as its central widget. When the user drags a text file from the desktop or from a file explorer and drops it onto the application, the application loads the file into the QTextEdit.

Here’s the definition of the example’s MainWindow class:

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

protected:
    void dragEnterEvent(QDragEnterEvent *event);
    void dropEvent(QDropEvent *event);

private:
    bool readFile(const QString &fileName);
    QTextEdit *textEdit;
};

The MainWindow class reimplements dragEnterEvent() and dropEvent() from QWidget. Since the purpose of the example is to show drag and drop, much of the functionality we would expect to be in a main window class has been omitted.

MainWindow::MainWindow()
{
    textEdit = new QTextEdit;
    setCentralWidget(textEdit);

    textEdit->setAcceptDrops(false);
    setAcceptDrops(true);

    setWindowTitle(tr("Text Editor"));
}

In the constructor, we create a QTextEdit and set it as the central widget. By default, QTextEdit accepts textual drags from other applications, and if the user drops a file onto it, it will insert the file name into the text. Since drop events are propagated from child to parent, by disabling dropping on the QTextEdit and enabling it on the main window, we get the drop events for the whole window in MainWindow.

void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    if (event->mimeData()->hasFormat("text/uri-list"))
        event->acceptProposedAction();
}

The dragEnterEvent() is called whenever the user drags an object onto a widget. If we call acceptProposedAction() on the event, we indicate that the user can drop the drag object on this widget. By default, the widget wouldn’t accept the drag. Qt automatically changes the cursor to indicate to the user whether the widget is a legitimate drop site.

Here we want the user to be allowed to drag files, but nothing else. To do so, we check the MIME type of the drag. The MIME type text/uri-list is used to store a list of uniform resource identifiers (URIs), which can be file names, URLs (such as HTTP or FTP paths), or other global resource identifiers. Standard MIME types are defined by the Internet Assigned Numbers Authority (IANA). They consist of a type and a subtype separated by a slash. The clipboard and the drag and drop system use MIME types to identify different types of data. The official list of MIME types is available at http://www.iana.org/assignments/media-types/.

void MainWindow::dropEvent(QDropEvent *event)
{
    QList<QUrl> urls = event->mimeData()->urls();
    if (urls.isEmpty())
        return;

    QString fileName = urls.first().toLocalFile();
    if (fileName.isEmpty())
        return;

    if (readFile(fileName))
        setWindowTitle(tr("%1 - %2").arg(fileName)
                                    .arg(tr("Drag File")));
}

The dropEvent() is called when the user drops an object onto the widget. We call QMimeData::urls() to obtain a list of QUrls. Typically, users drag only one file at a time, but it is possible for them to drag multiple files by dragging a selection. If there is more than one URL, or if the URL is not a local file name, we return immediately.

QWidget also provides dragMoveEvent() and dragLeaveEvent(), but for most applications they don’t need to be reimplemented.

The second example illustrates how to initiate a drag and accept a drop. We will create a QListWidget subclass that supports drag and drop, and use it as a component in the Project Chooser application shown in Figure 9.1.

The Project Chooser application

Figure 9.1. The Project Chooser application

The Project Chooser application presents the user with two list widgets, populated with names. Each list widget represents a project. The user can drag and drop the names in the list widgets to move a person from one project to another.

All of the drag and drop code is located in the QListWidget subclass. Here’s the class definition:

class ProjectListWidget : public QListWidget
{
    Q_OBJECT

public:
    ProjectListWidget(QWidget *parent = 0);

protected:
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void dragEnterEvent(QDragEnterEvent *event);
    void dragMoveEvent(QDragMoveEvent *event);
    void dropEvent(QDropEvent *event);

private:
    void performDrag();

    QPoint startPos;
};

The ProjectListWidget class reimplements five event handlers declared in QWidget.

ProjectListWidget::ProjectListWidget(QWidget *parent)
    : QListWidget(parent)
{
    setAcceptDrops(true);
}

In the constructor, we enable drops on the list widget.

void ProjectListWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        startPos = event->pos();
    QListWidget::mousePressEvent(event);
}

When the user presses the left mouse button, we store the mouse position in the startPos private variable. We call QListWidget’s implementation of mousePressEvent() to ensure that the QListWidget has the opportunity to process mouse press events as usual.

void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        int distance = (event->pos() - startPos).manhattanLength();
        if (distance >= QApplication::startDragDistance())
            performDrag();
    }
    QListWidget::mouseMoveEvent(event);
}

When the user moves the mouse cursor while holding the left mouse button, we consider starting a drag. We compute the distance between the current mouse position and the position where the left mouse button was pressed—the “Manhattan length” is a quick-to-calculate approximation of the length of a vector from its origin. If the distance is greater than or equal to QApplication’s recommended drag start distance (normally four pixels), we call the private function performDrag() to start dragging. This avoids initiating a drag just because the user’s hand shakes.

void ProjectListWidget::performDrag()
{
    QListWidgetItem *item = currentItem();
    if (item) {
        QMimeData *mimeData = new QMimeData;
        mimeData->setText(item->text());

        QDrag *drag = new QDrag(this);
        drag->setMimeData(mimeData);
        drag->setPixmap(QPixmap(":/images/person.png"));
        if (drag->exec(Qt::MoveAction) == Qt::MoveAction)
            delete item;
    }
}

In performDrag(), we create an object of type QDrag with this as its parent. The QDrag object stores the data in a QMimeData object. For this example, we provide the data as a text/plain string using QMimeData::setText(). QMimeData provides several functions for handling the most common types of drags (images, URLs, colors, etc.) and can handle arbitrary MIME types represented as QByteArrays. The call to QDrag::setPixmap() sets the icon that follows the cursor while the drag is taking place.

The QDrag::exec() call starts the dragging operation and blocks until the user drops or cancels the drag. It takes a combination of supported “drag actions” as argument (Qt::CopyAction, Qt::MoveAction, and Qt::LinkAction) and returns the drag action that was executed (or Qt::IgnoreAction if none was executed). Which action is executed depends on what the source widget allows, what the target supports, and which modifier keys are pressed when the drop occurs. After the exec() call, Qt takes ownership of the drag object and will delete it when it is no longer required.

void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

The ProjectListWidget widget not only originates drags, but also accepts such drags if they come from another ProjectListWidget in the same application. QDragEnterEvent::source() returns a pointer to the widget that initiated the drag if that widget is part of the same application; otherwise, it returns a null pointer. We use qobject_cast<T>() to ensure that the drag comes from a ProjectListWidget. If all is correct, we tell Qt that we are ready to accept the action as a move action.

void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

The code in dragMoveEvent() is identical to what we did in dragEnterEvent(). It is necessary because we need to override QListWidget’s (actually, QAbstractItemView’s) implementation of the function.

void ProjectListWidget::dropEvent(QDropEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        addItem(event->mimeData()->text());
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

In dropEvent(), we retrieve the dragged text using QMimeData::text() and create an item with that text. We also need to accept the event as a “move action” to tell the source widget that it can now remove the original version of the dragged item.

Drag and drop is a powerful mechanism for transferring data between applications. But in some cases, it’s possible to implement drag and drop without using Qt’s drag and drop facilities. If all we want to do is to move data within one widget in one application, we can often simply reimplement mousePressEvent() and mouseReleaseEvent().

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:

  1. We can provide arbitrary data as a QByteArray using QMimeData::setData() and extract it later using QMimeData::data().

  2. We can subclass QMimeData and reimplement formats() and retrieveData() to handle our custom data types.

  3. 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("	", "", "");
    result.replace("
", ""
"");
    result.prepend(""");
    result.append(""");
    return result;
}

QString MyTableWidget::toHtml(const QString &plainText)
{
    QString result = Qt::escape(plainText);
    result.replace("	", "<td>");
    result.replace("
", "
<tr><td>");
    result.prepend("<table>
<tr><td>");
    result.append("
</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

Clipboard Handling

Most applications make use of Qt’s built-in clipboard handling in one way or another. For example, the QTextEdit class provides cut(), copy(), and paste() slots as well as keyboard shortcuts, so little or no additional code is required.

When writing our own classes, we can access the clipboard through QApplication::clipboard(), which returns a pointer to the application’s QClipboard object. Handling the system clipboard is easy: Call setText(), setImage(), or setPixmap() to put data onto the clipboard, and call text(), image(), or pixmap() to retrieve data from the clipboard. We have already seen examples of clipboard use in the Spreadsheet application from Chapter 4.

For some applications, the built-in functionality might not be sufficient. For example, we might want to provide data that isn’t just text or an image, or we might want to provide data in many different formats for maximum interoperability with other applications. The issue is very similar to what we encountered earlier with drag and drop, and the answer is also similar: We can subclass QMimeData and reimplement a few virtual functions.

If our application supports drag and drop through a custom QMimeData subclass, we can simply reuse the QMimeData subclass and put it on the clipboard using the setMimeData() function. To retrieve the data, we can call mimeData() on the clipboard.

On X11, it is usually possible to paste a selection by clicking the middle button of a three-button mouse. This is done using a separate “selection” clipboard. If you want your widgets to support this kind of clipboard as well as the standard one, you must pass QClipboard::Selection as an additional argument to the various clipboard calls. For example, here’s how we would reimplement mouseReleaseEvent() in a text editor to support pasting using the middle mouse button:

void MyTextEditor::mouseReleaseEvent(QMouseEvent *event)
{
    QClipboard *clipboard = QApplication::clipboard();
    if (event->button() == Qt::MidButton
            && clipboard->supportsSelection()) {
        QString text = clipboard->text(QClipboard::Selection);
        pasteText(text);
    }
}

On X11, the supportsSelection() function returns true. On other platforms, it returns false.

If we want to be notified whenever the clipboard’s contents change, we can connect the QClipboard::dataChanged() signal to a custom slot.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset