Many PyQt widgets support drag and drop out of the box, only requiring us to switch on the support to make it work. For example, the application shown in Figure 10.2 starts out with items in the left hand QListWidget, and with nothing in the QListWidget in the middle or in the QTableWidget on the right. The screenshot shows the application after some items have been dragged and dropped.
The application's source code is in the file chap10/draganddrop.pyw.
The drag-and-drop functionality is achieved purely by setting properties on the widgets involved. Here is the code that created the left-hand list widget:
listWidget = QListWidget()
The middle list widget is similar, except that we have set it to icon view mode instead of list view mode:
iconListWidget = QListWidget()
Making the QTableWidget support drag and drop is achieved in exactly the same way, with a call of setAcceptDrops(True) and a call of setDragEnabled(True).
No other code is necessary; what is shown is sufficient to allow users to drag icon and text items from one list widget to another, and to and from cells in the table.
The built-in drag-and-drop facilities are very convenient, and are often sufficient. But if we need to be able to handle our own custom data, we must reimplement some event handlers, as we will see in the following subsection.
The application shown in Figure 10.3 supports drag and drop for custom data; in particular, icons and text. (The source code is in chap10/customdragand-drop.pyw.) Although this is the same functionality as the built-in drag-and-drop facilities offer, the techniques used are generic and can be applied to any arbitrary data we like.
The icons and text can be dragged from the list widget on the left to the list widget on the right (which is in icon mode), or to the custom widget at the bottom left, or to the custom line edit at the bottom right—although in this last case only the text is used.
For custom data that is put on the clipboard or used by PyQt's drag-and-drop system, we use QMimeData objects, with our own custom MIME types. MIME is a standardized format for handling multipart custom data. MIME data has a type and a subtype—for example, text/plain, text/html, or image/png. To handle custom MIME data we must choose a custom type and subtype, and wrap the data in a QMimeData object.
For this example we have created a MIME type of application/x-icon-and-text. It is good practice for custom MIME subtypes to begin with x-. We have stored the data in a QByteArray, a resizable array of bytes, and which for this example holds a QString and a QIcon, although it could hold any arbitrary data.
We will begin by seeing how to make a QLineEdit subclass that can accept drops of MIME type application/x-icon-and-text, making use of the text and ignoring the icon.
The initializer simply sets the line edit to accept drops.
def dragEnterEvent(self, event):
When the user drags over the line edit we want to display an icon if the MIME data being dragged is a type that we can handle; otherwise the line edit will display the "no drop" icon (which often appears as <S>). By accepting the drag enter event we signify that we can accept drops of the type of MIME data on offer; by ignoring we say that we cannot accept such data. The icon used for acceptable data is set when the drag is initiated as we will see later on.
The drag-related event handlers are called automatically by PyQt when necessary because we set accept drops in the initializer.
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-icon-and-text"): event.setDropAction(Qt.CopyAction) event.accept() else:
As the user drags over the widget dragMoveEvent() s occur; we want the data to be copied (rather than moved), so we set the drop action accordingly.
def dropEvent(self, event):
if event.mimeData().hasFormat("application/x-icon-and-text"): data = event.mimeData().data("application/x-icon-and-text") stream = QDataStream(data, QIODevice.ReadOnly) text = QString() stream >> text self.setText(text) event.setDropAction(Qt.CopyAction) event.accept() else:
Stream- If the user drops the data on the widget we must handle it. We do this by ingt° extracting the data (a QByteArray), and then creating a QDataStream to read the data. The QDataStream class can read and write from and to any QIODevice including files, network sockets, external processes, and byte arrays. Since we are only interested in the string, that is all that we extract from the byte array. Note that to be able to stream QIcons to or from a QDataStream we must use PyQt 4.1 or later.
The DropLineEdit only supports dropping, so for our next example, we will create a QListWidget subclass which supports both dragging and dropping.
super(DnDListWidget, self).__init__(parent) self.setAcceptDrops(True) self.setDragEnabled(True)
The initializer is similar to what we used before, except that we enable both dragging and dropping.
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-icon-and-text"): event.setDropAction(Qt.MoveAction) event.accept() else:
This is almost identical to the DropLineEdit's dragMoveEvent(); the difference is that here we set the drop action to be Qt.MoveAction rather than Qt.CopyAction. The code for the dragEnterEvent() is not shown: It is the same as for the DropLineEdit.
def dropEvent(self, event):
if event.mimeData().hasFormat("application/x-icon-and-text"): data = event.mimeData().data("application/x-icon-and-text") stream = QDataStream(data, QIODevice.ReadOnly) text = QString() icon = QIcon() stream >> text >> icon item = QListWidgetItem(text, self) item.setIcon(icon) event.setDropAction(Qt.MoveAction) event.accept() else:
Stream- This code is again similar to the DropLineEdit, only now we want the icon mgfr°m as well as the text. To add an item to a QListWidget we must create a new QListWidgetItem and pass the list widget (self) as the item's parent.
245« def startDrag(self, dropActions):
item = self.currentItem() icon = item.icon() data = QByteArray()
stream = QDataStream(data, QIODevice.WriteOnly) stream << item.text() << icon mimeData = QMimeData()
drag = QDrag(self)
pixmap = icon.pixmap(24, 24)
if drag.start(Qt.MoveAction) == Qt.MoveAction: self.takeItem(self.row(item))
This is the only method that is not in the DropLineEdit, and it is the one that makes it possible to drag from DnDListWidgets. We don't have to check the return value of currentItem() because only items can be dragged, so we know that if startDrag() is called there will be an item to drag. The startDrag() method is called automatically by PyQt when needed because we set drag enabled in the initializer.
We create a new empty byte array, and use QDataStream to populate it with the QListWidgetItem's icon and text. There is no need to call setVersion() on QDataStream when we use it purely for handling in-memory data that exists only during the runtime of the application and that is not exchanged with any other application. Once we have populated the byte array, we wrap it in a QMimeData object. Then we create a QDrag object, and give it the MIME data. We have chosen to use the data's icon as the icon to be used for the drag: If we had not done so, PyQt would provide a default icon. We have also set the drag's "hotspot" to be the center of the icon. The mouse's hotspot will always coincide with the icon's hotspot.
The call to QDrag.start() initiates the drag; we give as a parameter the action or actions that we will accept. If the drag succeeds, that is, if the data is successfully dropped, the start() method returns the action that occurred—for example, copy or move. If the action was move, we remove the dragged QList-WidgetItem from this list widget. From Qt 4.3, QDrag.exec_() should be used instead of QDrag.start().
The setAcceptDrops() method is inherited from QWidget, but setDragEnabled() is not, so by default it is available in only certain widgets. If we want to create a custom widget that supports drops, we can simply call setAcceptDrops(True) and reimplement dragEnterEvent(), dragMoveEvent(), and dropEvent(), as we have done in the preceding examples. If we also want the custom widget to support drags, and the widget inherits QWidget or some QWidget subclass that does not have setDragEnabled(), we must do two things to make the widget support dragging. One is to provide a startDrag() method so that a QDrag object can be created, and another is to make sure the startDrag() method is called at an appropriate time. The easiest way to ensure that startDrag() is called is to reimplement the mouseMoveEvent():
def mouseMoveEvent(self, event):
The widget at the bottom left of the example application is a direct QWidget subclass and uses this technique. Its startDrag() method is very similar to the one we have just seen, and only a tiny bit simpler because it initiates copy drags rather than move drags, so we don't have to do anything regardless of whether the drag is dropped successfully.
Was this article helpful?
Download Tube Jacker And Discover Everything You Need To Know About Jacking Unlimited Traffic From The Video Giant. The drop dead easy way to create winning video campaigns that will FLOOD your website with unstoppable FREE traffic, all on complete and total autopilot. How to exploit a sneaky method of boosting exposure and getting your videos to the top of Google within 72 hours, guaranteed.