How to repeatedly show a Dialog with PyGTK / Gtkbuilder?
Asked Answered
P

3

11

I have created a PyGTK application that shows a Dialog when the user presses a button. The dialog is loaded in my __init__ method with:

builder = gtk.Builder()
builder.add_from_file("filename")
builder.connect_signals(self) 
self.myDialog = builder.get_object("dialog_name")

In the event handler, the dialog is shown with the command self.myDialog.run(), but this only works once, because after run() the dialog is automatically destroyed. If I click the button a second time, the application crashes.

I read that there is a way to use show() instead of run() where the dialog is not destroyed, but I feel like this is not the right way for me because I would like the dialog to behave modally and to return control to the code only after the user has closed it.

Is there a simple way to repeatedly show a dialog using the run() method using gtkbuilder? I tried reloading the whole dialog using the gtkbuilder, but that did not really seem to work, the dialog was missing all child elements (and I would prefer to have to use the builder only once, at the beginning of the program).


[SOLUTION] (edited)
As pointed out by the answer below, using hide() does the trick. I first thought you still needed to catch the "delete-event", but this in fact not necessary. A simple example that works is:


import pygtk
import gtk

class DialogTest:

    def rundialog(self, widget, data=None):
        self.dia.show_all()
        result = self.dia.run() 
        self.dia.hide()


    def destroy(self, widget, data=None):
        gtk.main_quit()

    def __init__(self):
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.connect("destroy", self.destroy)

        self.dia = gtk.Dialog('TEST DIALOG', self.window, 
           gtk.DIALOG_MODAL  | gtk.DIALOG_DESTROY_WITH_PARENT)
        self.dia.vbox.pack_start(gtk.Label('This is just a Test'))


        self.button = gtk.Button("Run Dialog")    
        self.button.connect("clicked", self.rundialog, None)
        self.window.add(self.button)
        self.button.show()
        self.window.show()



if __name__ == "__main__":
    testApp = DialogTest()
    gtk.main()
Pollack answered 11/1, 2011 at 11:56 Comment(0)
S
7

Actually, read the documentation on Dialog.run(). The dialog isn't automatically destroyed. If you hide() it when the run() method exits, then you should be able to run() it as many times as you want.

Alternatively, you can set the dialog to be modal in your builder file, and then just show() it. This will achieve an effect that's similar, but not quite the same as run() - because run() creates a second instance of the main GTK loop.

EDIT

The reason you are getting a segmentation fault if you don't connect to the delete-event signal is that you are clicking the close button twice. Here is what happens:

  1. You click "Run Dialog", this calls the dialog's run() method.
  2. The modal dialog appears, and starts its own main loop.
  3. You click the close button. The dialog's main loop exits, but since run() overrides the normal behavior of the close button, the dialog is not closed. It is also not hidden, so it hangs around.
  4. You wonder why the dialog is still there and click the close button again. Since run() is not active anymore, the normal behavior of the close button is triggered: the dialog is destroyed.
  5. You click "Run Dialog" again, which tries to call the run() method of the destroyed dialog. Crash!

So if you make sure to hide() the dialog after step 3, then everything should work. There's no need to connect to the delete-event signal.

Shayna answered 11/1, 2011 at 22:14 Comment(4)
Well, the documentation is not really clear here. It seems that the "run" method itself does not destroy the dialog, but if you close it by pressing on the "close" control provided by the window manager (usually a little "X" on the top right of the window", the dialog gets destroyed if you do not catch the "delete-event"Pollack
@Julian, it's the other way around - see the second paragraph of the documentation. The dialog will not be destroyed even if you close the window.Shayna
Yes I read it but I am a bit confused: If in the example I posted above I comment out the line self.dia.connect("delete-event", self.closedialog) I get a Segmentation Fault the second time I click the button. So SOMETHING has been destroyed without me doing it, right? Thank you for your clarifications by the way.Pollack
@Julian, just remove the closedialog function altogether, and put self.dia.hide() after the run() call. This will do what you want. The problem is that you are clicking the close button twice when you get the segfault. See my edited answer for the reason why.Shayna
A
2

Your dialog should only need to run once. Assuming a menu item triggers the dialog, the code should look something like this:

def on_menu_item_clicked(self, widget, data=None):
    dialog = FunkyDialog()
    response = dialog.run()

    if response = gtk.RESPONSE_OK:
        // do something with the dialog data

    dialog.destroy()

dialog.run() is a blocking main-loop that returns when the dialog send a response. This is normally done via the Ok and Cancel buttons. When this happens, the dialog is finished and needs to be destroyed.

To show the dialog repeatedly, the user should follow the same workflow (in the example above, that would be clicking on a menu item). The dialog is responsible, in __init__, for setting itself up. If you hide() the dialog, you have the problem of communicating with that dialog so it stays up-to-date with the rest of the application even when it's hidden.

One of the reasons some people want to "run the dialog repeatedly" is because the user has entered invalid information, and you want to give the user the opportunity to correct it. This must be dealt with in the dialog's response signal handler. The order of events in a dialog is:

  1. User physically pushes the Ok button
  2. Dialog sends the response gtk.RESPONSE_OK (-5)
  3. Dialog calls the handler for the response signal
  4. Dialog calls the handler for the Ok button
  5. Dialog run() method returns the response

To prevent steps 4 and 5 from happening, the response handler must suppress the response signal. This is achieved as follows:

def on_dialog_response(self, dialog, response, data=None:
    if response == gtk.RESPONSE_OK:
        if data_is_not_valid:
            # Display an error message to the user

            # Suppress the response
            dialog.emit_stop_by_name('response')
Auberbach answered 22/1, 2011 at 10:2 Comment(2)
Well, but I use GtkBuilder to get the dialog and once the dialog is destroyed, it stays destroyed - I could not find a way to set it up again.Pollack
destroy() does exactly that, it destroys the object. If you want to show the same dialog again, you have to create another instance. Read pygtk.org/docs/pygtk/… for more information on destroy(), most notably: "If the widget is a toplevel (derived from gtk.Window), it will be removed from the list of toplevels, and the reference PyGTK holds to it will be removed."Auberbach
W
2

I just spent some time figuring this out. Re-fetching the same object from a builder will not create a new instance of the object, but only return a reference to the old (destroyed) object. If you create a new builder instance, however, and load your file into the new builder, it will create a new instance.

So my dialog creation function looks something like this:

def create():
    builder = gtk.Builder()
    builder.add_from_file('gui/main.ui')

    dlg = builder.get_object('new_dialog')

    def response_function(dialog, response_id):
        ... do stuff ...
        dialog.destroy()

    dlg.connect('response', response_function)
    dlg.show_all()

Note that I am not blocking for a response with run() in this case because I'm using twisted, but it should be equivalent.

Wangle answered 16/2, 2011 at 1:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.