How to handle modal dialog in pytest-qt without mocking the dialog - python

I am using pytest-qt to automate the testing of a PyQt GUI. The dialogs need to be handled as a part of the testing(dialogs should not be mocked).
For example, file dialog that comes after a button-click has to be handled. There are 2 problems
After the button click command, the program control goes to the event handler and not to the next line where I can try to send mouseclick/keystrokes to the dialog.
Since the QDialog is not added to the main widget, it is not being listed among the children of the main widget. So how to get the reference of the QDialog?
I tried multi-threading but that didn't work, later I found that QObjects are not thread-safe.
def test_filedialog(qtbot, window):
qtbot.mouseClick(window.browseButton, QtCore.Qt.LeftButton, delay=1)
print("After mouse click")
#This is where I need to get the reference of QDialog and handle it

It can be done using QTimer.
def test_filedialog(qtbot, window):
def handle_dialog():
# get a reference to the dialog and handle it here
QTimer.singleShot(500, handle_dialog)
qtbot.mouseClick(window.browseButton, QtCore.Qt.LeftButton, delay=1)
Refer this link for more details

Related

PyQt - QDialogButtonBox signals and tool tip

I got a couple of questions regarding qDialogButtonBox. While my code still works, I believed that there are a few parts that can be better refined/ I am not finding much info online
class testDialog(QtGui.QDialog):
def __init_(self, parent=None):
...
self.init_ui()
self.signals_connection()
def init_ui(self):
...
self.buttonBox = QtGui.QDialogButtonBox()
self.buttonBox.addButton("Help", QtGui.QDialogButtonBox.HelpRole)
self.buttonBox.addButton("Apply", QtGui.QDialogButtonBox.AcceptRole)
self.buttonBox.addButton("Cancel", QtGui.QDialogButtonBox.RejectRole)
#
def signals_connection(self):
self.test_random.clicked.connect(self.test_rand)
# Is this the latest/correct way to write it?
self.buttonBox.accepted.connect(self.test_apply)
self.buttonBox.rejected.connect(self.test_cancel)
self.buttonBox.helpRequested.connect(self.test_help)
def test_apply(self):
print "I am clicking on Apply"
def test_cancel(self):
print "I am clicking on Cancel"
self.close()
def test_help(self):
print "I am clicking for Help!"
My questions are as follows:
Under my function - signals_connection(), the lines that I wrote for
the buttonBox (though the code still works) are quite different
for the signal I have wrote for the self.test_random and I am
unable to find any similar online for the qdialogbuttonbox.. There
is another style that I have found - self.connect(self.buttonBox,
QtCore.SIGNAL("accepted()"), self, QtCore.SLOT("accept()")) but I
think that is the old style?? Otherwise what should be the right way
to write it?
In my test_cancel() function, is writing self.close() the best
way to close the application? The way that I run my program is as
follows:
dialog = testDialog();dialog.show()
Lastly, is it possible to add 3 different tool tips to the 3 buttons I have created? I saw that there is a command for it - self.buttonBox.setToolTip("Buttons for life!"), but this will results in all 3 buttons to have the same tool tip. Can I make it as individual?
Yes, that is the correct way to write signal connections (the other syntax you found is indeed the old way of doing it). You can find all the signals in the pyqt documentation for QDialogButtonBox. Different widgets and objects have different signals. QPushButton's and QDialogButtonBox's have different signals.
Yes, close() will close the dialog. The QApplication will exit by default if there are no other windows open. However, if this is a modal dialog, you typically want to close a dialog with either the accept or reject command. This will alert the calling function as to whether the dialog was closed with the Ok/Yes/Apply button or closed with the No/Cancel button.
You can set different tooltips for different buttons in the QDialogButtonBox. You just need to get a reference to the specific button you want to set the tooltip for.
For example
self.buttonBox.button(QDialogButtonBox.Help).setToolTip('Help Tooltip')
self.buttonBox.button(QDialogButtonBox.Ok).setToolTip('Apply Tooltip')
Or you could loop through all the buttons
for button in self.buttonBox.buttons():
if button.text() == 'Help':
button.setToolTip('Help Tooltip')
elif button.text() == 'Apply':
button.setToolTip('Apply Tooltip')
Also, you could connect the accepted and rejected signals from the QDialogButtonBox to the accept and reject slots on the QDialog
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
That way, you won't have to manually connect the Ok and Cancel buttons to your callbacks for closing the dialog.

