gtk MessageDialog not closing until enclosing method finishes - python

Here is a mocked up version of what I'm trying to do in my GUI. I have a MessageDialog which is created somewhere during the execution of a callback method. My problem is the MessageDialog won't close until the callback method finishes its execution.
I have a "dialog.destroy()" which I would expect to destroy the dialog. I click on "Yes/No" and the button depresses, but the dialog doesn't go away until "_go" finishes.
The "time.sleep(4)" is in there to simulate other stuff happening in my "_go" method after my MessageDialog interaction is over.
from gi.repository import Gtk, GObject
import time
class Gui(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self)
self.connect("delete_event", Gtk.main_quit)
self.set_size_request(700, 600)
notebook = Gtk.Notebook()
notebook.set_tab_pos(Gtk.PositionType.TOP)
notebook.append_page(MyTab(), Gtk.Label("A tab"))
self.add(notebook)
notebook.show_all()
self.show()
class MyTab(Gtk.VBox):
def __init__(self):
super(MyTab, self).__init__()
self.go_button = Gtk.Button()
self.go_button.add(Gtk.Image().new_from_stock(Gtk.STOCK_APPLY,
Gtk.IconSize.BUTTON))
top_box = Gtk.HBox()
top_box.pack_start(self.go_button, False, True, 5)
self.pack_start(top_box, False, True, 5)
# setup callbacks
self.go_button.connect("clicked", self._go)
def _go(self, _):
dialog = Gtk.MessageDialog(Gtk.Window(),
Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION,
Gtk.ButtonsType.YES_NO,
"RESPONSE REQUIRED")
dialog.format_secondary_text("are you having fun?")
response = dialog.run()
dialog.destroy()
print "your response is: " + str(response)
time.sleep(4)
print "left _go"
def main():
"""
Main entry point.
"""
Gui()
Gtk.main()
if __name__ == "__main__":
main()

This problem is not specific to dialogs. Any GUI change is invisible until you return to the main loop and give the system a chance to process the events accumulated by modifying the widgets.
If you really want to update the GUI immediately in the callback, you can manually spin the accumulated events with a loop like this after the call to dialog.destroy():
while Gtk.events_pending():
Gtk.main_iteration()
However, be aware that this will not only update the screen, but also run other accumulated events, including idle and timeout handlers and button click callbacks (if any are pending). That can have unexpected consequences.

This is the correct behaviour. The window only disappears when control is given back to Gtk's main loop which only happens at the end of your _go callback.

As per the comments on user4815162342's answer I came up with a solution that uses a nested main loop. This class takes in a dialog and provides a run method.
class NestedDialog(object):
def __init__(self, dialog):
self.dialog = dialog
self.response_var = None
def run(self):
self._run()
return self.response_var
def _run(self):
self.dialog.show()
self.dialog.connect("response", self._response)
Gtk.main()
def _response(self, dialog, response):
self.response_var = response
self.dialog.destroy()
Gtk.main_quit()
The dialog is then run as follows:
def _go(self, _):
dialog = Gtk.MessageDialog(Gtk.Window(),
Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION,
Gtk.ButtonsType.YES_NO,
"RESPONSE REQUIRED")
dialog.format_secondary_text("are you having fun?")
nested_dialog = NestedDialog(dialog)
response = nested_dialog.run()
print "your response is: " + str(response)
time.sleep(4)
print "left _go"

Related

Context menu didn't work on tray icon when using pyqt5

