为什么在我使用异步服务进行检查时,右键菜单没有显示?

编程语言 2026-07-11

我正在使用一个线程,并将语言工具的请求排队:

import threading
import queue
import language_tool_python

class SpellCheckService:

    def __init__(self, pure_english:bool=False):
        self.tool_en = language_tool_python.LanguageTool("en_US")
        self.tool_el = language_tool_python.LanguageTool("auto")


        self.queue = queue.Queue()
        self.thread = threading.Thread(target=self._worker, daemon=True)
        self.thread.start()

    def request(self, text, locale, callback):
        """Send text for spellcheck"""
        self.queue.put((text,locale, callback))

    def _worker(self):
        while True:
            text, language, callback = self.queue.get()

            if(language == "en_US"):
                matches = self.tool_en.check(text)
            else:
                matches = self.tool_el.check(text)

            callback(text,matches)
            self.queue.task_done()

接着我做了一个tk.Entry,它使用Spellcheck:

import tkinter as tk
from lib.widgets.text.pastable_entry import PasteableEntry
from lib.spellcheck_service import SpellCheckService


class SpellCheckEntry(tk.Entry):
    """
    NOTE use get_spellchecked_value andf provide a valid callback upon value_callback
    """

    def __init__(self,spellcheck_service:SpellCheckService, name:str="url",value_callback=None, locale="en_US",master=None, **kwargs):
        super().__init__(master, **kwargs)

        self.__spellcheck_service = spellcheck_service
        self.__value_callback = value_callback
        self.__name = name
        self.__locale = locale

        self.__spelling_errors=[]
        self.menu = tk.Menu(master, tearoff=0)

    def __spellcheck_callback(self,text,matches):
        self.__spelling_errors = []
        for m in matches:
            print(m)
            if ("EL_GR" in (m.rule_id) or "EN_US" in (m.rule_id)):
                self.__spelling_errors.append(m)
                print("Matching Rule", m.rule_id)

        self.configure(highlightbackground="gray")
        self.unbind("<Button-3>")

        if self.__spelling_errors:
            self.configure(highlightbackground="red", highlightcolor="red")
            for error in self.__spelling_errors:
                for suggestion in error.replacements:
                    self.menu.add_command(label=suggestion,command=lambda s=suggestion: self._apply_suggestion(s))
            print("Bind Menu")
            self.menu.bind("<Button-3>",self._show_suggestions)
            self.__value_callback(False,self.__name,text)
        elif self.__value_callback:
            self.__value_callback(True,self.__name,text)

    def get_spellchecked_value(self):
       self.__spellcheck_service.request(self.get().strip(),self.__locale,self.__spellcheck_callback)

    def _apply_suggestion(self,text):
        self.delete(0, tk.END)
        self.insert(0, text)
        self.__spelling_errors.clear()
        self.configure(highlightbackground="gray")
        self.unbind("<Button-3>")

    def _show_suggestions(self,event):
        print("Showing Suggestions")
        try:
            self.menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.menu.grab_release()

并将它应用到这个UI:

import tkinter as tk
from tkinter import ttk

import sys
import os

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', '..')))
from lib.dto.button_dto import ButtonFinalValueDTO
from lib.url import is_test_of,make_test
from lib.widgets.text.spellcheck_entry import SpellCheckEntry
from lib.widgets.url.url_entry import UrlEntry
from lib.spellcheck_service import SpellCheckService


