Drag and drop with Python (pyGTK)


One of the things which had me somewhat confused, was making Drag-and-drop work from one widget to another, different widget. In hindsght, the process isn't that difficult, but it is not trivial either, as both widget use the data differently.

The DnD mechanism, as it is implemented in GTK+, is so versatile, that it is even possible to move data from one application to another. Of course, this doesn't make it any easier to implement, as possibly the different programs could even be using different character encodings.

And, to make life even more complicated, the documentation in several tutorials either treat the (almost) trivial case where the DnD is actually inside de same widget, or are incomplete for those cases when they only discuss data coming in from another app.

In what follows, I'll try to document what I needed in a demo I implemented with students. I wanted to make a small program which could organize class scheduling. The list of courses to be distributed is in a TreeView, and for the course grid I selected a Layout.


As mentioned, the widgets used are a gtk.TreeView/gtk.ListStore, as the source of the data, and a gtk.Layout where the courses will be moved (destination). (Of course, to cover the complete Engineering dept, there's a series of Layouts, organized by school, and year).

Each Drag and Drop operation needs to be declared, and causes many events along the process, for which we'll have to write handlers. To simplify the proyect, we'll consider only one-way traffic. The other direction is completely symmetrical, it just adds more works...


This is the state of the interface, before initiating the move. Left is the TreeView, which shows the contents of a gtk.ListStore, and which will be the source of our data. To the right is a gtk.Notebook, of which each page contains a gtk.Layout, which will be the destination of the data


Here the process (DnD) is started, by pressing the left pointer button on the TreeView. To have any effect, we have to enable the TreeView widget to react to the DnD movement (else the only effect will be selecting a line):

    mi_tv.drag_source_set(gtk.gdk.BUTTON1_MASK, \
                         [('text/plain', gtk.TARGET_SAME_APP, 1)],
The first parameter indicates which pointer button will be the active one.

The second parameter is a a Python list: Each element is a tuple, which contains 3 pieces of information about the data which will be transferred. We just want to exchange a simple string, so we use'text/plain' (which is a standardized MIME type). gtk.TARGET_SAME_APP indicates we'll keep the movement inside our own program, and 1 is an identifier which can be used to know where the data came from.

Finally, the third parameter specifies what operation will be performed. We want to copy the info, but it is also possible to do a move.

With just this operation on the TreeView widget, the special cursor icon will be enabled to show DnD is enabled.

There is no event associated with this step, so we don't need to define a handler!


For the destination (our Layout) to be able to receive any 'package', we have to do a very similar operation on that widget:

                       [('text/plain', gtk.TARGET_SAME_APP, 1)], 
This operation does not change anything visibly: it only enables a series of calls to handlers when we arrive with our payload at the destination widget. There are several:

my_layout.connect("drag_motion", on_drag_motion)
will be called when the cursor arrives over the Layout widget (the Layout). It will give the handler continually the coordinates (relative to the widget) so we can decide if the spot is correct to receive the package.

my_layout.connect("drag_motion", on_drag_motion)
is called the moment we \'release\' the load (release the drag button). Mind, it's important to realize that the dragging motion with the button isn't actually moving the data! It just establishes the link between the source and destination. We will then be responsible to fetch the info at the source! The on_drag_drop is the actual moment to initiate our work:
def on_drag_drop(self, wid, context, x, y, time):
      wid.drag_get_data(context, context.targets[-1], time)
      return True
Here, 'wid' is a reference to the origin widget, which lets us request (drag_get_data) this widget to send us the information.

And of course, this now generates an event on the source widget! This means that (in our case) we have to connect a handler to process this:

mi_tv.connect("drag_data_get", on_drag_data_get)
. The code handling this:
def on_drag_data_get(widget, context, selection, info, time):
      # Here we access the source's ListStore, to fetch the info, and combine it
      # into one single string to send.
      selection.set(selection.target, 10, message)
'10' is the length of the message being sent. Even though Python manages string lengths automatically, we are potentially sending this to an app written in another language!

Finally, our data is on its way! On arrival, yet another event is triggered which we have to attend to:

mi_layout.connect("drag_data_received", on_drag_data_received)
and the function which processes the reception:
def on_drag_data_received(wid, context, x, y, data, info, time):
      message = data.get_text()
      # Here we process the received text...
      # and finally we signal that the data exchange has been finalized
      context.finish(True, False, time)

The final result (after the transfer, and already knowing the duration of the class) we adjust the size of the block, we show the appropriate data, and even add a small tooltip with the complete info.

(c) John Coppens ON6JC/LW3HAZ mail