I'm developing an application with pyqt5, which will hide window automatically when it starts to work. And I hope to build a tray icon with context menu to show the window again. Below is my brief code,
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.set_sytem_tray_icon()
self.current_status = Stop
self.track_str = ''
self.start_button.clicked.connect(self.start_button_clicked)
self.stop_button.clicked.connect(self.stop_button_clicked)
self.show()
def set_sytem_tray_icon(self):
contextMenu = QMenu()
action = contextMenu.addAction("Show Window")
action.triggered.connect(self.show)
contextMenu.addAction(action)
self.tray_icon = QSystemTrayIcon()
self.tray_icon.setIcon(QtGui.QIcon("img.ico"))
self.tray_icon.setContextMenu(contextMenu)
self.tray_icon.show()
def get_specific_window(self, class_name, title):
def check_ULink_status(self):
try:
while True:
# Doing something
break
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
thread.join()
self.show()
def stop_button_clicked(self):
reply = QMessageBox.information(self, "Warning", "Stop", QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.Yes:
self.current_status = Stop
self.change_button_availability()
Here is my problem, when I clicked start button, the application started working but tray icon didn't response to any action. I believe there was some conflict with my main thread, but I still can't figure out what's going on. Does anyone have answer for this?
Update,
I would like to provide another solution using qthread.
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.set_sytem_tray_icon()
self.current_status = Stop
self.track_str = ''
self.start_button.clicked.connect(self.start_button_clicked)
self.stop_button.clicked.connect(self.stop_button_clicked)
self.show()
def set_sytem_tray_icon(self):
contextMenu = QMenu()
action = contextMenu.addAction("Show Window")
action.triggered.connect(self.show)
contextMenu.addAction(action)
self.tray_icon = QSystemTrayIcon()
self.tray_icon.setContextMenu(contextMenu)
self.tray_icon.show()
def set_sytem_tray_icon_with_qthread(self):
set_sytem_tray_icon_qthread = qthread_definition.MyQThread2()
set_sytem_tray_icon_qthread.signal.connect(self.set_sytem_tray_icon)
set_sytem_tray_icon_qthread.start()
set_sytem_tray_icon_qthread.wait()
def show_mainwindow_with_qthread(self):
show_mainwindow_qthread = qthread_definition.MyQThread2()
show_mainwindow_qthread.signal.connect(self.show)
show_mainwindow_qthread.start()
show_mainwindow_qthread.wait()
def get_specific_window(self, class_name, title):
# doing something
def check_ULink_status(self):
self.set_sytem_tray_icon_with_qthread() # add new qthread here
try:
while True:
# Doing something
break
self.show_mainwindow_with_qthread()
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
def stop_button_clicked(self):
reply = QMessageBox.information(self, "Warning", "Stop", QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.Yes:
self.current_status = Stop
self.change_button_availability()
where MyQThread2 class is shown below,
class MyQThread2(QtCore.QThread):
# this thread is to create the tray icon
signal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
def run(self):
self.signal.emit()
To show the main window in the thread, we need to create a qthread to finish this task, because showing the window is a kind of modification of qt object, which is not allowed outside main thread.
As the docs of the join() method points out:
join(timeout=None)
Wait until the thread terminates. This blocks the
calling thread until the thread whose join() method is called
terminates – either normally or through an unhandled exception – or
until the optional timeout occurs.
When the timeout argument is present and not None, it should be a
floating point number specifying a timeout for the operation in
seconds (or fractions thereof). As join() always returns None, you
must call is_alive() after join() to decide whether a timeout happened
– if the thread is still alive, the join() call timed out.
When the timeout argument is not present or None, the operation will
block until the thread terminates.
A thread can be join()ed many times.
join() raises a RuntimeError if an attempt is made to join the current
thread as that would cause a deadlock. It is also an error to join() a
thread before it has been started and attempts to do so raise the same
exception.
(emphasis mine)
This method locks until the thread finishes executing preventing the GUI event loop from executing, causing it not to respond to events since it is frozen.
The solution is to remove this method:
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
# thread.join() # remove this line
self.show()
It seems that the OP use join() so that when the task is finished the window is displayed again, if so, the correct solution is to use a signal:
class MainWindow(QMainWindow, Ui_MainWindow):
finished = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.finished.connect(self.show)
# ...
# ...
def check_ULink_status(self):
# After finishing the task the signal must be emitted
self.finished.emit()
# ...
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
# ...

Trying to create a dialog in another thread wxpython

I'm running a function in another thread that is supposed to fill out a dialog and then show it but it just seg faults as soon as I tried to alter the dialog in any way. I've read that this is a common issue with WxPython and that devs are not intended to directly alter dialogs in another thread.
How do I get around this? I can just call the function in my main thread but that will block my GUI and it is a lengthy operation to initialize the dialog - I would like to avoid this.
My code is similar to the below.
In the main thread
# Create the dialog and initialize it
thread.start_new_thread(self.init_dialog, (arg, arg, arg...))
The function I am calling
def init_dialog(self, arg, arg, arg....):
dialog = MyFrame(self, "Dialog")
# Setup the dialog
# ....
dialog.Show()
Even with a blank dialog and just a simple call to show inside the function I get a segmentation fault. Any help would be greatly appreciated, thanks.
I have made an applet to demonstrate keeping GUI responsive during calculations and calling the message box after the calculations.
import wx
import threading
import time
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "I am a test frame")
self.clickbtn = wx.Button(self, label="click me!")
self.Bind(wx.EVT_BUTTON, self.onClick)
def onClick(self, event):
self.clickbtn.Destroy()
self.status = wx.TextCtrl(self)
self.status.SetLabel("0")
print "GUI will be responsive during simulated calculations..."
thread = threading.Thread(target=self.runCalculation)
thread.start()
def runCalculation(self):
print "you can type in the GUI box during calculations"
for s in "1", "2", "3", "...":
time.sleep(1)
wx.CallAfter(self.status.AppendText, s)
wx.CallAfter(self.allDone)
def allDone(self):
self.status.SetLabel("all done")
dlg = wx.MessageDialog(self,
"This message shown only after calculation!",
"",
wx.OK)
result = dlg.ShowModal()
dlg.Destroy()
if result == wx.ID_OK:
self.Destroy()
mySandbox = wx.App()
myFrame = TestFrame()
myFrame.Show()
mySandbox.MainLoop()
GUI stuff is kept in the main thread, while calculations continue unhindered. The results of the calculation are available at time of dialog creation, as you required.

QDialog switch to non-modal after accept not redrawing

So, I'm not sure if the title is the best description, but it's what I came up with.
Here's the deal. I'm working on a PyQt app that has a sort of plugin system where you can just add some sub classes to a folder and the app finds them. These commands have the option of being able to create little uis for themselves. Basically, they look like this:
class Command(object):
def do(self):
self.setupUi()
self.pre()
self.run()
self.post()
def pre(self):
# do setup stuff for run method
def run(self):
# do actual work
def post(self):
# clean up after run
def setupUi(self):
# create a ui for this command
diag = QDialog()
diag.exec_()
Now, the issue I'm running into is, I have one Command that creates a dialog, and waits for the user to accept it. Then, I need to switch the dialog to non-modal while the command is running, and up date the dialog. This all seems to work fine. But, the problem is I can't get the dialog to redraw until after the pre, run, and post methods have finished. So, if I have the setupUi like this:
def setupUi(self):
# create a ui for this command
diag = QDialog()
if diag.exec_():
diag.setModal(False)
diag.show()
I tried processEvents but that didn't seem to do it. Has anyone else run into this issue, or know of any work arounds?
Thanks
Using diag.exec_() will block until the dialog returns (is closed). So, if you will need to call show() on it's own. There are a few ways to proceed from here.
You can have the dialog accept slot run a reference to the rest of the commands
You can poll the dialog to see if the user has accepted
You can move the pre, run, and post commands to the dialog
Assuming you want to keep the meat of the code out of the dialog class, and since periodically polling is best to avoid if possible, here is an example of the first strategy:
import sys, time
from PyQt4 import QtCore, QtGui
class MyDialog(QtGui.QDialog):
def __init__(self,parent=None):
QtGui.QDialog.__init__(self,parent)
layout = QtGui.QVBoxLayout()
self.msg = QtGui.QLabel('some sort of status')
self.buttonbox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok, QtCore.Qt.Horizontal, self)
self.buttonbox.accepted.connect(self.accept)
layout.addWidget(self.msg)
layout.addWidget(self.buttonbox)
self.setLayout(layout)
def set_msg(self, new_msg):
self.msg.setText(new_msg)
def set_function_on_accept(self,fcn):
self.function = fcn
def accept(self):
self.function()
class Command(object):
def do(self):
self.setupUi()
def do_work(self):
self.pre()
self.run()
self.post()
def pre(self):
# do setup stuff for run method
time.sleep(1)
self.diag.set_msg("stuff setup")
QtGui.QApplication.processEvents()
def run(self):
# do actual work
time.sleep(1)
self.diag.set_msg("work done")
QtGui.QApplication.processEvents()
def post(self):
# clean up after run
time.sleep(1)
self.diag.set_msg("cleaned up")
QtGui.QApplication.processEvents()
def setupUi(self):
# create a ui for this command
diag = MyDialog()
self.diag = diag
diag.set_function_on_accept(self.do_work)
diag.show()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
command = Command()
command.do()
sys.exit(app.exec_())

