Python ttk.combobox 强制打开/显示

7 投票
2 回答
4009 浏览
提问于 2025-04-19 01:06

我正在尝试扩展ttk的下拉框类,让它可以自动建议。现在的代码运行得不错,但我希望在用户输入一些文字后,能够自动显示下拉列表,而不需要移开输入框的焦点。

我遇到的问题是,找不到一种方法来强制显示下拉列表。在Python的文档中,我没有找到相关的说明,不过在tk的文档中,我发现了一个方法,似乎可以做到这一点,但在Python的封装中似乎没有实现。

我还尝试在自动建议出现后生成一个向下箭头的按键事件,虽然这样可以显示下拉列表,但会导致输入框失去焦点,而在这个事件后再尝试设置焦点似乎也不管用(焦点没有恢复)。

有没有人知道可以用来实现这个功能的函数?

我使用的代码是针对Python 3.3,并且只用了标准库:

class AutoCombobox(ttk.Combobox):
    def __init__(self, parent, **options):
        ttk.Combobox.__init__(self, parent, **options)
        self.bind("<KeyRelease>", self.AutoComplete_1)
        self.bind("<<ComboboxSelected>>", self.Cancel_Autocomplete)
        self.bind("<Return>", self.Cancel_Autocomplete)
        self.autoid = None

    def Cancel_Autocomplete(self, event=None):
        self.after_cancel(self.autoid) 

    def AutoComplete_1(self, event):
        if self.autoid != None:
            self.after_cancel(self.autoid)
        if event.keysym in ["BackSpace", "Delete", "Return"]:
            return
        self.autoid = self.after(200, self.AutoComplete_2)

    def AutoComplete_2(self):
        data = self.get()
        if data != "":
            for entry in self["values"]:
                match = True
                try:
                    for index in range(0, len(data)):
                        if data[index] != entry[index]:
                            match = False
                            break
                except IndexError:
                    match = False
                if match == True:
                    self.set(entry)
                    self.selection_range(len(data), "end")
                    self.event_generate("<Down>",when="tail")
                    self.focus_set()
                    break
            self.autoid = None

2 个回答

1

你不需要去继承 ttk.Combobox 来处理这个事件;只需使用 event_generate 来强制显示下拉菜单:

box = Combobox(...)
def callback(box):
    box.event_generate('<Down>')
2

下面展示了一种使用工具提示来实现这种用户体验的方法。这个例子是用 PySimpleGUI 实现的,但也很容易改成“纯” tkinter 的方式。

结果界面/用户体验

from functools import partial
from typing import Callable, Any

from fuzzywuzzy import process, fuzz
import PySimpleGUI as sg


# SG: Helper functions:
def clear_combo_tooltip(*_, ui_handle: sg.Element, **__) -> None:
    if tt := ui_handle.TooltipObject:
        tt.hidetip()
        ui_handle.TooltipObject = None


def show_combo_tooltip(ui_handle: sg.Element, tooltip: str) -> None:
    ui_handle.set_tooltip(tooltip)
    tt = ui_handle.TooltipObject
    tt.y += 40
    tt.showtip()


def symbol_text_updated(event_data: dict[str, Any], all_values: list[str], ui_handle: sg.Element) -> None:
    new_text = event_data[ui_handle.key]
    if new_text == '':
        ui_handle.update(values=all_values)
        return
    matches = process.extractBests(new_text, all_values, scorer=fuzz.ratio, score_cutoff=40)
    sym = [m[0] for m in matches]
    ui_handle.update(new_text, values=sym)

    # tk.call('ttk::combobox::Post', ui_handle.widget)  # This opens the list of options, but takes focus
    clear_combo_tooltip(ui_handle=ui_handle)
    show_combo_tooltip(ui_handle=ui_handle, tooltip="\n".join(sym))


# Prepare data:
all_symbols = ["AAPL", "AMZN", "MSFT", "TSLA", "GOOGL", "BRK.B", "UNH", "JNJ", "XOM", "JPM", "META", "PG", "NVDA", "KO"]

# SG: Layout
sg.theme('DarkAmber')
layout = [
    [
        sg.Text('Symbol:'),
        sg.Combo(all_symbols, enable_per_char_events=True, key='-SYMBOL-')
    ]
]

# SG: Window
window = sg.Window('Symbol data:', layout, finalize=True)
window['-SYMBOL-'].bind("<Key-Down>", "KeyDown")

# SG: Event loop
callbacks: dict[str: Callable] = {
    '-SYMBOL-': partial(symbol_text_updated, all_values=all_symbols, ui_handle=window['-SYMBOL-']),
    '-SYMBOL-KeyDown': partial(clear_combo_tooltip, ui_handle=window['-SYMBOL-']),
}
unhandled_event_callback = partial(lambda x: print(f"Unhandled event key: {event}. Values: {x}"))

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    callbacks.get(event, unhandled_event_callback)(values)


# SG: Cleanup
window.close()

这个解决方案的灵感来源于 这个链接这个讨论

撰写回答