I have a small tkinter application where I've been playing with implementing a minimal "drag and drop", mostly as a learning experiment. All I really care about is the file path from the dropped files. Everything was actually working fine until I tried to pack a label widget after the drag and drop. Minimal working example below. The offending line is called out with a comment.
I don't typically struggle too much with debugging, but I just have no idea where to even go from here. I know what the GIL is, but not why or how it's an issue here.
The full error:
Fatal Python error: PyEval_RestoreThread: the function must be called with the GIL held, after Python initialization and before Python finalization, but the GIL is released (the
current Python thread state is NULL)
Python runtime state: initialized
Current thread 0x000030e0 (most recent call first):
File "C:\<User>\Python\Lib\tkinter\__init__.py", line 1504 in mainloop
File "c:\Users\<User>\Desktop\<directory>\ui.py", line 80 in __init__
File "c:\Users\<User>\Desktop\<directory>\ui.py", line 83 in <module>
Extension modules: _win32sysloader, win32api, win32comext.shell.shell, win32trace (total: 4)
ui.py
import tkinter as tk
from tkinter import ttk
import pythoncom
from dnd import DropTarget
class ScrollFrame(ttk.Labelframe):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.canvas = tk.Canvas(self, highlightthickness=0)
self.frame = ttk.Frame(self.canvas, padding=(10, 0))
self.scrollbar = ttk.Scrollbar(
self, orient='vertical', command=self.canvas.yview
)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.create_window((0, 0), window=self.frame, anchor='n')
self.canvas.pack(side='left', anchor='n', fill='both', expand=True)
self.scrollbar.pack(side='right', fill='y')
self.frame.bind('<Configure>', self.on_resize)
self.frame.bind(
'<Enter>',
lambda _: self.canvas.bind_all('<MouseWheel>', self.on_scroll)
)
self.frame.bind(
'<Leave>',
lambda _: self.canvas.unbind_all('<MouseWheel>')
)
def on_resize(self, _):
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
def on_scroll(self, e):
if self.canvas.winfo_height() < self.frame.winfo_height():
self.canvas.yview_scroll(-e.delta // 120, 'units')
class TrackList(ScrollFrame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.track_list = set()
hwnd = self.winfo_id()
pythoncom.OleInitialize()
pythoncom.RegisterDragDrop(
hwnd,
pythoncom.WrapObject(
DropTarget(self),
pythoncom.IID_IDropTarget,
pythoncom.IID_IDropTarget
)
)
def add_tracks(self, tracks):
for track in tracks:
if track not in self.track_list:
p = ttk.Label(self.frame, text=str(track))
print(p['text'])
p.pack() # This is the offending line
self.track_list.update(tracks)
class UI(ttk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tracks = TrackList(
self,
labelwidget=ttk.Label(text='Track List')
).pack()
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ui = UI(self, name='ui')
self.ui.pack(fill='both', expand=True)
self.mainloop()
App()
dnd.py
from pathlib import Path
from pythoncom import TYMED_HGLOBAL, IID_IDropTarget
from pywintypes import com_error
from win32con import CF_HDROP
from win32comext.shell import shell
from win32com.server.policy import DesignatedWrapPolicy
def get_drop_paths(drop):
try:
data = CF_HDROP, None, 1, -1, TYMED_HGLOBAL
path_data = drop.GetData(data)
except com_error:
return
paths = []
query = shell.DragQueryFile
for i in range(query(path_data.data_handle, -1)):
fpath = Path(query(path_data.data_handle, i))
if fpath.is_dir():
paths += list(fpath.iterdir())
else:
paths.append(fpath)
return paths
class DropTarget(DesignatedWrapPolicy):
_public_methods_ = ['DragEnter', 'DragOver', 'DragLeave', 'Drop']
_com_interface_ = [IID_IDropTarget]
def __init__(self, widget):
self._wrap_(self)
self.widget = widget
def DragEnter(self, *args):
...
def DragOver(self, *args):
...
def DragLeave(self, *args):
...
def Drop(self, data_object, *args):
paths = get_drop_paths(data_object)
self.widget.add_tracks(paths)
I've tried researching the issue with little to show for my efforts. What little I've found suggests it has something to do with C API calls (which I have little knowledge in) - perhaps something with the win32api - and mostly occuring with GUIs (as in my case).
From your code, it looks like this is what is happening:
DesignatedWrapPolicy
DropTarget.Drop
TrackList.add_tracks
p.pack()
but because that is inside a frame and the frame's size changes, its <Configure>
binding should be executed.I think that at some point, tkinter
is trying to call your ScrollFrame.on_resize
function but because it's inside the DesignatedWrapPolicy
it freaks out and fails.
To solve that, you can schedule a call to TrackList.add_tracks
in the future, when DesignatedWrapPolicy
isn't in the call stack. To do that, change self.widget.add_tracks(paths)
to self.widget.after(1, self.widget.add_tracks, paths)
Note: all of this is just speculation because I have no idea how DesignatedWrapPolicy
works.