gtkmm: How do I use a custom widget in a GtkBuilder XML file?

771 views Asked by At

I'm writing a gtkmm-4 application, and want to use a custom widget with GtkBuilder XML. If the widget is constructed from C++, it works fine. However, it doesn't render when constructed from XML.

NOTE: This question might be considered a duplicate of this one. I am not asking about using Glade with my widget; my widget simply does not work with plain, hand-written XML.

I'm sorry for posting this much code, but I believe this is about as minimal as I can get it.

Custom widget code

  • joystick.hpp
#ifndef _TASDI2_JOYSTICK_HPP_
#define _TASDI2_JOYSTICK_HPP_
#include <gtkmm.h>

namespace tasdi2 {
  class Joystick : public Gtk::Widget {
  public:
    Joystick();
    virtual ~Joystick() {}

    virtual Gtk::SizeRequestMode get_request_mode_vfunc() const override {
      return Gtk::Widget::get_request_mode_vfunc();
    }

    virtual void measure_vfunc(
      Gtk::Orientation orientation, int for_size, int& minimum, int& natural,
      int& minimum_baseline, int& natural_baseline) const override;
    
    void on_map() override;
    void on_unmap() override;
    
    void snapshot_vfunc(const Glib::RefPtr<Gtk::Snapshot> &snapshot) override;
    
    
    
    Glib::PropertyProxy<int> property_xpos() {
      return prop_xpos.get_proxy();
    }
    Glib::PropertyProxy_ReadOnly<int> property_xpos() const {
      return prop_xpos.get_proxy();
    }
    Glib::PropertyProxy<int> property_ypos() {
      return prop_ypos.get_proxy();
    }
    Glib::PropertyProxy_ReadOnly<int> property_ypos() const {
      return prop_ypos.get_proxy();
    }
  private:
    Glib::Property<int> prop_xpos;
    Glib::Property<int> prop_ypos;
  };
}  // namespace tasdi2
#endif
  • joystick.cpp
#include "joystick.hpp"
#include <gdkmm.h>
#include <gtkmm.h>
#include <iostream>
#include <numbers>

namespace {
  inline void circle(
    const Glib::RefPtr<Cairo::Context>& cairo, double cx, double cy, double r) {
    cairo->arc(cx, cy, r, 0, 2 * std::numbers::pi);
  }

  inline void line(
    const Glib::RefPtr<Cairo::Context>& cairo, double x1, double y1, double x2,
    double y2) {
    cairo->move_to(x1, y1);
    cairo->line_to(x2, y2);
  }
}  // namespace

namespace tasdi2 {
  Joystick::Joystick() :
    Glib::ObjectBase("Tasdi2Joystick"),
    Gtk::Widget(),
    prop_xpos(*this, "xpos", 0),
    prop_ypos(*this, "ypos", 0) {
    set_hexpand();
    set_hexpand_set();
    set_vexpand();
    set_vexpand_set();
  }

  void Joystick::measure_vfunc(
    Gtk::Orientation orientation, int for_size, int& minimum, int& natural,
    int& minimum_baseline, int& natural_baseline) const {
    minimum          = 128;
    natural          = 160;
    minimum_baseline = -1;
    natural_baseline = -1;
    return;
  }

  void Joystick::on_map() { Gtk::Widget::on_map(); }
  void Joystick::on_unmap() { Gtk::Widget::on_unmap(); }

  void Joystick::snapshot_vfunc(const Glib::RefPtr<Gtk::Snapshot>& snapshot) {
    const auto space = get_allocation();
    const Gdk::Rectangle rect(0, 0, space.get_width(), space.get_height());
    std::cout << "Allocation area: " << space.get_x() << ", " << space.get_y() << ", " << space.get_width() << ", " << space.get_height() << "\n";

    auto cairo = snapshot->append_cairo(rect);

    const double w  = space.get_width();
    const double h  = space.get_height();
    const double cx = w / 2;
    const double cy = h / 2;
    // colors
    const Gdk::RGBA color_bg0("#7F7F7F");
    const Gdk::RGBA color_bg1("#FFFFFF");
    const Gdk::RGBA color_oln("#000000");
    const Gdk::RGBA color_cln("#0000FF");
    const Gdk::RGBA color_dot("#FF0000");

    cairo->rectangle(0, 0, w, h);
    Gdk::Cairo::set_source_rgba(cairo, color_bg0);
    cairo->fill();

    circle(cairo, cx, cy, cx);
    Gdk::Cairo::set_source_rgba(cairo, color_bg1);
    cairo->fill_preserve();

    line(cairo, cx, 0, cx, h);
    line(cairo, 0, cy, w, cy);
    Gdk::Cairo::set_source_rgba(cairo, color_oln);
    cairo->stroke();
  }
}  // namespace tasdi2

