2025, Nov 10 06:03

Почему в X11 нет событий XDND в DearPyGUI и как это исправить

Разбираем баг с XDND в DearPyGUI на X11: dsp.pending_events() возвращает 0. Как выставить XdndAware и маски событий в python-xlib, чтобы drag-and-drop заработал.

При подключении системного перетаскивания (XDND) для окна DearPyGUI в X11 через python-xlib легко столкнуться с циклом, который так и не получает ни одного события. Интерфейс виден, окно находится, свойство XdndAware выставлено, но dsp.pending_events() упрямо возвращает 0 независимо от того, что вы перетаскиваете поверх окна.

Как воспроизвести проблему

Пример ниже создает вьюпорт DearPyGUI, находит соответствующее окно X11, помечает его как XDND-совместимое и запускает поток, слушающий трафик XDND. Цикл печатает количество ожидающих событий, но ни одно из них не обрабатывает.

import dearpygui.dearpygui as gui
import threading
import time
from Xlib import X, display as xdisplay, Xatom as xatom


def locate_window_by_title(title, dsp):
    root = dsp.screen().root

    def walk(node):
        for child in node.query_tree().children:
            try:
                nm = child.get_wm_name()
                if nm and title in nm:
                    return child
                got = walk(child)
                if got:
                    return got
            except:
                pass
        return None

    return walk(root)


def pick_content_window(parent):
    for ch in parent.query_tree().children:
        nm = ch.get_wm_name()
        if nm:
            print(f"Child: {hex(ch.id)}, name: {nm}")
        if nm and "Drag-and-Drop-Example" in nm:
            return ch
    return parent


def mark_xdnd_ready(win, dsp):
    xdnd_aware = dsp.intern_atom("XdndAware")
    win.change_property(xdnd_aware, xatom.ATOM, 32, [5])
    dsp.flush()

    prop = win.get_full_property(xdnd_aware, xatom.ATOM)
    if prop:
        print(f"XdndAware property set correctly: {prop.value}")
        print(f"Using child content window: {hex(win.id)}")
    else:
        print("Failed to set XdndAware property.")


def xdnd_event_loop(win, dsp):
    a_enter = dsp.intern_atom("XdndEnter")
    a_pos = dsp.intern_atom("XdndPosition")
    a_drop = dsp.intern_atom("XdndDrop")
    a_sel = dsp.intern_atom("XdndSelection")
    a_utf8 = dsp.intern_atom("UTF8_STRING")

    print(f"Listening for XDND events on: {hex(win.id)}")

    while True:
        print(dsp.pending_events())
        if dsp.pending_events():
            print("Processing pending events...")
            # e = dsp.next_event()
            # if e.type == X.ClientMessage:
            #     if e.client_type == a_enter:
            #         print("Drag entered")
            #     elif e.client_type == a_pos:
            #         print("Drag position updated")
            #     elif e.client_type == a_drop:
            #         print("Drop detected")
            #         win.convert_selection(a_sel, a_utf8, X.CurrentTime)
            # elif e.type == X.SelectionNotify:
            #     prop = win.get_full_property(e.property, 0)
            #     if prop:
            #         uris = prop.value.decode("utf-8").strip().splitlines()
            #         for uri in uris:
            #             if uri.startswith("file://"):
            #                 path = uri[7:]
            #                 print(f"Dropped file: {path}")
        else:
            time.sleep(0.01)


def print_hierarchy(node, depth=0):
    pad = "  " * depth
    nm = node.get_wm_name()
    print(f"{pad}-> {hex(node.id)}  name: {nm}")
    for ch in node.query_tree().children:
        print_hierarchy(ch, depth+1)


gui.create_context()
gui.create_viewport(title='Drag-and-Drop-Example', width=400, height=300)

with gui.window(label="main-window", tag="main-window", no_close=True):
    gui.add_text("Drag a file here")

gui.setup_dearpygui()
gui.show_viewport()