class UrlInput(ttk.Frame):
    def __init__(self, root, button: ButtonFinalValueDTO, button_value_callback):
        super().__init__(root)
        self.button = button

        self.__language_tool = SpellCheckService()

        self.__greek_value=None
        self.__en_value=None
        self.__button_value_callback=button_value_callback

        self.__draw()

    def trigger_get_value(self):
        self.__greek_value = None
        self.__en_value = None
        self.label_en.get_spellchecked_value()
        self.label_el.get_spellchecked_value()

    def __check_url(self) -> bool:
        # Run all checks and store results
        test_url_ok = self.url_test_input.check(self.test_url_error)
        test_example_ok = self.example_test_input.check(self.test_example_error)
        prod_example_ok = self.example_prod_input.check(self.prod_example_error)
        prod_url_ok = self.url_prod_input.check(self.prod_url_error)


        all_ok = test_url_ok and test_example_ok and prod_example_ok and prod_url_ok

        if not all_ok:
            return False

        test_url = self.url_test_input.get()
        prod_url = self.url_prod_input.get()

        if not is_test_of(prod_url,test_url):
            self.url_prod_input.mark_as_error()
            self.prod_url_error.config(text=f"{prod_url} is not valid it should be "+make_test(prod_url))
            return False

        return all_ok


    def __value_callback(self,value_correct:bool,name:str,text:str):
        print("Value callback",value_correct,name,text)
        if name == "label_el" and value_correct:
            self.__greek_value = text
        elif name == "label_en" and value_correct:
            self.__en_value = text

        if self.__greek_value is not None and self.__en_value is not None and self.__check_url():
            self.__get_button_values()

    def __get_button_values(self):
        dto = ButtonFinalValueDTO(
            label_en=self.__greek_value,
            label_el=self.__en_value,
            url_test=self.url_test_input.get().strip(),
            example_test=self.example_test_input.get().strip(),
            url_prod=self.url_prod_input.get().strip(),
            example_prod=self.example_prod_input.get().strip()
        )

        self.__button_value_callback(dto)


    def __draw(self):
        # URL
        label_frame = ttk.Frame(self)
        ttk.Label(label_frame, text="Label Config:").grid(row=0, column=0, sticky="e", padx=(0, 5))

        ttk.Label(label_frame, text="EN:").grid(row=1, column=0, sticky="e", padx=(0, 5))

        self.label_en= SpellCheckEntry(master=label_frame,
                                       spellcheck_service=self.__language_tool,
                                       locale="en_US",
                                       name="label_en",
                                       value_callback=self.__value_callback)
        self.label_en.grid(row=1, column=1, sticky="w")
        self.label_en.insert(tk.END, self.button.label_en)

        ttk.Label(label_frame, text="EL:").grid(row=1, column=2, sticky="e", padx=(0, 5))

        # Example
        self.label_el = SpellCheckEntry(master=label_frame,
                                        spellcheck_service=self.__language_tool,
                                        name="label_el",
                                        locale="el_GR",
                                        value_callback=self.__value_callback)
        self.label_el.grid(row=1, column=3, sticky="w")
        self.label_el.insert(tk.END, self.button.label_el)

        label_frame.pack()

        # URL
        url_frame = ttk.Frame(self)
        ttk.Label(url_frame, text="Url Config:").grid(row=0, column=0, sticky="e", padx=(0, 5))

        ttk.Label(url_frame, text="TEST:").grid(row=1, column=0, sticky="e", padx=(0, 5))

        self.url_test_input = UrlEntry(url_frame,is_test=True,initial_value=self.button.url_test,width=50)
        self.url_test_input.grid(row=1, column=1, sticky="w")

        self.example_test_input = UrlEntry(url_frame,is_test=True, initial_value=self.button.example_test, width=50)
        self.example_test_input.grid(row=1, column=2, sticky="w")

        self.test_url_error=ttk.Label(url_frame, text="",foreground="red")
        self.test_url_error.grid(row=2, column=1, sticky="e", padx=(0, 5))
        self.test_example_error=ttk.Label(url_frame, text="",foreground="red")
        self.test_example_error.grid(row=3, column=1, sticky="e", padx=(0, 5))

        ttk.Label(url_frame, text="PROD:").grid(row=4, column=0, sticky="e", padx=(0, 5))

        self.url_prod_input = UrlEntry(url_frame,initial_value=self.button.url_prod, width=50)
        self.url_prod_input.grid(row=4, column=1, sticky="w")

        self.example_prod_input = UrlEntry(url_frame,initial_value=self.button.example_prod, width=50)
        self.example_prod_input.grid(row=4, column=2, sticky="w")

        self.prod_url_error=ttk.Label(url_frame, text="",foreground="red")
        self.prod_url_error.grid(row=5, column=1, sticky="e", padx=(0, 5))
        self.prod_example_error=ttk.Label(url_frame, text="",foreground="red")
        self.prod_example_error.grid(row=6, column=1, sticky="e", padx=(0, 5))

        url_frame.pack()

def print_button(button_value):
    print(button_value)

if __name__ == "__main__":
    root = tk.Tk()
    root.title("UrlInput Test")

    dummy_button = ButtonFinalValueDTO(
        label_el="Παράδειγμα",
        label_en="Example",
        url_prod="https://example.com",
        example_prod="https://example.com/page",
        url_test="https://test.example.com",
        example_test="https://test.example.com/page"
    )

    widget = UrlInput(root, dummy_button,button_value_callback=print_button)
    widget.pack(padx=20, pady=20)

    ttk.Button(root, text="Check",command=widget.trigger_get_value).pack(pady=10)

    root.mainloop()

但在以下任一情况下,我都无法显示任何菜单:

self.label_el = SpellCheckEntry(master=label_frame,
                                        spellcheck_service=self.__language_tool,
                                        name="label_el",
                                        locale="el_GR",
                                        value_callback=self.__value_callback)

        self.label_el = SpellCheckEntry(master=label_frame,
                                        spellcheck_service=self.__language_tool,
                                        name="label_el",
                                        locale="el_GR",
                                        value_callback=self.__value_callback)

里面包含错误,如何修复?这个 问题 的主要区别在于:我使用一个异步进程来验证每个条目,而不是以同步的方式直接调用LanguageTool。

你知道为什么吗?

解决方案

问题在于Tkinter不是线程安全的。你的拼写检查在后台线程中运行,但你试图直接从该线程更新GUI(菜单、高亮)。Tkinter会忽略或错误处理这些更新,因此菜单永远不会显示。

修复方法:使用after() 将所有GUI更新调度到主线程执行:

def __spellcheck_callback(self, text, matches):
    def update_gui():
        # Update highlights and menu here safely
        pass
    self.after(0, update_gui)  # runs in main thread

这会让右键的建议正确显示。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章