Enabling Drag and Drop
Drag and drop involves two distinct actions: dragging and dropping. Widgets can serve as drag sites, as drop sites, or as both.
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 you want to do is to move data within one widget in one application, it is usually simpler to reimplement the widget's mouse event handlers. This is the approach we took in the DiagramView widget in Chapter 8 (p. 190).
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 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 MainWindow class:
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0, const char *name = 0); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QString strippedName(const QString &fullFileName); 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(QWidget *parent, const char *name) : QMainWindow(parent, name) { setCaption(tr("Drag File")); textEdit = new QTextEdit(this); setCentralWidget(textEdit); textEdit->viewport() ->setAcceptDrops (false); setAcceptDrops(true); }
In the constructor, we create a QTextEdit and set it as the central widget. We disable dropping on the QTextEdit's viewport and enable dropping on the main window.
The reason we must disable dropping on the QTextEdit is that we want to take over drag and drop handling ourselves in our MainWindow subclass. 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 we want to drop the entire contents of the file rather than the file's name, we cannot make use of QTextEdit's drag and drop functionality and must implement our own.
Because drop events are propagated from child to parent, we get the drop events for the whole main window, including those for the QTextEdit, in MainWindow.
void MainWindow::dragEnterEvent(QDragEnterEvent *event) { event->accept(QUriDrag::canDecode(event)); }
The dragEnterEvent() is called whenever the user drags an object onto a widget. If we call accept(true) on the event, we indicate that the user can drop the drag object on this widget; if we call accept(false), we indicate that the widget can't accept the drag. Qt automatically changes the cursor to indicate to the user whether or not 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 ask QUriDrag, the Qt class that handles file drags, whether it can decode the dragged object. The class can more generally be used for any universal resource identifier (URI), such as HTTP and FTP paths; hence the name QUriDrag.
void MainWindow::dropEvent(QDropEvent *event) { QStringList fileNames; if (QUriDrag::decodeLocalFiles(event, fileNames)) { if (readFile(fileNames[0])) setCaption(tr("%1 - Drag File") .arg(strippedName(fileNames[0]))); } }
The dropEvent() is called when the user drops an object onto the widget. We call the static function QUriDrag::decodeLocalFiles() to get a list of file names dragged by the user and read in the first file in the list. (The second argument is passed as a non-const reference.) Typically, users only drag one file at a time, but it is possible for them to drag multiple files by dragging a selection.
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 QListBox subclass that supports drag and drop, and use it as a component in the Project Chooser application shown in Figure 9.1.
Figure 9.1. The Project Chooser application
The Project Chooser application presents the user with two list boxes, populated with names. Each list box represents a project. The user can drag and drop the names in the list boxes to move a person from one project to another.
The drag and drop code is all located in the QListBox subclass. Here's the class definition:
class ProjectView : public QListBox { Q_OBJECT public: ProjectView(QWidget *parent, const char *name = 0); protected: void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void contentsDragEnterEvent(QDragEnterEvent *event); void contentsDropEvent(QDropEvent *event); private: void startDrag(); QPoint dragPos; };
ProjectView reimplements four of the event handlers declared in QScrollView (QListBox's base class).
ProjectView::ProjectView(QWidget *parent, const char *name) : QListBox(parent, name) { viewport()->setAcceptDrops(true); }
In the constructor, we enable drops on the QScrollView viewport.
void ProjectView::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) dragPos = event->pos(); QListBox::contentsMousePressEvent(event); }
When the user presses the left mouse button, we store the mouse position in the dragPos private variable. We call QListBox's implementation of contentsMousePressEvent() to ensure that QListBox has the opportunity to process mouse press events as usual.
void ProjectView::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) { int distance = (event->pos() - dragPos).manhattanLength(); if (distance > QApplication::startDragDistance()) startDrag(); } QListBox::contentsMouseMoveEvent(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.
If the distance is larger than QApplication's recommended drag start distance (normally 4 pixels), we call the private function startDrag() to start dragging. This avoids initiating a drag just because the user's hand shakes.
void ProjectView::startDrag() { QString person = currentText(); if (!person.isEmpty()) { QTextDrag *drag = new QTextDrag(person, this); drag->setSubtype("x-person"); drag->setPixmap(QPixmap::fromMimeSource("person.png")); drag->drag(); } }
In startDrag(), we create an object of type QTextDrag with this as its parent. The QTextDrag class represents a drag and drop object for transferring text. It is one of several predefined types of drag objects that Qt provides; others include QImageDrag, QColorDrag, and QUriDrag. We also set a pixmap to represent the drag. The pixmap is a small icon that follows the cursor while the drag is taking place.
We call setSubtype() to set the subtype of the object's MIME type to x-person. This causes the object's full MIME type to be text/x-person. If we didn't call setSubtype(), the MIME type would be text/plain.
Standard MIME types are defined by the Internet Assigned Numbers Authority (IANA). They consist of a type and a subtype separated by a slash. When we create non-standard types, such as text/x-person, it is recommended that an x- is prepended to the subtype. MIME types are used by the clipboard and by the drag and drop system to identify different types of data.
The drag() call starts the dragging operation. After the call, the QTextDrag object will remain in existence until the drag operation is finished. Qt takes ownership of the drag object and will delete it when it is no longer required, even if it is never dropped.
void ProjectView::contentsDragEnterEvent(QDragEnterEvent *event) { event->accept(event->provides("text/x-person")); }
The ProjectView widget not only originates drags of type text/x-person, it also accepts such drags. When a drag enters the widget, we check whether it has the correct MIME type and reject it if it hasn't.
void ProjectView::contentsDropEvent(QDropEvent *event) { QString person; if (QTextDrag::decode(event, person)) { QWidget *fromWidget = event->source(); if (fromWidget && fromWidget != this && fromWidget->inherits("ProjectView")) { ProjectView *fromProject = (ProjectView *) fromWidget; QListBoxItem *item = fromProject->findItem(person, ExactMatch); delete item; insertItem(person); } } }
In contentsDropEvent(), we use the QTextDrag::decode() function to extract the text carried by the drag. The QDropEvent::source() function returns a pointer to the widget that initiated the drag, if that widget is part of the same application. If the source widget is different from the target widget and is a ProjectView, we remove the item from the source widget (by calling delete) and insert a new item into the target.