Search code examples
c++xmlgtkgtkmm

How to correctly create a simple GTK application in C++ with header, class definition and builder XML external file?


I have learned how to create a simple GTK application using C++ and gtkmm-3.0.

So far I have learned two possible approaches, and I would like a third approach, which would be the merging of those two.

First approach: 3 files (header, constructor, main). Widgets in the code

In this approach I write three files:

  1. window.h where the MainWindow class is declared
#pragma once
#include <gtkmm.h>

class MainWindow : public Gtk::Window
{
public:
 MainWindow();
};
  1. window.cpp where the constructor of the class is written
#include "window.h"

MainWindow::MainWindow()
{
  set_title("Simple application");
  set_default_size(200, 200);
}

  1. main.cpp where the app->run() launches the window and runs the 'event loop'
#include "window.h"

int main(int argc, char* argv[])
{
  auto app = Gtk::Application::create("org.gtkmm.window");

  MainWindow w;

  return app->run(w);
}

Second approach: 1 code file. Widgets in XML file

In this approach I have only one 'code' file, main.cpp:

#include <gtkmm.h>
#include <iostream>

Gtk::Window* gWindow = nullptr;

int main(int argc, char const *argv[])
{
    auto app = Gtk::Application::create("org.gtkmm.window");

    auto gladeBuilder = Gtk::Builder::create();

    gladeBuilder->add_from_file("builder.ui");

    gladeBuilder->get_widget("MainWindow", gWindow);

    app->run(*gWindow);
    return 0;
}

and then I have an XML file builder.ui where the widgets are defined:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <requires lib="gtk+" version="3.22"/>
  <object class="GtkWindow" id="MainWindow">
    <property name="default_width">400</property>
    <property name="default_height">400</property>
  </object>
</interface>

The advantage of the first approach is to separate code in different files, which becames useful when writing more complex applications, e.g. with more than one window. The advantage of the second approach is to have the widgets in a separate file, and the possibility to use a graphical application to design more complex UI.

So, I would like a third approach which merges those two, combining all the advantages.

The problem I see is how to write the class for the main window, because it would be both in the window.cpp code file, and also in the XML builder.ui file.

I tried a naive idea, modifying the window.cpp file as:

#include "window.h"


MainWindow::MainWindow()
{
    auto builder = Gtk::Builder::create();

    builder->add_from_file("builder.ui");

    builder->get_widget("MainWindow", (*this) );
}

i.e. having the same MainWindow class defined in both the code and the XML file, but I know it is too naive, and indeed I get an error.

I have some experience in writing GUI in Python, and I am a newbie in doing it in C++, any help is appreciated!


Solution

  • After some research, including some questions in Stack Overflow (here, here and here) I have put together the following code for a simple application, with few widgets and some connected signals, which fulfills the requirements in the question:

    myWindow.h

    #pragma once
    
    #include <gtkmm/window.h>
    #include <gtkmm/builder.h>
    #include <gtkmm/button.h>
    #include <gtkmm/entry.h>
    #include <gtkmm/label.h>
    
    class MainWindow : public Gtk::Window
    {
    public:
        MainWindow(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& p_builder);
    
    protected:
        Glib::RefPtr<Gtk::Builder> m_builder;
        Glib::RefPtr<Gtk::Button> m_button;
        Glib::RefPtr<Gtk::Entry> m_entry;
        Glib::RefPtr<Gtk::Label> m_label;
    
        // Signal handler:
        void on_button_clicked();
    };
    

    myWindow.cpp

    #include "myWindow.h"
    #include <gtkmm/builder.h>
    #include <gtkmm/button.h>
    #include <iostream>
    
    MainWindow::MainWindow(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& p_builder)
    : Gtk::Window(cobject), m_builder{p_builder}
    {
    
        Gtk::Button* p_button = nullptr; // Create a raw pointer to a button
        Gtk::Entry* p_entry = nullptr; // Create a raw pointer to an entry field
        Gtk::Label* p_label = nullptr; // Create a raw pointer to a label
    
    
        m_builder->get_widget("button1", p_button); // Retrieve the button from XML file
        m_button = Glib::RefPtr<Gtk::Button>(p_button); // Assign to m_button
    
        m_builder->get_widget("entry1", p_entry); // Retrieve the entry from XML file
        m_entry = Glib::RefPtr<Gtk::Entry>(p_entry); // Assign to m_entry
    
        m_builder->get_widget("label1", p_label); // Retrieve the label from XML file
        m_label = Glib::RefPtr<Gtk::Label>(p_label); // Assign to m_label
    
    
        // connect the button click event to the function
        m_button->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_button_clicked));
    }
    
    void MainWindow::on_button_clicked()
    {
        m_label->set_label(m_entry->get_text());
    }
    

    main.cpp

    #include <iostream>
    #include <cstring>
    #include <gtkmm/application.h>
    #include <gtkmm/builder.h>
    #include "myWindow.h"
    
    int main(int argc, char** argv)
    {
        // Create an application object. This is mandatory since it initializes
        // the toolkit. If you don't do this, widgets won't show.
        auto app = Gtk::Application::create(argc, argv);
    
        // Since you want to use the builder, you need to instantiate one. You
        // can do this directly using your UI file.
        auto builder = Gtk::Builder::create_from_file("builder.ui");
    
        // At this point, you are ready to create your window. The builder is
        // going to create an instance for you, and you get a handle to this
        // istance to later use.
        // First, you create a MainWindow pointer, which points to nothing. It
        // is going to be used by the builder to get you the handle.
        MainWindow* window = nullptr;
    
        // Then, using you builder instance. You call get_widget_derived and
        // pass the pointer to get the handle.
        builder->get_widget_derived("mainWindow", window);
    
        // Check if the window was created successfully
        if (window) {
    
            // You then show the window by using you handle (i.e window).
            app->run(*window);
        } else {
            std::cerr << "Failed to create main window." << std::endl;
            return 1;
        }
    
        return 0;
    }
    

    (the great comments in this code are from @BobMorane, written in the answer to this question)

    builder.ui

    <?xml version='1.0' encoding='UTF-8'?>
    <interface>
      <requires lib="gtk+" version="3.20"/>
      <object class="GtkWindow" id="mainWindow">
        <property name="default-height">300</property>
        <property name="default-width">400</property>
        <property name="title">Simple copy</property>
        <child>
          <object class="GtkGrid" id="grid">
            <property name="visible">True</property>
            <child>
              <object class="GtkLabel" id="label1">
                <property name="visible">True</property>
                <property name="width-request">2</property>
              </object>
              <packing>
                <property name="height">2</property>
                <property name="left-attach">0</property>
                <property name="top-attach">0</property>
                <property name="width">2</property>
              </packing>
            </child>
            <child>
              <object class="GtkEntry" id="entry1">
                <property name="visible">True</property>
                <property name="width-request">2</property>
              </object>
              <packing>
                <property name="left-attach">0</property>
                <property name="top-attach">3</property>
                <property name="width">2</property>
              </packing>
            </child>
            <child>
              <object class="GtkButton" id="button1">
                <property name="label">copy</property>
                <property name="visible">True</property>
                <property name="width-request">2</property>
              </object>
              <packing>
                <property name="left-attach">0</property>
                <property name="top-attach">2</property>
              </packing>
            </child>
          </object>
        </child>
      </object>
    </interface>
    
    

    This code builds and runs, creating a window with a label, a button and an entry field. A click on the button copies to the label the text entred in the field. One of the key aspects is the datatype of the pointer used to handle the different widgets.