yPhil
yPhil

Reputation: 8357

Python : How can two GTK widgets interact with each other?

What is the proper way to interact with a button without actually clicking on it?

I have a button "button", that can, upon click :

And I have a treeview, whose rows must, upon double click :

And only the 1st part works. The "foo" function is called (via a callback for the button, directly for the treeview item) and the argument ("filename") is retrieved OK, but how to execute part 2 of the job (changing "button"'s attributes, here its icon)?


import gtk

class Lister(object):

    def __init__(self):
        self.hbox = gtk.HBox()

        liststore = gtk.ListStore(str)
        liststore.append(["foo"])
        liststore.append(["bar"])
        treeview = gtk.TreeView(liststore)
        self.hbox.pack_start(treeview, False)
        cell = gtk.CellRendererText()
        col = gtk.TreeViewColumn("Column 1")
        col.pack_start(cell, True)
        col.set_attributes(cell,text=0)
        treeview.connect('row-activated', self.open_file)
        treeview.append_column(col)

    def open_file(self, button, *args):
        Buttons().the_method(self, "foo")

class Buttons(object):

    OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
    CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)

    def __init__(self):

        self.button = gtk.Button() # THIS is the button to modify
        self.hbox = gtk.HBox()
        self.hbox.pack_start(self.button, False)
        self.button.set_image(self.OPEN_IMAGE)

        self.button.connect('clicked', self.the_method, "plop")
        self.toggled = True

    def the_method(self, button, filename):
        print filename
        print vars(self)

        if self.toggled:
            self.button.set_image(self.CLOSED_IMAGE)
            self.toggled = False
        else:
            self.button.set_image(self.OPEN_IMAGE)
            self.toggled = True

class GUI(object):

    def delete_event(self, widget, event, data=None):
        gtk.main_quit()
        return False

    def __init__(self):
        self.window = gtk.Window()
        self.window.set_size_request(100, 150)
        self.window.connect("delete_event", self.delete_event)

        vbox = gtk.VBox()
        vbox.pack_start(Buttons().hbox, False, False, 1)
        vbox.pack_start(Lister().hbox)

        self.window.add(vbox)
        self.window.show_all()
        return

def main():
    gtk.main()

if __name__ == "__main__":
    GUI()
    main()

Upvotes: 1

Views: 1450

Answers (2)

liberforce
liberforce

Reputation: 11454

I strongly disagree with user1146332 answer. This is not a GTK+ issue, nor a strong design issue, just an object oriented programming issue. The cause of your bug is that you call the_method like this:

Buttons().the_method(self, "foo")

This can't work, because you're mixing up two different fundamental things: a class, and an instance of a class. When you call Buttons(), you're creating a new instance of the Buttons class. Thus, as this class is not a singleton, you're in fact creating a new instance, with a new GtkButton, and end up not interacting with the button you previously created.

The solution here is to make the Lister object aware of what it needs to modify, which means storing around the Buttons instance you previously created, for example in self.button, and calling the_method on it.

self.button.the_method("foo")

Here's a slightly modified version of your code. The important thing is that the Lister instance is now aware of the Buttons instance it needs to modify.

import gtk

class Lister(object):

    def __init__(self, button):
        self.hbox = gtk.HBox()
        self.button = button

        liststore = gtk.ListStore(str)
        liststore.append(["foo"])
        liststore.append(["bar"])
        treeview = gtk.TreeView(liststore)
        self.hbox.pack_start(treeview, False)
        cell = gtk.CellRendererText()
        col = gtk.TreeViewColumn("Column 1")
        col.pack_start(cell, True)
        col.set_attributes(cell,text=0)
        treeview.connect('row-activated', self.open_file)
        treeview.append_column(col)

    def open_file(self, button, *args):
        self.button.the_method("foo")

class Buttons(object):

    OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
    CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)

    def __init__(self):

        self.button = gtk.Button() # THIS is the button to modify
        self.hbox = gtk.HBox()
        self.hbox.pack_start(self.button, False)
        self.button.set_image(self.OPEN_IMAGE)

        self.button.connect('clicked', self.the_method, "plop")
        self.toggled = True

    def the_method(self, filename):
        print filename
        print vars(self)

        if self.toggled:
            self.button.set_image(self.CLOSED_IMAGE)
            self.toggled = False
        else:
            self.button.set_image(self.OPEN_IMAGE)
            self.toggled = True

