0
votes

I've inserted two matplotlib graphs into a container using GTK3, Python, and Glade. They plot the weather of a locale using data from a third-party API. I want the graphs to update when I enter a new locale and press a refresh button.

Each of the solutions below ended with different problems. Too many to delineate. Generally, they've managed to display another instance of the main window with the new charts, but the old instance with the old charts remain open. I've tried:

  • Destroying the parent container and then reloading an updated one. The following code produces a segmentation fault. The code tries to pass an object from a new instance of the builder to the old one. I believe this to be my main problem. I don't know how to pass the instance of the builder I'm working with into on_refresh_button_click() without having to rewrite everything into a single class.

    def on_refresh_button_click(self, widget):
    
        parent = self.get_parent()
        grandparent = parent.get_parent()
        parent.destroy()
    
        city_name = "yerevan"
        db = "test.db"
    
        get_updated_data(city_name)
    
        builder = builder_with_signals()
        read_weather_from_db(db, builder, city_name)
        grandparent.add(parent)
    
        parent.show_all()
    
  • Using self.get_parent() to acquire the button's parent container as an object to work on. This almost worked, I think. I could remove() and destroy() the container containing the graph. And I think I also successfully added the updated one. But I couldn't get it to show. The code:

    def on_refresh_button_click(self, widget):
    
        parent = self.get_parent()
    
        city_name = "yerevan"
        db = "test.db"
    
        get_updated_data(city_name)
        fig_list = read_weather_from_db(db, city_name)
    
        for child in parent:
            try:
                for grandchild in child:
                    if Gtk.Buildable.get_name(grandchild) == "chart_past":
                        parent = child # Confusing, yes.
                        old = grandchild
                        new = FigureCanvas(fig_list[0])
    
                        props = {}
                        for key in parent.list_child_properties():
                            props[key.name] = parent.child_get_property(old, key.name)
    
                        parent.remove(old)
                        parent.add(new)
    
                        for name, value in props.items():
                            parent.child_set_property(new, name, value)
    
                        parent.show_all()
                        child.show_all()
                        grandchild.show_all()
    
                        # Try to find the newly added object
                        for item in parent:
                            print("trying to find another:", Gtk.Buildable.get_name(item))
        except:
            print(Gtk.Buildable.get_name(child))
    
  • Removing the old container/widget and then adding a new one, using this code from here:

    def replace_widget(old, new):
        parent = old.get_parent()
    
        props = {}
        for key in Gtk.ContainerClass.list_child_properties(type(parent)):
            props[key.name] = parent.child_get_property(old, key.name)
    
        parent.remove(old)
        parent.add(new)
    
        for name, value in props.iteritems():
            parent.child_set_property(new, name, value)
    
  • Destroying the main window before running the script from scratch with a different locale:

    def on_refresh_button_click(self, widget):
        builder = setup_builder()
        add_signals(builder)
        window = builder.get_object("window1")
        window.destroy()
        display_data("yerevan")
    
  • Closing the program and restarting it, which doesn't make sense even to me but:

    def on_refresh_button_click(self, widget):
        Gtk.main_quit()
        display_data("yerevan")
    
  • Using canvas.draw() from here, here, and here.

  • Replacing add_with_viewport() with add() because this says it's a problem.

Also read different parts of this documentation and tried several other things but it's been a long two days so I forget.

Most examples seem to build applications with GTK3 and Python but without Glade. They also use classes. I don't want to use classes (for the moment). I'd like to see if anyone knows a solution before I rewrite the whole thing into a single class. I'm probably just misunderstanding or missing something.

I'm super new to GTK and Glade and this is my first attempt so pardon the mess. I've left out the SQL CRUD code and the code that sends requests to the API. Those work perfectly. The relevant code:

# SETUP THE BUILDER
def setup_builder():
    return Gtk.Builder()

def add_signals(builder):
    builder.add_objects_from_file('weather.xml', ('window1', 'refresh_button', 'box_charts'))

    return builder.connect_signals({'on_window1_destroy': (on_window1_destroy,'window1'),
                                    'on_refresh_button_click': (on_refresh_button_click,),
                                    })
def builder_with_signals():
    builder = setup_builder()
    add_signals(builder)
    return builder


# READ DATA FROM DATABASE
def read_weather_from_db(db, builder, city_name):
    chart_future_values = read_db(db, "chart_future", city_name)
    chart_past_values = read_db(db, "chart_past", city_name)

    fig_future = embed_chart("day and time", "temp", chart_future_values["xticks"], chart_future_values["datetimes_x_axis"], chart_future_values["temps"])
    fig_past = embed_chart("day and time", "temp", chart_past_values["xticks"], chart_past_values["datetimes_x_axis"], chart_past_values["temps"])

    add_canvas(builder, "chart_future", fig_future)
    add_canvas(builder, "chart_past", fig_past)

    return builder


