尝试理解此错误:致命Python错误:PyEval_RestoreThread:必须在持有GIL的情况下调用该函数

问题描述 投票:0回答:1

我有一个小型 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 中(如我的情况)。

python tkinter winapi
1个回答
0
投票

我读了你的代码,看起来发生的事情是这样的:

  • 您删除了一个文件,该文件会触发其中的内容
    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
是如何工作的。

© www.soinside.com 2019 - 2024. All rights reserved.