class GUI(object):

    def delete_event(self, widget, event, data=None):
        gtk.main_quit()
        return False

    def __init__(self):
        self.window = gtk.Window()
        self.window.set_size_request(100, 150)
        self.window.connect("delete_event", self.delete_event)

        vbox = gtk.VBox()
        buttons = Buttons()
        vbox.pack_start(buttons.hbox, False, False, 1)
        vbox.pack_start(Lister(buttons).hbox)

        self.window.add(vbox)
        self.window.show_all()
        return

def main():
    gtk.main()

if __name__ == "__main__":
    GUI()
    main()

However, there's still lots of room for improvement. I suggest you don't use the __init__ function to create your widgets, but a create method that will return the toplevel widget of your widget tree. This is because you can't return anything in __init__, so it's easier to use a different method instead of raising exceptions there.

b = Buttons()
vbox.pack_start(b.create(), False, False, 1)
l = Lister(b)
vbox.pack_start(l.create(), False, False, 1)

Other improvement might be (sorry, i'm using the C naming here for GTK classes/functions, which I know better than the python one):

  • using a GtkToggleButton instead of tracking the button state yourself
  • using gtk_button_set_use_stock to tell the button to interpret the label you will set in the button as the stock id for the button (this may print the associated text too, not sure about this)
  • switching to GTK 3 (uses pyGObject), as this is GTK 2 code (uses pyGTK), unless you want Windows compatibility

See you on linuxfr :-)

Upvotes: 4

user1146332
user1146332

Reputation: 2770

First of all i don't know anything about python but i have some experiences with gtk+ and i'm more or less familiar with its concepts.

The first thing i noticed is that you define a class called GUI and two separate classes called Buttons and Lister. For me such approach makes only sense if you design the two last mentioned classes in a way that they are a kind of (composite) widget theirselves. So that you can instantiate them at a higher level for example in the GUI class. This would be a generic approach and makes perfectly sense if you want to reuse these new widgets.

The way you did it doesn't make sense to me. From what i have gathered so far the actual aim of Buttons and Lister is to populate your main application window with widgets, to connect callbacks to signals of those widgets and to define these callbacks as methods.

I think you limit the flexibility of gtk if you make it this way. For example you connect signals to callbacks at a point where in principle you aren't able to access all the widgets of your interface. In contrast, I prefer a common place in the code at which i connect signals to callbacks and at which i can principally pass all widgets of interest to a specific callback.

In fact one often have to act upon several widgets from inside a callback. So you have to consider to implement the callbacks as methods of your GUI class where they can principally access all elements of your user interface.

Also you should consider to design your interface with glade. That way your code would be much more legible.


Supplement (after some code pushing):

import gtk

class GUI(object):

    OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
    CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
    toggled = True

    def __init__(self):
        self.window = gtk.Window()
        self.window.set_size_request(100, 150)
        self.window.connect("delete_event", gtk.main_quit)

        vbox = gtk.VBox()

        self.button = gtk.Button() # THIS is the button to modify
        self.button.set_image(self.OPEN_IMAGE)

        liststore = gtk.ListStore(str)
        liststore.append(["foo"])
        liststore.append(["bar"])
        self.treeview = gtk.TreeView(liststore)
        cell = gtk.CellRendererText()
        col = gtk.TreeViewColumn("Column 1")
        col.pack_start(cell, True)
        col.set_attributes(cell,text=0)
        self.treeview.append_column(col)

        vbox.pack_start(self.button, False, False, 1)
        vbox.pack_start(self.treeview, False, False, 1)

        self.treeview.connect('row-activated', self.the_method_wrapper, "plop")
        self.button.connect('clicked', self.the_method, "plop")

        self.window.add(vbox)
        self.window.show_all()
        return

    def the_method_wrapper(self, button, *args):
        self.the_method(self, "foo")

    def the_method(self, button, filename):
        print filename
        print vars(self)

        if self.toggled:
            self.button.set_image(self.CLOSED_IMAGE)
            self.toggled = False
        else:
            self.button.set_image(self.OPEN_IMAGE)
            self.toggled = True


def main():
    gtk.main()

if __name__ == "__main__":
    GUI()
    main()

Upvotes: 0

Related Questions