Create a python tkinter window with no X (close) button

I'm writing a 'wizard' type Python Tkinter GUI that collects information from the user and then performs several actions based on the user's entries: file copying, DB updates, etc. The processing normally takes 30-60 seconds and during that time, I want to:
Provide the user with text updates on the activity and progress
Prevent the user from closing the app until it's finished what it's doing
I started on the route of having the text updates appear in a child window that's configured to be trainsient and using wait_window to pause the main loop until the activities are done. This worked fine for other custom dialog boxes I created which have OK/cancel buttons that call the window's destroy method. The basic approach is:
def myCustomDialog(parent,*args):
winCDLG = _cdlgWin(parent,*args)
winCDLG.showWin()
winCDLG.dlgWin.focus_set()
winCDLG.dlgWin.grab_set()
winCDLG.dlgWin.transient(parent)
winCDLG.dlgWin.wait_window(winCDLG.dlgWin)
return winCDLG.userResponse
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.userResponse = ''
def showWin(self):
#Tkinter widgets and geometry defined here
def _btnOKClick(self):
#self.userResponse assigned from user entry/entries on dialog
self.dlgWin.destroy()
def _btnCancelClick(self):
self.dlgWin.destroy()
However this approach isn't working for the new monitor-and-update dialog I want to create.
First, because there's no user-initiated action to trigger the copy/update activities and then the destroy, I have to put them either in showWin, or in another method. I've tried both ways but I'm stuck between a race condition (the code completes the copy/update stuff but then tries to destroy the window before it's there), and never executing the copy/update stuff in the first place because it hits the wait_window before I can activate the other method.
If I could figure out a way past that, then the secondary problem (preventing the user from closing the child window before the work's done) is covered by the answers below.
So... is there any kind of bandaid I could apply to make this approach work the way I want? Or do I need to just scrap this because it can't work? (And if it's the latter, is there any way I can accomplish the original goal?)
self.dlgWin.overrideredirect(1) will remove all of the buttons (make a borderless window). Is that what you're looking for?
As far as I know, window control buttons are implemented by the window manager, so I think it is not possible to just remove one of them with Tkinter (I am not 100% sure though). The common solution for this problem is to set a callback to the protocol WM_DELETE_WINDOW and use it to control the behaviour of the window:
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.dlgWin.protocol('WM_DELETE_WINDOW', self.close)
self.userResponse = ''
def close(self):
tkMessageBox.showwarning('Warning!',
'The pending action has not finished yet')
# ...

Destroyed about dialog doesn't reappear properly

I'm working on a GUI application in Python / Glade, and have the following issue.
I am trying to get an About dialog properly working...however when I click 'Close' (in the About dialog) and then attempt to open it again, this is all I see:
So, just a tiny little snippet of the window, and a non-functioning close button.
This is my class for my Glade window:
# glade object
class MainWindow(object):
builder_ = None
# load main window
def __init__(self):
handler = {
"sigWindowDestroy" : gtk.main_quit,
"sigShowAbout" : self.show_about
}
projfile = "proj.glade"
self.builder_ = gtk.Builder()
self.builder_.add_from_file(projfile)
self.builder_.connect_signals(handler)
window = self.builder_.get_object("main_window")
window.show()
# show about dialog
def show_about(self, *args):
dAbout = self.builder_.get_object("dAbout")
dAbout.run()
dAbout.destroy()
And in my main function:
# load glade gui
app = MainWindow()
gtk.main()
On the second click, I see the following output in my terminal window (using Mac OS X).
GtkWarning: gtk_widget_show: assertion `GTK_IS_WIDGET (widget)' failed
dAbout.run()
GtkWarning: gtk_label_set_markup: assertion `GTK_IS_LABEL (label)' failed
dAbout.run()
Edit: sorry, must reopen for general unfamiliarity with PyGTK.
I've used the show()/hide() methods instead of run()/destroy() as proposed. Now, I was following along with another SO post, which highlighted this tutorial (who said to use run()/destroy()), and am seeing this behavior.
First, the Close button does nothing. I had thought for some reason its behavior was pre-defined.
Second, closing the dialog with the corner close button still provides the same behavior that I see with run()/destroy() as above.
Edit 2: Solved by adding the following:
dAbout.connect("response", lambda d, r: d.hide())
Don't try to (deep-)copy a widget. It doesn't work, as you found out.
Instead, hide() the dialog instead of destroy()ing it.
You could have even used run(). You just shouldn't use destroy(). What made you think, you shouldn't use run() and hide() together? See, when you destroy a widget, that means removing it from memory as if it never had been build. If you hide it, you can reuse it later, but take care of changes a user might have done to it, as the window will re-appear in the state it was in before being hidden. You can manipulate a widgets properties from code while hidden.
The "predefined" action of your close button was caused by run(). The solution you posted, using the lambda function is little more than what run() does for you. Basically it does the following:
Connect the "response" signal of your DialogWindow
Connect the "delete-event" signal of your DialogWindow
Start a new Gtk main loop to block the application
Show your widget
Disconnect the signals
Return the response
You just need to hide() it afterwards and are able to run() it again.

