2025, Nov 07 13:00

DearPyGUI XDND drag-and-drop on X11 with python-xlib: set the event mask so pending_events() works

Troubleshooting DearPyGUI drag-and-drop on X11: why XDND events never arrive with python-xlib and how setting the window event mask fixes pending_events()=0.

When wiring up system drag and drop (XDND) for a DearPyGUI window on X11 through python-xlib, it’s easy to end up with a loop that never sees any events. The UI is visible, the window can be found, the XdndAware property is set, but dsp.pending_events() stubbornly returns 0 no matter what you drag over the window.

Reproducing the issue

The example below builds a DearPyGUI viewport, locates the corresponding X11 window, marks it as XDND-aware, and starts a thread that listens for XDND traffic. The loop prints the number of pending events but never processes any.

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()

What is actually going on

The XDND atoms are created, the window is found and marked as XdndAware, and the loop polls dsp.pending_events(). Yet nothing arrives. The crucial detail is that the target X11 window has not been configured to receive the relevant notifications. Without changing attributes to select an event mask, the connection simply does not see the traffic, so dsp.pending_events() stays at 0 even while you drag files over the window.

The fix

Set the event mask on the target X window and flush the display before entering the loop. After that, process all queued events in a tight inner loop.

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")
    # This is crucial:
    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)

With the event mask set through change_attributes and the display flushed, dsp.pending_events() starts producing work for the loop to consume.

Why this matters

When integrating DearPyGUI with X11-level drag and drop, you might successfully locate the parent window, walk the tree, switch to the real content window, and set XdndAware. None of that guarantees you will actually see anything in your event loop unless the window is configured to receive notifications. Selecting the proper masks prevents silent failures where everything looks correct but the loop never fires.

Practical takeaways

Make the DearPyGUI window XDND-aware and also select the relevant event masks on the target XID before polling. If you suspect the loop is idle, print what you have and where you are in the flow to validate assumptions: what window you matched, what atoms you set, and what the tree looks like. Keep the output readable so the control flow is obvious. Finally, be explicit about the behavior you expect to observe once events arrive; that will guide what you log in the loop.

With these steps in place, DearPyGUI can cooperate with X11 drag and drop predictably, and your event loop will respond when files or images are dragged over the window.

The article is based on a question from StackOverflow by kalopseeia and an answer by kalopseeia.