Gtk.Menu中的Gtk.SpinButton内嵌Gtk.Entry能否获取键盘焦点?
问题
开发一款通过Gtk.Menu修改参数的应用时,遇到以下问题:
- 使用Gtk.MenuButton弹出带自菜单的Gtk.Menu,其中一个子菜单的Gtk.MenuItem内嵌了Gtk.SpinButton
- 已通过
Gdk.Window.show()让关联窗口处于Z序前端,SpinButton可接收输入,但内嵌的Gtk.Entry无法获取全局键盘焦点 - 尝试
Gtk.Widget.grab_focus()无效:Entry文本会高亮、能输入新内容,但点击别处或按回车后,SpinButton的值并未实际更新 - 已连接
change-value和value-changed信号,但输入操作不会触发这些信号 - 已知顶层Gtk.Window未获得全局焦点时,控件无法拥有全局输入焦点,寻求解决办法
最小复现代码:
import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk class Menu(Gtk.Menu): def __init__(self): Gtk.Menu.__init__(self) menu_item = Gtk.MenuItem(label="Submenu") menu_item2 = Gtk.MenuItem(label="Item") self.append(menu_item) self.append(menu_item2) submenu = Gtk.Menu() sub_mi = Gtk.MenuItem() spin_button = Gtk.SpinButton() spin_adj = Gtk.Adjustment(value=0,lower=0, upper=10, step_increment=1) spin_button.set_adjustment(spin_adj) sub_mi.add(spin_button) submenu.append(sub_mi) menu_item.set_submenu(submenu) spin_button.connect("realize", self.on_realize) spin_button.connect("map-event", self.on_map_event) submenu.connect("button-release-event", self.on_button_release) submenu.connect("enter-notify-event", self.on_enter) self.show_all() def on_realize(self, spin_button): spin_button.add_events(Gdk.EventMask.STRUCTURE_MASK) def on_map_event(self, spin_button, event): for win in spin_button.get_window().get_children(): win.show() def on_button_release(self, menu, event): return True def on_enter(self, menu, event): mouse = event.get_device() mouse.ungrab(event.time) win = Gtk.Window() win.set_default_size(100, 20) win.connect("destroy", Gtk.main_quit) mb = Gtk.MenuButton() win.add(mb) menu = Menu() mb.set_popup(menu) win.show_all() Gtk.main()
解决方案
核心问题在于Gtk.Menu是临时弹出窗口,默认会拦截焦点和事件以保证菜单的临时特性。要让SpinButton正常工作,需要调整菜单的事件处理逻辑,并确保顶层窗口获得焦点:
取消菜单的鼠标抓取并维持打开状态
修改on_enter方法,释放鼠标抓取的同时重新弹出菜单,避免菜单自动关闭:def on_enter(self, menu, event): mouse = event.get_device() mouse.ungrab(event.time) menu.popup(None, None, None, None, event.button, event.time)强制顶层窗口获取全局焦点
在SpinButton的map-event信号中,触发顶层窗口获取焦点,让内嵌Entry能获得真正的输入焦点:def on_map_event(self, spin_button, event): top_win = spin_button.get_toplevel() if top_win.is_toplevel(): top_win.present() entry = spin_button.get_child() if entry: entry.grab_focus() return False允许事件传递到SpinButton
原on_button_release返回True会阻止事件传递,改为返回False让事件正常到达SpinButton:def on_button_release(self, menu, event): return False手动同步Entry输入到调整对象
绑定Entry的activate信号,手动将输入值同步到SpinButton的调整对象,绕过菜单事件拦截导致的信号失效:def __init__(self): # ... 原有代码 ... entry = spin_button.get_child() if entry: entry.connect("activate", self.on_entry_activate, spin_adj) spin_adj.connect("value-changed", self.on_value_changed) def on_entry_activate(self, entry, adj): try: value = float(entry.get_text()) clamped_value = max(adj.get_lower(), min(value, adj.get_upper())) adj.set_value(clamped_value) except ValueError: entry.set_text(f"{adj.get_value():.0f}") def on_value_changed(self, adj): print(f"值已更新: {adj.get_value()}")
修改后的完整代码:
import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk class Menu(Gtk.Menu): def __init__(self): Gtk.Menu.__init__(self) menu_item = Gtk.MenuItem(label="Submenu") menu_item2 = Gtk.MenuItem(label="Item") self.append(menu_item) self.append(menu_item2) submenu = Gtk.Menu() sub_mi = Gtk.MenuItem() spin_button = Gtk.SpinButton() spin_adj = Gtk.Adjustment(value=0,lower=0, upper=10, step_increment=1) spin_button.set_adjustment(spin_adj) sub_mi.add(spin_button) submenu.append(sub_mi) menu_item.set_submenu(submenu) spin_button.connect("realize", self.on_realize) spin_button.connect("map-event", self.on_map_event) submenu.connect("button-release-event", self.on_button_release) submenu.connect("enter-notify-event", self.on_enter) entry = spin_button.get_child() if entry: entry.connect("activate", self.on_entry_activate, spin_adj) spin_adj.connect("value-changed", self.on_value_changed) self.show_all() def on_realize(self, spin_button): spin_button.add_events(Gdk.EventMask.STRUCTURE_MASK) def on_map_event(self, spin_button, event): top_win = spin_button.get_toplevel() if top_win.is_toplevel(): top_win.present() entry = spin_button.get_child() if entry: entry.grab_focus() return False def on_button_release(self, menu, event): return False def on_enter(self, menu, event): mouse = event.get_device() mouse.ungrab(event.time) menu.popup(None, None, None, None, event.button, event.time) def on_entry_activate(self, entry, adj): try: value = float(entry.get_text()) clamped_value = max(adj.get_lower(), min(value, adj.get_upper())) adj.set_value(clamped_value) except ValueError: entry.set_text(f"{adj.get_value():.0f}") def on_value_changed(self, adj): print(f"值已更新: {adj.get_value()}") win = Gtk.Window() win.set_default_size(100, 20) win.connect("destroy", Gtk.main_quit) mb = Gtk.MenuButton() win.add(mb) menu = Menu() mb.set_popup(menu) win.show_all() Gtk.main()
关键说明
- Gtk.Menu默认的事件拦截是为了保证菜单的临时交互逻辑,修改后需要手动维护菜单的打开状态
- 必须让顶层窗口获得全局焦点,否则内嵌控件无法获取真正的输入焦点
- 手动同步Entry输入到调整对象,是绕过菜单事件拦截导致的信号失效问题的有效方式
内容的提问来源于stack exchange,提问作者abinitio
相关产品推荐
相关产品推荐

