使用 GTK4 进行拖放:通过 ContentProvider 连接 DragSource 和 DropTarget 以获得派生 类

Drag and drop with GTK4: connecting DragSource and DropTarget via ContentProvider for derived classes

我正在研究 (py)gtk4 的拖放功能,但遇到了困难。我有一个流框派生的 class MediaGallery,其中包含带有图像及其文件名的帧(class MediaFile),以及一个列表框派生的 class Albums.我想将一个或多个选定的图像从 MediaGallery 拖到 Albums,这最终会将它们添加到基础数据库中。

相关代码:

class Album(gtk.Box):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, orientation=gtk.Orientation.HORIZONTAL, **kwargs)
        self.label = gtk.Label(hexpand=False)
        self.append(self.label)
        self.label.set_visible(False)
        self.entry = gtk.Entry()
        self.append(self.entry)
        self.entry.set_visible(True)
        self.entry.connect('activate', self.on_entry_changed)

        dnd = gtk.DropTarget.new(gdk.FileList, gdk.DragAction.COPY)
        dnd.connect('drop', self.on_dnd_drop)
        dnd.connect('accept', self.on_dnd_accept)
        dnd.connect('enter', self.on_dnd_enter)
        dnd.connect('motion', self.on_dnd_motion)
        dnd.connect('leave', self.on_dnd_leave)
        self.add_controller(dnd)

    def on_entry_changed(self, entry):
        name = entry.get_text()
        self.label.set_text(name)
        self.entry.set_visible(False)
        self.label.set_visible(True)

    def on_dnd_drop(self, value, x, y, user_data):
        print(f'in on_dnd_drop(); value={value}, x={x}, y={y}, user_data={user_data}')

    def on_dnd_accept(self, drop, user_data):
        print(f'in on_dnd_accept(); drop={drop}, user_data={user_data}')
        return True

    def on_dnd_enter(self, drop_target, x, y):
        print(f'in on_dnd_enter(); drop_target={drop_target}, x={x}, y={y}')
        return gdk.DragAction.COPY

    def on_dnd_motion(self, drop_target, x, y):
        print(f'in on_dnd_motion(); drop_target={drop_target}, x={x}, y={y}')
        return gdk.DragAction.COPY

    def on_dnd_leave(self, user_data):
        print(f'in on_dnd_leave(); user_data={user_data}')

class MediaFile(gtk.FlowBoxChild):
    def __init__(self, *args, file, **kwargs):
        super().__init__(*args, **kwargs)

        self.filename = file

        frame = gtk.Frame()
        self.set_child(frame)

        vbox = gtk.Box(orientation=gtk.Orientation.VERTICAL)
        frame.set_child(vbox)

        self.image = gtk.Image.new_from_file(file)
        self.image.set_pixel_size(256)
        vbox.append(self.image)

        label = gtk.Label.new(file[file.rfind('/')+1:])
        vbox.append(label)

    def __repr__(self):
        return f'<MediaFile {self.filename}>'

class MediaGallery(gtk.FlowBox):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.name = kwargs.get('name', 'default')
        self.connect('child-activated', self.on_media_selected)

        dnd = gtk.DragSource.new()
        dnd.set_actions(gdk.DragAction.COPY)
        dnd.connect('prepare', self.on_dnd_prepare)
        dnd.connect('drag-begin', self.on_dnd_begin)
        dnd.connect('drag-end', self.on_dnd_end)
        self.add_controller(dnd)

    def __repr__(self):
        return f'<MediaGallery {self.name}>'

    def on_media_selected(self, gallery, media_file):
        print(f'on_media_selected(); gallery={gallery}, media_file={media_file}')

    def on_dnd_prepare(self, drag_source, x, y):
        data = self.get_selected_children()
        print(f'in on_dnd_prepare(); drag_source={drag_source}, x={x}, y={y}, data={data}')
        if len(data) == 0:
            return None

        paintable = data[0].image.get_paintable()
        drag_image = gtk.Image.new_from_paintable(paintable)
        drag_image.set_opacity(0.5)  # FIXME: not sure why transparency doesn't work
        drag_source.set_icon(drag_image.get_paintable(), 128, 128)  # FIXME: not sure why hot_x and hot_y don't work
        
        content = gdk.ContentProvider.new_for_value(data)
        return content

    def on_dnd_begin(self, drag_source, data):
        content = data.get_content()
        print(f'in on_dnd_begin(); drag_source={drag_source}, data={data}, content={content}')

    def on_dnd_end(self, drag, drag_data, flag):
        print(f'in on_dnd_end(); drag={drag}, drag_data={drag_data}, flag={flag}')

