我有一个小型 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 中(如我的情况)。
The error you're getting, "Fatal Python error: PyEval_RestoreThread: the function must be called with the GIL held", indicates a clash between Python's Global Interpreter Lock (GIL) and multi-threaded operations, likely triggered by the interaction between your Tkinter application, drag-and-drop functionality, and potentially the
pythoncom
library.
Here's a breakdown of the issue and how to approach it:
Understanding the GIL
- Python's GIL is a mechanism that allows only one thread to execute Python bytecode at a time within a single process. This simplifies many aspects of CPython (the most common Python implementation) but can limit true parallelism in CPU-bound tasks.
-
C extensions to Python (like those in
tkinter
orpythoncom
) often need to interact with Python objects. To ensure safety and prevent data corruption, the GIL must be held during these interactions.
The Problem
The error message implies that a function that requires the GIL is being called when the GIL is released. This suggests a thread is attempting to access or modify Python objects from a context where it's not allowed. This is likely happening because:
- Drag-and-drop is asynchronous: The drag-and-drop operation might be running in a separate thread provided by the operating system.
-
Callback execution:
When a drop occurs, your
DropTarget.Drop
method is called. This method, in turn, callsself.widget.add_tracks(paths)
, which interacts with Tkinter widgets. - Tkinter's threading model: Tkinter (in CPython) is generally not thread-safe. Modifying Tkinter widgets from a thread other than the main thread can lead to unpredictable behavior and crashes.
Solution: Enforcing Thread Safety
The solution involves ensuring that any interaction with Tkinter widgets from the drag-and-drop callback happens on the main thread. Here's how you can modify your code:
import tkinter as tk
from tkinter import ttk
import pythoncom
from dnd import DropTarget
# ... [Your other classes: ScrollFrame, UI]
class TrackList(ScrollFrame):
# ... [your existing code]
def add_tracks(self, tracks):
def update_track_list(): # Function to be executed on the main thread
for track in tracks:
if track not in self.track_list:
p = ttk.Label(self.frame, text=str(track))
print(p['text'])
p.pack()
self.track_list.update(tracks)
self.after_idle(update_track_list) # Schedule execution on the main thread
# ... [Rest of your code: App class]
Explanation
-
update_track_list
function: We've encapsulated the code that modifies your Tkinter widgets within this function. -
self.after_idle(update_track_list)
: This line is crucial. It uses Tkinter'safter_idle
method to schedule theupdate_track_list
function to be executed on the main thread when the application is idle. This ensures that the widget modifications are handled safely.
Important Considerations
- Error Handling: Always wrap external library calls and thread interactions in robust error handling blocks to gracefully handle any potential exceptions.
-
Thread Management:
If you're dealing with more complex multi-threaded scenarios, consider using Python's
threading
module or a higher-level library likeconcurrent.futures
to manage threads more effectively.
By ensuring that interactions with Tkinter happen on the main thread, you can avoid the GIL-related errors and maintain the stability of your application.
标签:python,tkinter,winapi From: 78780456