How to repeatedly show a Dialog with PyGTK / Gtkbuilder? - python

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()

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:
You click "Run Dialog", this calls the dialog's run() method.
The modal dialog appears, and starts its own main loop.
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.
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.
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.

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:
User physically pushes the Ok button
Dialog sends the response gtk.RESPONSE_OK (-5)
Dialog calls the handler for the response signal
Dialog calls the handler for the Ok button
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')

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.

Related

How to intercept QProgressDialog cancel click

I have a standard QProgressDialog with a cancel button. If/When the user clicks the cancel button, I don't want the dialog to immediately hide, instead I would prefer to disable the cancel button and perform some clean-up work, and then close the QProgressDialog once I'm sure this work is complete.
How to I intercept the current function?
From the docs it seems like I should be overwriting:
PySide.QtGui.QProgressDialog.cancel()
Resets the progress dialog. PySide.QtGui.QProgressDialog.wasCanceled()
becomes true until the progress dialog is reset. The progress dialog
becomes hidden.
I've tried subclassing this method but it doesn't even seem to be called when I click the cancel button.
To disable the button of the dialog you have to get a reference to it. Since it is a basic QPushButton, you can use findChild():
dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.setEnabled(False)
Consider that disabling a button that would never get enabled is annoying from the UX point of view, so a better choice would be to not show it at all, and setCancelButton() explains how to do it:
If nullptr is passed, no cancel button will be shown.
In python terms, nullptr means None:
dialog = QProgressDialog(self)
dialog.setCancelButton(None)
Unfortunately, this won't prevent the user to cancel the dialog by closing it or by pressing Esc.
This is valid for any QDialog, and, to properly avoid that, subclassing is the better choice: you need to prevent rejecting the dialog (the Esc key) and the close event. While they have similar results, they are handled in different ways.
Overriding reject() (and doing nothing) prevents any action that would trigger a rejection (cancelling), including pressing Esc.
Overriding the closeEvent() requires an extra step: you have to ensure that the event is spontaneous() (triggered by the system - normally, the user presses the close button of the window), and eventually ignore that. This is necessary as you might need to call close() or accept() to actually close the dialog upon completing the process.
class NonStopProgressDialog(QtWidgets.QProgressDialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setCancelButton(None)
def reject(self):
pass
def closeEvent(self, event):
if event.spontaneous():
event.ignore()
Note that there is no direct way to know if the spontaneous close event is directly triggered by the user (trying to close the window), or the system (while shutting down).
Also note that if you do need to close the dialog programmatically, you either call accept(), or you call the base implementation, which allows you to get the proper return value from the dialog's reject():
def rejectNoMatterWhat(self):
super().reject()
Finally, if, for any reason, you still need the cancel button, you have to disconnect its signals.
In general, this might do the work:
dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.disconnect()
But the above would disconnect any signal to any slot, and there are some cases for which this should be avoided.
We know from the sources that the clicked signal is actually connected to the canceled() slot, so a better solution would be to do the following instead:
dialog = QProgressDialog(self)
cancelButton = dialog.findChild(QPushButton)
cancelButton.clicked.disconnect(self.canceled)
Since you may need to be notified about that in the parent/main class, a more appropriate solution would be to create a custom signal in the subclass used above:
class NonStopProgressDialog(QtWidgets.QProgressDialog):
userCancel = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
cancelButton = self.findChild(QPushButton)
cancelButton.clicked.disconnect(self.canceled)
cancelButton.clicked.connect(
lambda: cancelButton.setEnabled(False))
cancelButton.clicked.connect(self.userCancel)
def reject(self):
pass
def closeEvent(self, event):
if event.spontaneous():
event.ignore()
class SomeWindow(QtWidgets.QWidget):
def showProgress(self):
self.progressDialog = NonStopProgressDialog(self)
self.progressDialog.userCancel.connect(self.stopSomething)
# ...
def stopSomething(self):
self.progressDialog.setCancelButtonText('Please wait')
# do something...

Button behaviour

I work with Python 3.5 and TKinter.
I defined a label and file dialog that updates this label.
A button is responsible to launch this dialog.
self.sel_folder_val = the label that will be updated.
The code:
self.sel_folder_val['text']=filedialog.askdirectory()
After pressing the button in order to launch this dialog, the button stays pressed. Any dialog that a button is responsible to open cause the button to stay low (pressed) after closing this dialog.
I have tried this also with no help...:
self.select_folder_btn.config(relief=RAISED)
Code example:
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
def update_folder_value(self,event):
self.sel_folder_val['text']=filedialog.askdirectory()
return
After executing update_folder_value() function, self.select_folder_btn stays down.
I used the command:
self.select_folder_btn.configure(command=self.update_folder_value)
Instead of bind:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
It solved my problem.
Thanks
First for future reference this is a minimal working example:
from Tkinter import *
import tkFileDialog as filedialog
class app:
def __init__(self):
self.top = Tk()
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
#self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.configure(command=self.update_folder_value)
self.sel_folder_val = {}
self.top.mainloop()
def update_folder_value(self):
self.sel_folder_val['text']=filedialog.askdirectory()
self.top.update_idletasks()
app()
and even that's not minimal. Second your problem is hard to find since this isn't minimal- you're doing something really weird - binding the button to a click. You're overriding the built-in binding, and apparently it still affects the state of the button on press, but not going back. What you wanted is:
self.select_folder_btn.configure(command=self.update_folder_value)
instead of your:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
You could also define that in the Button command. What you did is bypassed the button mechanism, so apparently only half of it is executed, and the relief is not raised. Note you have to remove the event parameter your method accepts.

How to open a popup window with a spinner in python + Gtk

I have a python Gtk application, with the GUI designed in Glade. It has a "scan" feature which will scan the network for a few seconds and then report its results to the user. During the scanning I want a popup window to appear stealing the focus from the parent until scanning is done.
I use a threading.Lock to synchronize the GUI and the scan thread, which makes the popup to last exactly the right time I want (see scanLock.acquire() ). It seems straightforward to me to implement something like a show() and hide() call before and after the scanLock.acquire(). I did use waitPopupShow and waitPopupHide instead of just calling the window.show() and window.hide() because I also may want to set the Label in the popup or start/stop the GtkSpinner. Here is some code from the GUI class:
def scan(self):
sT = scannerThread(self,self.STagList)
self.dataShare.threadsList.append(sT)
sT.start() # start scanning
self.waitPopupShow('Scanning... Please Wait')
self.scanLock.acquire() # blocks here until scan is finished
self.waitPopupHide()
def waitPopupShow(self, msg): # shows a GtkSpinner until the semaphore is cleared
self.waitDialogLabel.set_text(msg)
self.waitDialogBox.show_all()
self.waitDialog.show()
self.waitDialogSpinner.start()
def waitPopupHide(self):
# how to get the handle to the spinner and stop it?
self.waitDialogSpinner.stop()
self.waitDialog.hide()
def getAll(self):
# GUI
self.builder = Gtk.Builder()
self.builder.add_from_file(path to main GUI)
# ... getting stuff from a first glade file
# getting stuff from the waitDialog glade file
self.builder.add_from_file(path to waitDialog GUI)
self.waitDialog = self.builder.get_object("waitDialog") # GtkWindow
self.waitDialogBox = self.builder.get_object("waitDialogBox") # GtkBox
self.waitDialogLabel = self.builder.get_object("waitDialogLabel") # GtkLabel
self.waitDialogSpinner = self.builder.get_object("waitDialogSpinner") # GtkSpinner
self.waitDialog.hide()
I'm trying hardly since a couple of days to show a dialog with a label and a Gtk.Spinner. The best I obtain at the moment is to have the window showing up with no content. Please note that the self.waitDialog.hide() right after getting it with self.builder.get_object is needed because I set the property of the waitDialog Gtkwindow to Visibile. If I stop with the debugger before .hide() the waitDialog shows up perfectly. Afterwards its broken.
This is the waitDialog GUI file: http://pastebin.com/5enDQg3g
So my best guess is that I'm dooing something wrong, and I could find nothing on creating a new Gtk window over the main one, only basic examples and dialogs. A pointer to the documentation saying a bit about this would be a good starting point...

Functional Test for QMessageBox... why does not work?

I would develop some functional tests for a pyqt application that uses PyQt (or PySide) as GUI library. The tests use Unittest and Qttest library, as reported in many resources, for example this stackoverflow question: Unit and functional testing a PySide-based application?
For the main window all works fine, and the code simulate perfectly Keyboard Types and Mouse Clicks and Movements, but the "devil is in the details"... and this method does not work for a QMessageBox.
In the class of the Main Window, for manage a IOError on opening a file, I initialize a QMessageBox:
self.IOErrMsgBox = QtGui.QMessageBox()
self.IOErrMsgBox.setText("<b>Error</b>")
self.IOErrMsgBox.setInformativeText("""
<p>There was an error opening
the project file:
%s.</p>"""%(path,))
self.IOErrMsgBox.setStandardButtons(QtGui.QMessageBox.Ok)
self.IOErrMsgBox.setDefaultButton(QtGui.QMessageBox.Ok)
self.IOErrMsgBox.exec_()
To test how it works, in functional test I have:
def test__open_project(self):
self.MainWin._project_open(wrong_path, flag='c')
# the function that handles the exception
# and initializes the QMessageBox.
IOErrMsgBox = self.MainWin.IOErrMsgBox
# Reference to the initialized QMessageBox.
self.assertIsInstance(IOErrMsgBox, QMessageBox)
okWidget = self.MainWin.IOErrMsgBox.button(IOErrMsgBox.Ok)
QTest.mouseClick(okWidget, Qt.LeftButton)
or, in altenative:
def test__open_project(self):
#... some code, exactly like previous example except for last row...
QTest.keyClick(okWidget, 'o', Qt.AltModifier)
but No one works... and the Ok button is not clicked and I can do it with my mouse pointer :(
Any suggestions?
The question is in general about how to test modal dialogs.
Any modal dialog including QMessageBox will not return from exec_() until it is closed, so the test code in your second code box probably never gets executed.
You could just show() it (making it non-modal) and then follow your code but don't forget to close and delete the dialog afterwards.
Or you use a Timer and schedule a click on the OK button (similar to Test modal dialog with Qt Test). Here is an example:
from PySide import QtGui, QtCore
app = QtGui.QApplication([])
box = QtGui.QMessageBox()
box.setStandardButtons(QtGui.QMessageBox.Ok)
button = box.button(QtGui.QMessageBox.Ok)
QtCore.QTimer.singleShot(0, button.clicked)
box.exec_()

Overriding Tkinter "X" button control (the button that close the window) [duplicate]

This question already has answers here:
How do I handle the window close event in Tkinter?
(11 answers)
Closed 7 years ago.
When the user presses a close Button that I created, some tasks are performed before exiting. However, if the user clicks on the [X] button in the top-right of the window to close the window, I cannot perform these tasks.
How can I override what happens when the user clicks [X] button?
It sounds as if your save window should be modal.
If this is a basic save window, why are you reinventing the wheel?
Tk has a tkFileDialog for this purpose.
If what you want is to override the default behaviour of destroying the window, you can simply do:
root.protocol('WM_DELETE_WINDOW', doSomething) # root is your root window
def doSomething():
# check if saving
# if not:
root.destroy()
This way, you can intercept the destroy() call when someone closes the window (by any means) and do what you like.
Using the method procotol, we can redefine the WM_DELETE_WINDOW protocol by associating with it the call to a function, in this case the function is called on_exit:
import tkinter as tk
from tkinter import messagebox
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("Handling WM_DELETE_WINDOW protocol")
self.geometry("500x300+500+200")
self.make_topmost()
self.protocol("WM_DELETE_WINDOW", self.on_exit)
def on_exit(self):
"""When you click to exit, this function is called"""
if messagebox.askyesno("Exit", "Do you want to quit the application?"):
self.destroy()
def center(self):
"""Centers this Tk window"""
self.eval('tk::PlaceWindow %s center' % app.winfo_pathname(app.winfo_id()))
def make_topmost(self):
"""Makes this window the topmost window"""
self.lift()
self.attributes("-topmost", 1)
self.attributes("-topmost", 0)
if __name__ == '__main__':
App().mainloop()
The command you are looking for is wm_protocol, giving it "WM_DELETE_WINDOW" as the protocol to bind to. It lets you define a procedure to call when the window manager closes the window (which is what happens when you click the [x]).
I found a reference on Tkinter here. It's not perfect, but covers nearly everything I ever needed. I figure section 30.3 (Event types) helps, it tells us that there's a "Destroy" event for widgets. I suppose .bind()ing your saving jobs to that event of your main window should do the trick.
You could also call mainwindow.overrideredirect(True) (section 24), which disables minimizing, resizing and closing via the buttons in the title bar.

Categories

Resources