time.sleep(2)

dsp = xdisplay.Display()
found = locate_window_by_title("Drag-and-Drop-Example", dsp)
if not found:
    raise Exception("Could not find DPG window")
print(f"Found Parent window: {hex(found.id)}")

print("Dumping window tree:")
print_hierarchy(found)

target = pick_content_window(found)

mark_xdnd_ready(target, dsp)

threading.Thread(target=xdnd_event_loop, args=(target, dsp), daemon=True).start()

while gui.is_dearpygui_running():
    gui.render_dearpygui_frame()

gui.destroy_context()

Что происходит на самом деле

Атомы XDND создаются, окно находится и помечается как XdndAware, а цикл опрашивает dsp.pending_events(). Однако ничего не приходит. Ключевой момент в том, что целевое окно X11 не было настроено на получение соответствующих уведомлений. Без изменения атрибутов и выбора маски событий соединение просто не видит трафик, поэтому dsp.pending_events() остается равным 0, даже когда вы перетаскиваете файлы поверх окна.

Как исправить

Задайте маску событий на целевом X-окне и выполните flush дисплея перед входом в цикл. Затем обрабатывайте все накопившиеся события во внутреннем тесном цикле.

def xdnd_event_loop(win, dsp):
    a_enter = dsp.intern_atom("XdndEnter")
    a_pos = dsp.intern_atom("XdndPosition")
    a_drop = dsp.intern_atom("XdndDrop")
    a_sel = dsp.intern_atom("XdndSelection")
    a_utf8 = dsp.intern_atom("UTF8_STRING")

    # Это важно:
    win.change_attributes(event_mask=X.PropertyChangeMask | X.StructureNotifyMask | X.SubstructureNotifyMask)
    dsp.flush()

    print(f"Listening for XDND events on: {hex(win.id)}")

    while True:
        while dsp.pending_events():
            # e = dsp.next_event()
            # if e.type == X.ClientMessage:
            #     if e.client_type == a_enter:
            #         print("Drag entered")
            #     elif e.client_type == a_pos:
            #         print("Drag position updated")
            #     elif e.client_type == a_drop:
            #         print("Drop detected")
            #         win.convert_selection(a_sel, a_utf8, X.CurrentTime)
            # elif e.type == X.SelectionNotify:
            #     prop = win.get_full_property(e.property, 0)
            #     if prop:
            #         uris = prop.value.decode("utf-8").strip().splitlines()
            #         for uri in uris:
            #             if uri.startswith("file://"):
            #                 path = uri[7:]
            #                 print(f"Dropped file: {path}")
        time.sleep(0.01)

После установки маски событий через change_attributes и сброса дисплея dsp.pending_events() начинает отдавать работу, которую цикл может обрабатывать.

Почему это важно

Интегрируя DearPyGUI с перетаскиванием уровня X11, вы можете успешно найти родительское окно, пройтись по дереву, переключиться на окно с реальным содержимым и установить XdndAware. Но все это не гарантирует появление событий в вашем цикле, если окно не настроено на получение уведомлений. Правильно выбранные маски предотвращают тихие сбои, когда с виду все настроено, но цикл так и не срабатывает.

Практические выводы

Сделайте окно DearPyGUI XDND-совместимым и дополнительно выберите релевантные маски событий на целевом XID до начала опроса. Если подозреваете, что цикл простаивает, выводите текущие данные и точку в процессе, чтобы проверить предположения: какое окно сопоставлено, какие атомы установлены и как выглядит дерево. Следите за читаемостью лога, чтобы ход выполнения был очевиден. И, наконец, явно сформулируйте ожидаемое поведение при появлении событий — это подскажет, что логировать в цикле.

С этими шагами DearPyGUI сможет стабильно работать с перетаскиванием X11, и ваш цикл начнет реагировать, когда файлы или изображения проводят над окном.

Статья основана на вопросе на StackOverflow от kalopseeia и ответе от kalopseeia.