我有一个小型 tkinter 应用程序,我一直在其中实现最小的“拖放”,主要作为学习实验。我真正关心的是删除文件的文件路径。一切实际上都工作正常,直到我尝试在拖放后打包标签小部件。下面的最小工作示例。有问题的行会通过评论指出。
我通常不会在调试方面遇到太多困难,但我只是不知道从这里该去哪里。我知道 GIL 是什么,但不知道为什么或如何成为这里的问题。
完整错误:
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)
我尝试研究这个问题,但没有什么成果。我发现的一点点表明它与 C API 调用(我对此知之甚少)有关 - 也许与 win32api 有关 - 并且主要发生在 GUI 中(如我的情况)。
我读了你的代码,看起来发生的事情是这样的:
DesignatedWrapPolicy
TrackList.add_tracks
p.pack()
并且因为它位于框架内并且它的大小发生变化,所以应该执行它的 <Configure>
绑定我认为在某些时候,
tkinter
正在尝试调用您的ScrollFrame.on_resize
函数,但因为它位于DesignatedWrapPolicy
内部,所以它会崩溃并失败。
要解决此问题,您可以安排将来对
TrackList.add_tracks
的调用,以便 DesignatedWrapPolicy
不在调用堆栈中。为此,请将 self.widget.add_tracks(paths)
更改为 self.widget.after(1, self.widget.add_tracks, paths)
注意:所有这些都只是猜测,因为我不知道
DesignatedWrapPolicy
是如何工作的。