¿Cómo tener arrastrar y soltar y ordenar GtkTreeView en GTK3?

Estoy portando liblarch , una biblioteca para el manejo de gráficos acíclicos dirigidos, desde PyGTK (GTK2) a la introspección PyGObject (GTK3). Me encontré con el problema con GtkTreeView.

La aplicación que utiliza liblarch necesita ordenar GtkTreeView por una columna, pero al mismo tiempo, el usuario puede arrastrar y soltar filas, mover una fila debajo de otra fila. Para eso tuve que procesar manualmente dnd_data_get() y dnd_data_receive() que está perfectamente bien.

Hay una configuración mínima para GtkTreeView que funciona bajo PyGTK. Las filas se ordenan y el usuario puede mover filas.

 #!/usr/bin/python # -*- coding: utf-8 -*- import gtk window = gtk.Window() window.set_size_request(300, 200) window.connect('delete_event', lambda w,e: gtk.main_quit()) # Define Liblarch Tree store = gtk.TreeStore(str, str) store.insert(None, -1, ["A", "Task A"]) store.insert(None, -1, ["B", "Task B"]) store.insert(None, -1, ["C", "Task C"]) d_parent = store.insert(None, -1, ["D", "Task D"]) store.insert(d_parent, -1, ["E", "Task E"]) # Define TreeView in similar way as it happens in GTG/Liblarch_gtk tv = gtk.TreeView() col = gtk.TreeViewColumn() col.set_title("Title") render_text = gtk.CellRendererText() col.pack_start(render_text, expand=True) col.add_attribute(render_text, 'markup', 1) col.set_resizable(True) col.set_expand(True) col.set_sort_column_id(0) tv.append_column(col) tv.set_property("expander-column", col) treemodel = store def _sort_func(model, iter1, iter2): """ Sort two iterators by function which gets node objects. This is a simple wrapper which prepares node objects and then call comparing function. In other case return default value -1 """ node_a = model.get_value(iter1, 0) node_b = model.get_value(iter2, 0) if node_a and node_b: sort = cmp(node_a, node_b) else: sort = -1 return sort treemodel.set_sort_func(1, _sort_func) tv.set_model(treemodel) def on_child_toggled(treemodel2, path, iter, param=None): """ Expand row """ if not tv.row_expanded(path): tv.expand_row(path, True) treemodel.connect('row-has-child-toggled', on_child_toggled) tv.set_search_column(1) tv.set_property("enable-tree-lines", False) tv.set_rules_hint(False) #### Drag and drop stuff dnd_internal_target = '' dnd_external_targets = {} def on_drag_fail(widget, dc, result): print "Failed dragging", widget, dc, result def __init_dnd(): """ Initialize Drag'n'Drop support Firstly build list of DND targets: * name * scope - just the same widget / same application * id Enable DND by calling enable_model_drag_dest(), enable_model-drag_source() It didnt use support from gtk.Widget(drag_source_set(), drag_dest_set()). To know difference, look in PyGTK FAQ: http://faq.pygtk.org/index.py?file=faq13.033.htp&req=show """ #defer_select = False if dnd_internal_target == '': error = 'Cannot initialize DND without a valid name\n' error += 'Use set_dnd_name() first' raise Exception(error) dnd_targets = [(dnd_internal_target, gtk.TARGET_SAME_WIDGET, 0)] for target in dnd_external_targets: name = dnd_external_targets[target][0] dnd_targets.append((name, gtk.TARGET_SAME_APP, target)) tv.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, dnd_targets, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE) tv.enable_model_drag_dest(\ dnd_targets, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE) def on_drag_data_get(treeview, context, selection, info, timestamp): """ Extract data from the source of the DnD operation. Serialize iterators of selected tasks in format ,,..., and set it as parameter of DND """ print "on_drag_data_get(", treeview, context, selection, info, timestamp treeselection = treeview.get_selection() model, paths = treeselection.get_selected_rows() iters = [model.get_iter(path) for path in paths] iter_str = ','.join([model.get_string_from_iter(iter) for iter in iters]) selection.set(dnd_internal_target, 0, iter_str) print "Sending", iter_str def on_drag_data_received(treeview, context, x, y, selection, info,\ timestamp): """ Handle a drop situation. First of all, we need to get id of node which should accept all draged nodes as their new children. If there is no node, drop to root node. Deserialize iterators of dragged nodes (see self.on_drag_data_get()) Info parameter determines which target was used: * info == 0 => internal DND within this TreeView * info > 0 => external DND In case of internal DND we just use Tree.move_node(). In case of external DND we call function associated with that DND set by self.set_dnd_external() """ print "on_drag_data_received", treeview, context, x, y, selection, info, timestamp model = treeview.get_model() destination_iter = None destination_tid = None drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info destination_iter = model.get_iter(path) if destination_iter: destination_tid = model.get_value(destination_iter, 0) # Get dragged iter as a TaskTreeModel iter # If there is no selected task (empty selection.data), # explictly skip handling it (set to empty list) if selection.data == '': iters = [] else: iters = selection.data.split(',') dragged_iters = [] for iter in iters: print "Info", info if info == 0: try: dragged_iters.append(model.get_iter_from_string(iter)) except ValueError: #I hate to silently fail but we have no choice. #It means that the iter is not good. #Thanks shitty gtk API for not allowing us to test the string print "Shitty iter", iter dragged_iter = None elif info in dnd_external_targets and destination_tid: f = dnd_external_targets[info][1] src_model = context.get_source_widget().get_model() dragged_iters.append(src_model.get_iter_from_string(iter)) for dragged_iter in dragged_iters: if info == 0: if dragged_iter and model.iter_is_valid(dragged_iter): dragged_tid = model.get_value(dragged_iter, 0) try: row = [] for i in range(model.get_n_columns()): row.append(model.get_value(dragged_iter, i)) #tree.move_node(dragged_tid, new_parent_id=destination_tid) print "move_after(%s, %s) ~ (%s, %s)" % (dragged_iter, destination_iter, dragged_tid, destination_tid) #model.move_after(dragged_iter, destination_iter) model.insert(destination_iter, -1, row) model.remove(dragged_iter) except Exception, e: print 'Problem with dragging: %s' % e elif info in dnd_external_targets and destination_tid: source = src_model.get_value(dragged_iter,0) # Handle external Drag'n'Drop f(source, destination_tid) dnd_internal_target = 'gtg/task-iter-str' __init_dnd() tv.connect('drag_data_get', on_drag_data_get) tv.connect('drag_data_received', on_drag_data_received) tv.connect('drag_failed', on_drag_fail) window.add(tv) window.show_all() tv.expand_all() gtk.main() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 