# EMBED THE CHARTS INTO CONTAINERS
def embed_chart(xlabel, ylabel, xticks, xticklabels, yticks):

    fig = Figure(figsize=(5, 5), dpi=100)
    chart = fig.add_subplot(111)
    chart.set_xlabel(xlabel)
    chart.set_ylabel(ylabel)
    chart.set_xticks(xticks)
    chart.set_xticklabels(xticklabels, rotation=90)
    chart.plot(xticks, yticks)
    return fig

def add_canvas(builder, chart_container, fig):
    canvas = FigureCanvas(fig)
    subbox_chart = builder.get_object(chart_container)
    subbox_chart.add(canvas)


# THIS RUNS THE SCRIPT
def display_data(city_name="isfahan"):
    get_updated_data(city_name)
    builder = builder_with_signals()
    read_weather_from_db("test.db", builder, city_name)
    show_gtk(builder)

def on_window1_destroy(self, widget):
    Gtk.main_quit()

# HERE IS THE IMPORTANT BIT
def on_refresh_button_click(self, widget):
    # I DON'T KNOW WHAT TO PUT HERE

def show_gtk(builder):
    window_main = builder.get_object('window1')
    window_main.show_all()
    Gtk.main()

I don't think you need the Glade xml file but I'm not sure because I'm so new to this:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkWindow" id="window1">
    <property name="can_focus">False</property>
    <signal name="destroy" handler="on_window1_destroy" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child>
      <object class="GtkBox" id="box_main">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <placeholder/>
        </child>
        <child>
          <object class="GtkBox" id="box_charts">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <object class="GtkScrolledWindow" id="chart_past">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="shadow_type">in</property>
                <property name="min_content_width">500</property>
                <property name="min_content_height">500</property>
                <child>
                  <placeholder/>
                </child>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkScrolledWindow" id="chart_future">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="shadow_type">in</property>
                <property name="min_content_width">500</property>
                <property name="min_content_height">500</property>
                <child>
                  <placeholder/>
                </child>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="refresh_button">
            <property name="label" translatable="yes">refresh</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="button-press-event" handler="on_refresh_button_click" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>
3

3 Answers

1
votes

It turns out that using self.parent() as a starting point was probably the most optimal choice given that I didn't want to rewrite the whole thing as a class. But:

  • destroying/removing the child container containing the graph meant that there was now no more container to put the new graph in.
  • I assumed that I couldn't delete the graph in the child container. This assumption was wrong.

Deleting the graph took a little effort. Let's assume child is the variable holding the container object. Printing child.get_children() returned None. This was partly what led me to my wrong assumption.

But I noticed that when I tried to add() an updated graph anyway, it gave me this error: gtk_scrolled_window_add: assertion 'child_widget == NULL' failed. Something was in there.

I couldn't see it, but could I remove this child_widget anyway?

# This successfully removes any descendent in `child`.
# It leaves the container empty, so to speak.
for grandkid in child.get_children():
    child.remove(grandkid)

# This adds and displays the updated figure.
new = FigureCanvas(fig)
child.add(new)
child.show_all()

Worked.

0
votes

I am not sure I can give you an example that works with your existing code, but here is how I did it:

Figure = None
def invoice_chart_clicked (self, button):
        global Figure
        if Figure == None:
            from matplotlib.figure import Figure
            from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
            from matplotlib.pyplot import pie
            self.figure = Figure(figsize=(4, 4), dpi=100)
            canvas = FigureCanvas(self.figure)  # a Gtk.DrawingArea
            canvas.set_size_request(800, 500)
            overlay = self.builder.get_object('overlay1')
            overlay.add (canvas)
        a = self.figure.add_subplot(111)
        labels = list()
        fractions = list()
        unpaid = 0
        self.cursor.execute("SELECT SUM(amount_due), c.name FROM invoices "
                            "JOIN contacts AS c ON c.id = invoices.customer_id "
                            "WHERE (canceled, paid, posted) = "
                            "(False, False, True) GROUP BY customer_id, c.name "
                            "ORDER BY SUM(amount_due)")
        for row in self.cursor.fetchall():
            customer_total = row[0]
            customer_name = row[1]
            fractions.append(customer_total)
            labels.append(customer_name)
            unpaid += 1
        if unpaid == 0:
            labels.append("None")
            fractions.append(1.00)
        a.pie(fractions, labels=labels, autopct='%1.f%%', radius=0.7)
        window = self.builder.get_object('window1')
        window.show_all()

Every time I reload this function, the plot will be regenerated. You can find the full code here. I never ran any tests to see that all memory is properly freed, etc. Maybe it will give enough to go from there.

0
votes

Create a separate plot window like here and the button in the GUI will trigger the refresh? https://github.com/f4iteightiz/UWR_simulator A continuous running funcanimation should give to you the required "refresh" function.