PyQt:Why a popup dialog prevents execution of other code?

I am having a little problem with a pop up dialog.I have a combobox,which when the option changes it pops up a dialog with a textedit widget,do some stuff and insert some text in the textedit widget.
This is what i use for the popup:
def function_1(self):
dialog = QDialog()
dialog.ui = Ui_Dialog_popup()
dialog.ui.setupUi(dialog)
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
dialog.exec_()
I have the pop up gui code made in QtDesignere in a separate py file.
The popup dialog appears,but if the dialog is not closed,nothing else can be executed.Do you know how can I deal with this ? Thanks.
That's exactly what the exec method of QDialog is designed for: modal dialogs. Read the "Modal" and "Modeless dialog" sections.
If you don't the dialog to block your main UI, call show() instead of exec() (and check the modal property documentation).
Elaborating on what Mat said: The show() function immediately returns, and as dialog is local to this function, the object gets deleted as soon as "function_1" returns. You might want to make the dialog a member or global (whichever suits your requirement) so that the object stays in memory.
HTH
Since you're setting the WA_DeleteOnClose window attribute, I'm assuming you want to create a new dialog every time the function_1 method is called (which is probably a good idea).
If so, the simplest way to solve your issue (based on the code you've given), is to give your dialog a parent (so it is kept alive), and then display it modelessly using show():
def function_1(self):
dialog = QDialog(self)
dialog.ui = Ui_Dialog_popup()
dialog.ui.setupUi(dialog)
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
dialog.show()

Block and hide QDialog: Alternative to exec_()?

In my Qt-based application (built using PyQt 4.8.6), I have a class that is a subclass of QtGui.QDialog:
class ModelDialog(QtGui.QDialog):
...
When I run the application's user interface, I can display the QDialog like so:
def main():
app = QtGui.QApplication(sys.argv)
dialog = ModelDialog()
dialog.exec_()
According to the Qt docs and the PyQt docs, exec_() is a blocking function for this QDialog, which defaults to a modal window (which by definition prevents the user from interacting with any other windows within the application). This is exactly what happens under normal circumstances.
Recently, however, I've been working on a way to call through the entire QApplication using defaults for all input values, and not asking the user for any input. The application behaves as expected except for one single aspect: calling dialog.exec_() causes the modal dialog to be shown.
The only workaround I've been able to find has been to catch the showEvent function and to promptly hide the window, but this still allows the QDialog object to be shown for a split second:
class ModelDialog(QtGui.QDialog):
...
def showEvent(self, data=None):
self.hide()
Is there a way to prevent the modal window from being shown altogether, while continuing to block the main event loop? I'd love for there to be something like:
def main():
app = QtGui.QApplication(sys.argv)
dialog = ModelDialog()
dialog.setHideNoMatterWhat(True)
dialog.exec_()
(to that end, I tried using QWidget.setVisible(False), but dialog.exec_() sets the dialog to be visible anyways, which is expected according to the Qt docs)
Use app.exec_() instead of dialog.exec_().

Categories