完整代码可从 here 获得。我得到的输出:

in on_dnd_prepare(); drag_source=<Gtk.DragSource object at 0x7f755e760940 (GtkDragSource at 0x2d100e0)>, x=176.15234375, y=172.96092224121094, data=[<MediaFile 20210712_190722A.jpg>]
in on_dnd_begin(); drag_source=<Gtk.DragSource object at 0x7f755e760940 (GtkDragSource at 0x2d100e0)>, data=<__gi__.GdkWaylandDrag object at 0x7f75424f2e00 (GdkWaylandDrag at 0x41b6ac0)>, content=<__gi__.GdkContentProviderValue object at 0x7f75424eb040 (GdkContentProviderValue at 0x43a8c10)>
in on_dnd_accept(); drop=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, user_data=<__gi__.GdkWaylandDrop object at 0x7f7542374440 (GdkWaylandDrop at 0x7f7560157390)>
in on_dnd_enter(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
in on_dnd_motion(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
in on_dnd_motion(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: ../../../gobject/gtype.c:4322: type id '0' is invalid
  return Gio.Application.run(self, *args, **kwargs)
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: cant peek value table for type '<invalid>' which is not currently referenced
  return Gio.Application.run(self, *args, **kwargs)
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: ../../../gobject/gvalue.c:185: cannot initialize GValue with type '(null)', this type has no GTypeValueTable implementation
  return Gio.Application.run(self, *args, **kwargs)

(python:196456): Gdk-CRITICAL **: 22:21:40.071: gdk_content_provider_get_value: assertion 'G_IS_VALUE (value)' failed

(python:196456): Gdk-CRITICAL **: 22:21:40.071: gdk_content_provider_get_value: assertion 'G_IS_VALUE (value)' failed

(python:196456): GLib-GIO-CRITICAL **: 22:21:40.071: g_task_return_error: assertion 'error != NULL' failed
in on_dnd_leave(); user_data=<Gtk.DropTarget object at 0x7f7542374480 (GtkDropTarget at 0x3f56aa0)>

我找不到任何关于如何为 DropTarget 适当设置 gdk 类型以及如何使 DragSourceDropTarget 通过 [= 交换数据的文档22=]。任何人都可以提供任何见解吗?提前致谢!

你实际上应该 return GObject.Value 在 ::prepare

例如,这里更改为使用 gliststore 将多个项目存储在单个 GObject 中,并从中创建一个 GValue:

    def on_dnd_prepare(self, drag_source, x, y):
        data = gio.ListStore()
        data.splice(0, 0, self.get_selected_children())
        print(data.get_n_items())
        print(f'in on_dnd_prepare(); drag_source={drag_source}, x={x}, y={y}, data={data}')
        if len(data) == 0:
            return None

        paintable = data[0].image.get_paintable()  # TODO: make this nicer for multiple selections
        drag_image = gtk.Image.new_from_paintable(paintable)
        drag_image.set_opacity(0.5)  # FIXME: not sure why transparency doesn't work
        drag_source.set_icon(drag_image.get_paintable(), 128, 128)  # FIXME: not sure why hot_x and hot_y don't work
        
        content = gdk.ContentProvider.new_for_value(gobject.Value(gio.ListModel, data))
        return content

您应该让放置目标接受正确的类型(GdkFileList 在 PyGObject 中损坏 https://gitlab.gnome.org/GNOME/pygobject/-/issues/468

dnd = gtk.DropTarget.new(gio.ListModel, gdk.DragAction.COPY)

并且你应该修正 drop 函数的参数

    def on_dnd_drop(self, drop_target, value, x, y):
        print(f'in on_dnd_drop(); value={value}, x={x}, y={y}')
        print(list(value))

您可以在此处访问创建 gobject.Value 时传递的内容作为值参数(在本例中为 MediaFiles 列表)