Search code examples
pythonpython-3.xgtkpygobject

How to create PyGObject application with a menubar using Gtk.Builder?


There is no full documentation about how to use Gtk.Builder in PyGObject to create a menubar.

I don't use that Gtk.UIManager because it is deprecated. The example code below is based on my experience with Gtk.UIManager.

In the example should appear a menubar with Foo as a top menu group having an clickable item Bar.

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio

class Window(Gtk.ApplicationWindow):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.set_default_size(200, 100)

        #
        self.interface_info = """
        <interface>
          <menu id='TheMenu'>
            <section>
              <attribute name='foo'>Foo</attribute>
              <item>
                <attribute name='bar'>Bar</attribute>
              </item>
            </section>
          </menu>
        </interface>
        """

        builder = Gtk.Builder.new_from_string(self.interface_info, -1)

        action_bar = Gio.SimpleAction.new('bar', None)
        action_bar.connect('activate', self.on_menu)
        self.add_action(action_bar)

        menubar = builder.get_object('TheMenu')

        # layout
        self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.layout.pack_start(menubar, True, True, 0)
        self.add(self.layout)

        self.connect('destroy', Gtk.main_quit)
        self.show_all()

    def on_menu(self, widget):
        print(widget)

if __name__ == '__main__':
    win = Window()
    Gtk.main()

The current error is

Traceback (most recent call last):
  File "./_menubar.py", line 46, in <module>
    win = Window()
  File "./_menubar.py", line 36, in __init__
    self.layout.pack_start(menubar, True, True, 0)
TypeError: argument child: Expected Gtk.Widget, but got gi.repository.Gio.Menu

I am unsure about

  • How to create the XML string.
  • How to get the menubar-widget.
  • How to create Actions/Click-handlers for menu items.

Of course the question could be extended to toolbars but I wouldn't made it to complexe.

btw: I don't want to use Gtk.Application.set_menubar(). Because there is no Gtk.Application.set_toolbar() and currently I see no advantage on having a Gtk-based application object.

EDIT: I also tried this variant (without any success):

gio_menu = builder.get_object('TheMenu')
menubar = Gtk.Menubar.new_from_model(gio_menu)

Solution

  • My answer is based on a foreign answer on the gtk-dev-app mailinglist.

    I prefere Variant 3.

    Variant 1: with XML-String

    Please be aware of the different naming of the action between the XML-string (win.bar) and the Gio.SimpleAction(bar).

    #!/usr/bin/env python3
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
    from gi.repository import Gio
    
    class Window(Gtk.ApplicationWindow):
        def __init__(self):
            Gtk.Window.__init__(self)
            self.set_default_size(200, 100)
    
            #
            self.interface_info = """
            <interface>
              <menu id='TheMenu'>
                <submenu>
                  <attribute name='label'>Foo</attribute>
                  <item>
                    <attribute name='label'>Bar</attribute>
                    <attribute name='action'>win.bar</attribute>
                  </item>
                </submenu>
              </menu>
            </interface>
            """
    
            builder = Gtk.Builder.new_from_string(self.interface_info, -1)
    
            action_bar = Gio.SimpleAction.new('bar', None)
            action_bar.connect('activate', self.on_menu)
            self.add_action(action_bar)
    
            menumodel = builder.get_object('TheMenu')
            menubar = Gtk.MenuBar.new_from_model(menumodel)
    
            # layout
            self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.layout.pack_start(menubar, False, False, 0)
            self.add(self.layout)
    
            self.connect('destroy', Gtk.main_quit)
            self.show_all()
    
        def on_menu(self, action, value):
            print('Action: {}\nValue: {}'.format(action, value))
    
    if __name__ == '__main__':
        win = Window()
        Gtk.main()
    

    Variant 2: without XML but with Actions

    I prefere this variant because it doesn't use (human unreadable XML) and Gtk.Builder. Here you create the structure of your menu as a data structure based on Gio.Menu and connect a Action (which itself is connected to an event handler) to it's items. Out of that informations the widget for the menubar is kind of generated.

    #!/usr/bin/env python3
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
    from gi.repository import Gio
    
    class Window(Gtk.ApplicationWindow):
        def __init__(self):
            Gtk.Window.__init__(self)
            self.set_default_size(200, 100)
    
            action_bar = Gio.SimpleAction.new('bar', None)
            action_bar.connect('activate', self.on_menu)
            self.add_action(action_bar)
    
            # root of the menu
            menu_model = Gio.Menu.new()
    
            # menu item "Bar"
            menu_item = Gio.MenuItem.new('Bar', 'win.bar')
    
            # sub-menu "Foo" with item "Bar"
            menu_foo = Gio.Menu.new()
            menu_foo.append_item(menu_item)
            menu_model.append_submenu('Foo', menu_foo)
    
            # create menubar widget from the model
            menubar = Gtk.MenuBar.new_from_model(menu_model)
    
            # layout
            self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.layout.pack_start(menubar, False, False, 0)
            self.add(self.layout)
    
            self.connect('destroy', Gtk.main_quit)
            self.show_all()
    
        def on_menu(self, action, value):
            print('Action: {}\nValue: {}'.format(action, value))
    
    if __name__ == '__main__':
        win = Window()
        Gtk.main()
    

    Variant 3: Old-school, easy without XML, Actions or Gio layer

    This variant works kind of "old school" because you simply build your menu widgets together and connect signalls directly to them. This works without using a underlying and abstract data structure (e. g. Gio.MenuModel or an XML-string) and without a Application class.

    #!/usr/bin/env python3
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
    
    class Window(Gtk.Window):
        def __init__(self):
            Gtk.Window.__init__(self)
            self.set_default_size(200, 100)
    
            # create menubar
            menubar = self._create_menubar()
    
            # create a toolbar
            toolbar = self._create_toolbar()
    
            # layout
            self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.layout.pack_start(menubar, False, False, 0)
            self.layout.pack_start(toolbar, False, False, 0)
            self.add(self.layout)
    
            self.connect('destroy', Gtk.main_quit)
            self.show_all()
    
        def _create_menubar(self):
            # menu item 'Bar'
            item_bar = Gtk.MenuItem.new_with_label('Bar')
            item_bar.connect('activate', self.on_menu)
    
            # sub menu for 'Bar'
            menu_foo = Gtk.Menu.new()
            menu_foo.append(item_bar)
    
            # main menu 'Foo' with attached sub menu
            item_foo = Gtk.MenuItem.new_with_label('Foo')
            item_foo.set_submenu(menu_foo)
    
            # the menubar itself
            menubar = Gtk.MenuBar.new()
            menubar.append(item_foo)
    
            return menubar
    
        def _create_toolbar(self):
            toolbar = Gtk.Toolbar.new()
    
            # button with label
            bar_item = Gtk.ToolButton.new(None, 'Bar')
            bar_item.connect('clicked', self.on_menu)
            toolbar.insert(bar_item, -1)
    
            # button with icon
            bar_item = Gtk.ToolButton.new_from_stock(Gtk.STOCK_OK)
            bar_item.connect('clicked', self.on_menu)
            toolbar.insert(bar_item, -1)
    
            return toolbar
    
        def on_menu(self, caller):
            print(caller)
    
    if __name__ == '__main__':
        win = Window()
        Gtk.main()