He portado este script en PyGObject (GTK3). Mi código:

 #!/usr/bin/python # -*- coding: utf-8 -*- from gi.repository import Gtk, Gdk window = Gtk.Window() window.set_size_request(300, 200) window.connect('delete_event', lambda w,e: Gtk.main_quit()) # Define Liblarch Tree store = Gtk.TreeStore(str, str) store.insert(None, -1, ["A", "Task A"]) store.insert(None, -1, ["B", "Task B"]) store.insert(None, -1, ["C", "Task C"]) d_parent = store.insert(None, -1, ["D", "Task D"]) store.insert(d_parent, -1, ["E", "Task E"]) # Define TreeView in similar way as it happens in GTG/Liblarch_gtk tv = Gtk.TreeView() col = Gtk.TreeViewColumn() col.set_title("Title") render_text = Gtk.CellRendererText() col.pack_start(render_text, expand=True) col.add_attribute(render_text, 'markup', 1) col.set_resizable(True) col.set_expand(True) col.set_sort_column_id(0) tv.append_column(col) tv.set_property("expander-column", col) treemodel = store def _sort_func(model, iter1, iter2): """ Sort two iterators by function which gets node objects. This is a simple wrapper which prepares node objects and then call comparing function. In other case return default value -1 """ node_a = model.get_value(iter1, 0) node_b = model.get_value(iter2, 0) if node_a and node_b: sort = cmp(node_a, node_b) else: sort = -1 return sort treemodel.set_sort_func(1, _sort_func) tv.set_model(treemodel) def on_child_toggled(treemodel2, path, iter, param=None): """ Expand row """ if not tv.row_expanded(path): tv.expand_row(path, True) treemodel.connect('row-has-child-toggled', on_child_toggled) tv.set_search_column(1) tv.set_property("enable-tree-lines", False) tv.set_rules_hint(False) #### Drag and drop stuff dnd_internal_target = '' dnd_external_targets = {} def on_drag_fail(widget, dc, result): print "Failed dragging", widget, dc, result def __init_dnd(): """ Initialize Drag'n'Drop support Firstly build list of DND targets: * name * scope - just the same widget / same application * id Enable DND by calling enable_model_drag_dest(), enable_model-drag_source() It didnt use support from Gtk.Widget(drag_source_set(), drag_dest_set()). To know difference, look in PyGTK FAQ: http://faq.pygtk.org/index.py?file=faq13.033.htp&req=show """ #defer_select = False if dnd_internal_target == '': error = 'Cannot initialize DND without a valid name\n' error += 'Use set_dnd_name() first' raise Exception(error) dnd_targets = [(dnd_internal_target, Gtk.TargetFlags.SAME_WIDGET, 0)] for target in dnd_external_targets: name = dnd_external_targets[target][0] dnd_targets.append((name, Gtk.TARGET_SAME_APP, target)) tv.enable_model_drag_source( Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) tv.enable_model_drag_dest(\ dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) def on_drag_data_get(treeview, context, selection, info, timestamp): """ Extract data from the source of the DnD operation. Serialize iterators of selected tasks in format ,,..., and set it as parameter of DND """ print "on_drag_data_get(", treeview, context, selection, info, timestamp treeselection = treeview.get_selection() model, paths = treeselection.get_selected_rows() iters = [model.get_iter(path) for path in paths] iter_str = ','.join([model.get_string_from_iter(iter) for iter in iters]) selection.set(dnd_internal_target, 0, iter_str) print "Sending", iter_str def on_drag_data_received(treeview, context, x, y, selection, info,\ timestamp): """ Handle a drop situation. First of all, we need to get id of node which should accept all draged nodes as their new children. If there is no node, drop to root node. Deserialize iterators of dragged nodes (see self.on_drag_data_get()) Info parameter determines which target was used: * info == 0 => internal DND within this TreeView * info > 0 => external DND In case of internal DND we just use Tree.move_node(). In case of external DND we call function associated with that DND set by self.set_dnd_external() """ print "on_drag_data_received", treeview, context, x, y, selection, info, timestamp model = treeview.get_model() destination_iter = None destination_tid = None drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info destination_iter = model.get_iter(path) if destination_iter: destination_tid = model.get_value(destination_iter, 0) # Get dragged iter as a TaskTreeModel iter # If there is no selected task (empty selection.data), # explictly skip handling it (set to empty list) if selection.data == '': iters = [] else: iters = selection.data.split(',') dragged_iters = [] for iter in iters: print "Info", info if info == 0: try: dragged_iters.append(model.get_iter_from_string(iter)) except ValueError: #I hate to silently fail but we have no choice. #It means that the iter is not good. #Thanks shitty Gtk API for not allowing us to test the string print "Shitty iter", iter dragged_iter = None elif info in dnd_external_targets and destination_tid: f = dnd_external_targets[info][1] src_model = context.get_source_widget().get_model() dragged_iters.append(src_model.get_iter_from_string(iter)) for dragged_iter in dragged_iters: if info == 0: if dragged_iter and model.iter_is_valid(dragged_iter): dragged_tid = model.get_value(dragged_iter, 0) try: row = [] for i in range(model.get_n_columns()): row.append(model.get_value(dragged_iter, i)) #tree.move_node(dragged_tid, new_parent_id=destination_tid) print "move_after(%s, %s) ~ (%s, %s)" % (dragged_iter, destination_iter, dragged_tid, destination_tid) #model.move_after(dragged_iter, destination_iter) model.insert(destination_iter, -1, row) model.remove(dragged_iter) except Exception, e: print 'Problem with dragging: %s' % e elif info in dnd_external_targets and destination_tid: source = src_model.get_value(dragged_iter,0) # Handle external Drag'n'Drop f(source, destination_tid) dnd_internal_target = 'gtg/task-iter-str' __init_dnd() tv.connect('drag_data_get', on_drag_data_get) tv.connect('drag_data_received', on_drag_data_received) tv.connect('drag_failed', on_drag_fail) window.add(tv) window.show_all() tv.expand_all() Gtk.main() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 