Connect a method for window destroy

I have a main window with a Gtk Button named openDialog. If I click on this button another Window (addName) popups. I would like to write a method (or a function, don't know which is the right name in python) in my main window file, called printHi. I would like to run this printHi method (in my main window file), when addName window is destroyed.
I tried something like this:
def on_addName_destroy():
printHi()
But it doesn't work. Any suggestion?
You can make use of "delete-event" signal of gtk.Widget. It is also possible to make use of "destroy" signal of gtk.Object. Here is a sample which connects to both the signals although in your case connecting to any one of them should suffice.
#!/usr/bin/env python
import gtk
def on_addName_destroy(gtkobject, data=None):
print "This is called later after delete-event callback has been called"
print "Indication that the reference of this object should be destroyed"
print "============================================"
def on_addName_delete(widget, event, data=None):
print "This is called on delete request"
print "Propagation of this event further can be controlled by return value"
print "--------------------------------------------"
return False
def show_popup(widget, data=None):
dialog = gtk.Window(gtk.WINDOW_TOPLEVEL)
dialog.set_size_request(100, 100)
label = gtk.Label("Hello!")
dialog.add(label)
dialog.connect("delete-event", on_addName_delete)
dialog.connect("destroy", on_addName_destroy)
dialog.show_all()
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_size_request(100, 100)
button = gtk.Button("Popup")
button.connect("clicked", show_popup)
window.add(button)
window.connect("destroy", lambda x: gtk.main_quit())
window.show_all()
gtk.main()
Hope this helps!

How to get a gtkDialog's default response to trigger of the space bar as well

I have a messageDialog set up so that its default response is gtk.RESPONSE_OK so the okay button is clicked when the user hits enter even if the okay button does not have focus. I would like to also have the space bar trigget the default_response. What is the best way to do this?
This is with python 2.4 in a linux environment. Unfortunately I don't have permission to upgrade python.
Connect to the key-press-event signal on the message dialog:
def on_dialog_key_press(dialog, event):
if event.string == ' ':
dialog.response(gtk.RESPONSE_OK)
return True
return False
dialog = gtk.MessageDialog(message_format='Some message', buttons=gtk.BUTTONS_OK_CANCEL)
dialog.add_events(gtk.gdk.KEY_PRESS_MASK)
dialog.connect('key-press-event', on_dialog_key_press)
dialog.run()
Bear in mind, though, that changing users' expectations of the user interface is generally considered Not Cool.
I'm a total noob at pygtk, but I could not get #ptomato's example + "hello world" boilerplate to work unless I responded to space and return plus added a call to dialog.destroy(). Take it for what it is worth.
#!/usr/bin/env python
# example helloworld.py
import pygtk
pygtk.require('2.0')
import gtk
def md_event(dialog, event):
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.space):
dialog.response(gtk.RESPONSE_OK)
dialog.destroy()
return True
elif event.keyval == gtk.keysyms.Escape:
dialog.response(gtk.RESPONSE_CANCEL)
dialog.destroy()
return True
return False
class HelloWorld:
# This is a callback function. The data arguments are ignored
# in this example. More on callbacks below.
def hello(self, widget, data=None):
print "Hello World"
# Another callback
def destroy(self, widget, data=None):
gtk.main_quit()
def create_message_dialog(self, x, y):
md = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, message_format="wawawawaaaaa")
md.add_events(gtk.gdk.KEY_PRESS_MASK)
md.connect("key-press-event", md_event)
result = md.run()
print result
def __init__(self):
# create a new window
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
# Here we connect the "destroy" event to a signal handler.
# This event occurs when we call gtk_widget_destroy() on the window,
# or if we return FALSE in the "delete_event" callback.
self.window.connect("destroy", self.destroy)
# Sets the border width of the window.
self.window.set_border_width(10)
self.button2 = gtk.Button("Message Dialog")
self.button2.connect("clicked", self.create_message_dialog, None)
self.window.add(self.button2)
self.button2.show()
# and the window
self.window.show()
def main(self):
# All PyGTK applications must have a gtk.main(). Control ends here
# and waits for an event to occur (like a key press or mouse event).
gtk.main()
def run_hello():
hello = HelloWorld()
hello.main()
# If the program is run directly or passed as an argument to the python
# interpreter then create a HelloWorld instance and show it
if __name__ == "__main__":
run_hello()

Categories