You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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正常工作,需要调整菜单的事件处理逻辑,并确保顶层窗口获得焦点:

  1. 取消菜单的鼠标抓取并维持打开状态
    修改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)
    
  2. 强制顶层窗口获取全局焦点
    在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
    
  3. 允许事件传递到SpinButton
    原on_button_release返回True会阻止事件传递,改为返回False让事件正常到达SpinButton:

    def on_button_release(self, menu, event):
        return False
    
  4. 手动同步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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 13:45:48