MVE test code

  • In XML
#include <gtkmm.h>
#include "joystick.hpp"

static const std::string ui_data = R"(
<interface>
  <object class="GtkAspectFrame" id="root">
    <property name="margin-start">10</property>
    <property name="margin-end">10</property>
    <property name="margin-top">10</property>
    <property name="margin-bottom">10</property>
    
    <child>
      <object class="gtkmm__CustomObject_Tasdi2Joystick" />
    </child>
  </object>
</interface>
)";

class MainWindow : public Gtk::Window {
public:
  MainWindow() :
    builder(Gtk::Builder::create_from_string(ui_data)) {
    
    set_child(*builder->get_widget<Gtk::AspectFrame>("root"));
    set_size_request(256, 256);
  }
protected:
  Glib::RefPtr<Gtk::Builder> builder;
};

int main(int argc, char* argv[]) {
  auto app = Gtk::Application::create("io.github.jgcodes2020.testapp");
  app->signal_startup().connect([&]() {
    tasdi2::Joystick joystick;
  });
  return app->make_window_and_run<MainWindow>(argc, argv);
}

Empty window, nothing visible

  • in C++
#include <gtkmm.h>
#include "joystick.hpp"

static const std::string ui_data = R"(
<interface>
  <object class="GtkAspectFrame" id="root">
    <property name="margin-start">10</property>
    <property name="margin-end">10</property>
    <property name="margin-top">10</property>
    <property name="margin-bottom">10</property>
  </object>
</interface>
)";

class MainWindow : public Gtk::Window {
public:
  MainWindow() :
    builder(Gtk::Builder::create_from_string(ui_data)) {
    
    auto& root = *builder->get_widget<Gtk::AspectFrame>("root");
    root.set_child(stick);
    
    set_child(root);
    set_size_request(256, 256);
  }
protected:
  Glib::RefPtr<Gtk::Builder> builder;
  tasdi2::Joystick stick;
};

int main(int argc, char* argv[]) {
  auto app = Gtk::Application::create("io.github.jgcodes2020.testapp");
  return app->make_window_and_run<MainWindow>(argc, argv);
}

Widget showing with circle and crosshair

1

There are 1 answers

0
NoDakker On BEST ANSWER

Using the comments above about including derived widgets, I reviewed the sample code out on the Gnome developer site and reviewed the class constructor for your "Joystick" widget. I left your standard constructor alone as it would be useful for any widget directly built in your code. Then, using the derived widget example on the developer site, I revised the "joystick.hpp" header file to include the builder constructor prototype as follows.

Joystick();
Joystick(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade); /* New builder function for a derived widget */
virtual ~Joystick() {}

Then within the "tasdi2" namespace in file "joystick.cpp", I added the derived builder constructor function, basically cloning the detail within the standard constructor for the "Joystick" class.

Joystick::Joystick(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& /* refGlade */)
: // To register custom properties, you must register a custom GType.  If
  // you don't know what that means, don't worry, just remember to add
  // this Glib::ObjectBase constructor call to your class' constructor.
  // The GType name will be gtkmm__CustomObject_Tasdi2Joystick.
  Glib::ObjectBase("Tasdi2Joystick"),
  Gtk::Widget(cobject),
  prop_xpos(*this, "xpos", 0),
  prop_ypos(*this, "ypos", 0)
{
    set_hexpand();
    set_hexpand_set();
    set_vexpand();
    set_vexpand_set();
}

Finally, within the "XML" version of "main.cpp", I revised the code to accommodate the derived widget. In your XML text body, I added an "ID" attribute so that calling the derived builder function later on in the program has a hook to the widget.

<object class="gtkmm__CustomObject_Tasdi2Joystick" id="stick_range"/>

Along with the addition of an "ID" attribute, I included the building of the custom joystick widget within the "MainWindow" constructor.

  MainWindow() :
    builder(Gtk::Builder::create_from_string(ui_data)) {
    set_child(*builder->get_widget<Gtk::AspectFrame>("root"));
    set_size_request(256, 256);
    tasdi2::Joystick * stick_range = nullptr;
    stick_range = Gtk::Builder::get_widget_derived<tasdi2::Joystick>(builder, "stick_range");
    if (stick_range == nullptr) 
       printf("Joystick widget definition was not found\n");
  }

The net result of combining your "XML" version of "main.cpp" with the additional changes displayed the widget.

Sample Joystick

You may already have figured this stuff out from the previous comments, but if not, these additional code snippets may be of use to you.

Regards.