在Tkinter中使用Toplevel制作弹出键盘

2 投票
2 回答
2213 浏览
提问于 2025-04-18 01:35

我有一个小模块,当一个 Entry 组件获得焦点时,它会弹出一个 Toplevel 窗口。这个 Toplevel 窗口是一个键盘,所以它会等待按钮点击,然后把这个点击的内容插入到 Entry 组件中。这个 Toplevel 窗口需要在两种情况下关闭:1)用户按下他们实际键盘上的某个键,2)Entry 组件的父窗口被移动或调整大小。

一切都正常,除了一个小问题:如果用户点击了 Toplevel 窗口,它就会变得活跃,而如果发生了某个关闭事件,我会得到一些意想不到的结果(比如当 Entry 再次获得焦点时,弹窗又出现了)。

我在想,如果我能让 Entry 在整个过程中保持焦点,一切就会正常,但我还没找到实现这个的方法。

这里有个例子,我尽量简化了代码,同时保留了模块的结构。注意:使用的是 Python 2.7。

from Tkinter import *

class Top(Toplevel):
    def __init__(self, attach):
        Toplevel.__init__(self)
        self.attach = attach
        Button(self, text='Button A', command=self.callback).pack()

        self.bind('<Key>', self.destroyPopup)

    def callback(self):
        self.attach.insert(END, 'A')

    def destroyPopup(self, event):
        self.destroy()

class EntryBox(Frame):
    def __init__(self, parent, *args, **kwargs):
        Frame.__init__(self, parent)
        self.parent = parent

        self.entry = Entry(self, *args, **kwargs)
        self.entry.pack()

        self.entry.bind('<FocusIn>', self.createPopup)
        self.entry.bind('<Key>', self.destroyPopup)
        self.parent.bind('<Configure>', self.destroyPopup)

    def createPopup(self, event):
        self.top = Top(self.entry)

    def destroyPopup(self, event):
        try:
            self.top.destroy()
        except AttributeError:
            pass

root = Tk()

e1 = EntryBox(root).pack()

root.mainloop()

所以,我有没有找到什么 never_get_focus() 方法可以应用到 Toplevel 窗口上,还是我在这个问题上走错了方向,或者还有其他什么?任何帮助都很感激。

编辑:我找到了一种临时解决方案,似乎有效,但我觉得还有更好的处理方法我还没找到。

这是我在 Frame 子类的弹出方法中添加的内容:

def createPopup(self, event):
    try:                           #When focus moves in to the entry widget,
        self.top.destroy()         #try to destroy the Toplevel
        del self.top               #and remove the reference to it
    except AttributeError:         #unless it doesn't exist,
        self.top = Top(self.entry) #then, create it

def destroyPopup(self, event):
    try:
        self.top.destroy()
        del self.top
    except AttributeError:
        pass

我想加个悬赏,因为我想看看有没有其他更简洁的方法来实现这个。我想要的步骤是:

  1. 焦点移动到 Entry 组件
  2. 创建一个弹出的 Toplevel 窗口(这是一个键盘)
  3. 当发生以下情况时关闭 Toplevel 窗口:a)实际键盘上按下一个键,b)焦点从 Entry 组件移动到其他组件或离开 GUI,c)主窗口被移动
  4. 如果焦点稍后再次移动回 Entry 组件,这个过程可以重复

2 个回答

0

这可能对你有帮助:让tkinter组件获取焦点

如果你真的想要改变焦点,你可以尝试写一些代码来实现这个功能。

我可能不太明白你具体在做什么,但我觉得它表现得奇怪的原因可能是因为你在顶层窗口上设置的绑定。

def destroyPopup(self, event):
    self.destroy()

正在使用:

self.entry.bind('<FocusIn>', self.createPopup)

不确定这是否有帮助,但你可以考虑添加一些:

print "____ is triggered"

到每个方法中,看看在你切换焦点时到底发生了什么,这可能有助于找出问题所在。

2

你可以用状态机来处理你描述的行为。状态机在图形用户界面中很常见,用来实现各种行为。下面是一个简单的例子,给你一个大概念。

首先设计状态机,这里有一个简单的例子,几乎能满足你的需求(为了简洁,省略了配置部分)。

fsm picture

在实现方面,你可以选择一个现成的库,自己搭建一个框架,或者用传统的嵌套if语句。接下来是我快速而简单的实现。

调整订阅以创建状态,并将事件重定向到状态机:

    self.state = "idle"
    self.entry.bind('<FocusIn>', lambda e:self.fsm("focus_entry"))
    self.entry.bind('<FocusOut>', lambda e:self.fsm("focus_out_entry"))
    self.entry.bind('<Key>', lambda e:self.fsm("key_entry"))
    self.parent.bind('<Configure>', lambda e:self.fsm("configure_parent"))

选择你想要处理的状态和事件组合,并设置相应的动作。你可能会发现自己被困在某个状态中,这时可以相应地调整你的状态机。

def fsm(self, event):
    old_state = self.state #only for logging
    if self.state == "idle":
        if event == "focus_entry":
            self.createPopup()
            self.state = "virtualkeyboard"
    elif self.state == "virtualkeyboard":
        if event == "focus_entry":
            self.destroyPopup()
            self.state = "typing"
        if event == "focus_out_entry":
            self.destroyPopup()
            self.state = "idle"
        elif event == "key_entry":
            self.destroyPopup()
            self.state = "typing"
    elif self.state == "typing":
        if event == "focus_out_entry":
            self.state = "idle"
    print "{} --{}--> {}".format(old_state, event, self.state)

撰写回答