No puedo manejar dnd_data_receive() correctamente cuando no se evoca o no se reciben datos. Siempre falla con la siguiente callback + sus parámetros:

 Failed dragging    

Mi pregunta: ¿Cómo portar el primer script a PyGObject (GTK3) para que se pueda ordenar GtkTreeView y al mismo tiempo se puedan arrastrar y soltar las filas? ¿Cómo cambiar el manejo de las devoluciones de llamada de arrastrar y soltar para procesar el arrastrar y soltar correctamente?

En primer lugar, el error que aparece parece estar relacionado con la versión de PyGObject. Yo reproduzco información de error similar antes de reinstalar mi computadora portátil con la última versión beta de Ubuntu 13.04. Pero después de la actualización, la callback de error cambia a algo así como

 on_drag_data_get(    0 21962912 Traceback (most recent call last): File "dnd_gtk3_org.py", line 116, in on_drag_data_get selection.set(dnd_internal_target, 0, iter_str) File "/usr/lib/python2.7/dist-packages/gi/types.py", line 113, in function return info.invoke(*args, **kwargs) TypeError: argument type: Expected Gdk.Atom, but got str on_drag_data_received   45 77  0 21962912 Traceback (most recent call last): File "dnd_gtk3_org.py", line 151, in on_drag_data_received if selection.data == '': AttributeError: 'SelectionData' object has no attribute 'data' 

Solo hay dos pequeños problemas:

  • el primer parámetro de SelectionData.set() parece que solo puede ser Gtk.gdk.Atom pero no una cadena que especifica eso como en pygtk.
  • SelectionData no tiene atributos de data pero en su lugar tiene un método get_data() .

Un fragmento de código de trabajo enumerado a continuación

 #!/usr/bin/python # -*- coding: utf-8 -*- from gi.repository import Gtk, Gdk window = Gtk.Window() window.set_size_request(300, 200) window.connect('delete_event', Gtk.main_quit) # Define Liblarch Tree store = Gtk.TreeStore(str, str) store.insert(None, -1, ["A", "Task A"]) store.insert(None, -1, ["B", "Task B"]) store.insert(None, -1, ["C", "Task C"]) d_parent = store.insert(None, -1, ["D", "Task D"]) store.insert(d_parent, -1, ["E", "Task E"]) # Define TreeView in similar way as it happens in GTG/Liblarch_gtk tv = Gtk.TreeView() col = Gtk.TreeViewColumn() col.set_title("Title") render_text = Gtk.CellRendererText() col.pack_start(render_text, expand=True) col.add_attribute(render_text, 'markup', 1) col.set_resizable(True) col.set_expand(True) col.set_sort_column_id(0) tv.append_column(col) tv.set_property("expander-column", col) treemodel = store def _sort_func(model, iter1, iter2): """ Sort two iterators by function which gets node objects. This is a simple wrapper which prepares node objects and then call comparing function. In other case return default value -1 """ node_a = model.get_value(iter1, 0) node_b = model.get_value(iter2, 0) if node_a and node_b: sort = cmp(node_a, node_b) else: sort = -1 return sort treemodel.set_sort_func(1, _sort_func) tv.set_model(treemodel) def on_child_toggled(treemodel2, path, iter, param=None): """ Expand row """ if not tv.row_expanded(path): tv.expand_row(path, True) treemodel.connect('row-has-child-toggled', on_child_toggled) tv.set_search_column(1) tv.set_property("enable-tree-lines", False) tv.set_rules_hint(False) #### Drag and drop stuff dnd_internal_target = '' dnd_external_targets = {} def on_drag_fail(widget, dc, result): print "Failed dragging", widget, dc, result def __init_dnd(): """ Initialize Drag'n'Drop support Firstly build list of DND targets: * name * scope - just the same widget / same application * id Enable DND by calling enable_model_drag_dest(), enable_model-drag_source() It didnt use support from Gtk.Widget(drag_source_set(), drag_dest_set()). To know difference, look in PyGTK FAQ: http://faq.pygtk.org/index.py?file=faq13.033.htp&req=show """ #defer_select = False if dnd_internal_target == '': error = 'Cannot initialize DND without a valid name\n' error += 'Use set_dnd_name() first' raise Exception(error) dnd_targets = [(dnd_internal_target, Gtk.TargetFlags.SAME_WIDGET, 0)] for target in dnd_external_targets: name = dnd_external_targets[target][0] dnd_targets.append((name, Gtk.TARGET_SAME_APP, target)) tv.enable_model_drag_source( Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) tv.enable_model_drag_dest(\ dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) def on_drag_data_get(treeview, context, selection, info, timestamp): """ Extract data from the source of the DnD operation. Serialize iterators of selected tasks in format ,,..., and set it as parameter of DND """ print "on_drag_data_get(", treeview, context, selection, info, timestamp treeselection = treeview.get_selection() model, paths = treeselection.get_selected_rows() iters = [model.get_iter(path) for path in paths] iter_str = ','.join([model.get_string_from_iter(iter) for iter in iters]) selection.set(selection.get_target(), 0, iter_str) print "Sending", iter_str def on_drag_data_received(treeview, context, x, y, selection, info,\ timestamp): """ Handle a drop situation. First of all, we need to get id of node which should accept all draged nodes as their new children. If there is no node, drop to root node. Deserialize iterators of dragged nodes (see self.on_drag_data_get()) Info parameter determines which target was used: * info == 0 => internal DND within this TreeView * info > 0 => external DND In case of internal DND we just use Tree.move_node(). In case of external DND we call function associated with that DND set by self.set_dnd_external() """ print "on_drag_data_received", treeview, context, x, y, selection, info, timestamp model = treeview.get_model() destination_iter = None destination_tid = None drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info destination_iter = model.get_iter(path) if destination_iter: destination_tid = model.get_value(destination_iter, 0) # Get dragged iter as a TaskTreeModel iter # If there is no selected task (empty selection.data), # explictly skip handling it (set to empty list) data = selection.get_data() if data == '': iters = [] else: iters = data.split(',') dragged_iters = [] for iter in iters: print "Info", info if info == 0: try: dragged_iters.append(model.get_iter_from_string(iter)) except ValueError: #I hate to silently fail but we have no choice. #It means that the iter is not good. #Thanks shitty Gtk API for not allowing us to test the string print "Shitty iter", iter dragged_iter = None elif info in dnd_external_targets and destination_tid: f = dnd_external_targets[info][1] src_model = context.get_source_widget().get_model() dragged_iters.append(src_model.get_iter_from_string(iter)) for dragged_iter in dragged_iters: if info == 0: if dragged_iter and model.iter_is_valid(dragged_iter): dragged_tid = model.get_value(dragged_iter, 0) try: row = [] for i in range(model.get_n_columns()): row.append(model.get_value(dragged_iter, i)) #tree.move_node(dragged_tid, new_parent_id=destination_tid) print "move_after(%s, %s) ~ (%s, %s)" % (dragged_iter, destination_iter, dragged_tid, destination_tid) #model.move_after(dragged_iter, destination_iter) model.insert(destination_iter, -1, row) model.remove(dragged_iter) except Exception, e: print 'Problem with dragging: %s' % e elif info in dnd_external_targets and destination_tid: source = src_model.get_value(dragged_iter,0) # Handle external Drag'n'Drop f(source, destination_tid) dnd_internal_target = 'gtg/task-iter-str' __init_dnd() tv.connect('drag_data_get', on_drag_data_get) tv.connect('drag_data_received', on_drag_data_received) tv.connect('drag_failed', on_drag_fail) window.add(tv) window.show_all() tv.expand_all() Gtk.main() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 

La diferencia entre el fragmento de arriba y el de tu pregunta es

 116c116 < selection.set(selection.get_target(), 0, iter_str) --- > selection.set(dnd_internal_target, 0, iter_str) 151,152c151 < data = selection.get_data() < if data == '': --- > if selection.data == '': 155c154 < iters = data.split(',') --- > iters = selection.data.split(',') 

Además, hay otro ejemplo para la versión GTK + 3 Arrastrar y soltar de TreeView en otro hilo: arrastrar y soltar sin respuesta en pygobject

¡GTG es una gran pieza de software! Pero es demasiado lento, al menos en mi computadora. Así que he estado escribiendo una biblioteca de C ++ que muestra un gráfico acíclico dirigido usando Gtk :: TreeView, y miré mucho el código fuente de LibLarch.

Por lo que sé, los enlaces Python y C ++ de GTK comparten la misma limitación, provenientes del propio GTK (una vez miré el código fuente de GTK para encontrar exactamente por qué funciona así): si giras la tecla arrastrar y soltar y la clasificación , arrastrar y soltar no funcionará. Te ofrezco tres cosas que puedes hacer al respecto:

  1. Haga un parche a GTK que limite el dnd cuando la ordenación esté habilitada, en lugar de bloquearla por completo.

  2. Implementar la clasificación por ti mismo. Es fácil: comience cargando sus datos en una vista de árbol ordenada. Ahora, cada vez que el usuario arrastra y suelta, mueva la fila arrastrada a la nueva posición usando su función de clasificación. Pero deja la clasificación GTK apagada.

  3. Esto se puede hacer además de 2, es un problema de diseño de la GUI: en GtkTreeView puede insertar un elemento entre elementos asociados, lo que no tiene mucho sentido en los árboles ordenados. En términos de la interfaz de usuario, es mejor permitir la eliminación solo de filas ON, no entre ellas. Ejemplo: Nautilus list-view funciona así. La solución es anular el manejador predeterminado TreeView drag_data_received (), o mejor que eso en términos de mantenibilidad: envíe a su modelo una pista de la vista que le diga al modelo si la posición de caída está ACTIVADA o ANTES. Si la posición es ANTES, haga que la anulación virtual de drop_possible () de su árbol devuelva falso, y entonces no verá el “no se puede jugar”, puede obtener una GUI más limpia.

2 y 3 es lo que hago en C ++, deberías poder hacer eso en Python fácilmente 🙂

Además, una nota con respecto a la opción 1: GtktreeView (¿o es GtkTreeStore? Olvidé) simplemente bloquea cualquier gota si la clasificación está habilitada. Si alguien simplemente arregla eso (tú … o yo …), o al menos escribe una clase de vista derivada, tendremos una GUI limpia predeterminada para árboles ordenados con soporte para dnd.