I'm currently learning both Python and GTK 3+ and I've a problem when synchronizing threads. I'll try to be quick and clear:
I have to make a Social Network client. As the purpose is to learn how to create a GUI the "access to the social network API" will be simulated, but I have to "wait" for network responses with time.sleep(). Calling time.sleep() in the main thread freezes the GUI (it stops the execution of Gtk.Main()) so I have to make all my connections in a separate thread. And here lies my problem. When I'm authenticating a user (verifying_credentials) I need to wait from that thread to finish to continue the execution of the main program. If I try a Thread.join GUI freezes. I've tried using queues, but queue.get is also blocking Gtk.main(). I've tried emitting a signal when my thread is finished , but the handler starts in the same thread, so when I try to modify the GUI (which I need) , program crashes (you're not supposed to touch the GUI from anywhere except main thread).
My solution ? I do busy-waiting / active-waiting , which is by definition an antipattern. I keep asking if the thread has finished and forcing cicles of Gtk.main()
There has to be another way, a more elegant / efficient way than mine. I don't know if I can signal another thread, or there is a way of using queues without blocking the main thread. Any help will be very much appreciated.
The python code :
from os.path import abspath, dirname, join
import gettext
import threading
import math
import time
import random
import locale
from gi.repository import Gtk, Gdk, GLib, GObject
import list
APP = "UDC_Social_API"
user_list = {
"user": "password",
"admin": "admin",
"carnotan": "1234"
}
class UDC_Social_API:
def on_applicationwindow1_key_press_event(self, widget, event):
# keyname = Gdk.keyval_name(event.keyval)
# print (_("Key %(name)s (%(val)d) was pressed" )%{"name":keyname, "val":event.keyval})
if event.keyval == 65293:
self.on_login_button_clicked()
def delay(self):
if self.delay_option.get_active():
time.sleep(math.exp(random.random()*5))
else:
pass
def active_waiting(self):
while self.finished is False:
Gtk.main_iteration_do(False)
self.finished = False
self.z_handler(None)
def verify_credentials(self, user, password):
GLib.idle_add(self.active_waiting)
self.delay()
if user in user_list:
if password == user_list.get(user):
self.authentication = True
self.finished = True
else:
self.authentication = False
self.finished = True
else:
self.authentication = False
self.finished = True
def on_login_button_clicked(self, data=None):
user = self.user_entry.get_text()
password = self.password_entry.get_text()
thread = threading.Thread(target=self.verify_credentials, args=(user, password))
thread.daemon = True
thread.start()
def z_handler(self, data=None):
if self.authentication is False:
self.message_dialog.set_markup(_("User/Password incorrect\nPlease, verify login information"))
self.message_dialog.run()
self.message_dialog.hide()
return False
else:
self.window.hide()
print ("Success!")
def on_applicationwindow1_destroy(self, data=None):
Gtk.main_quit()
def on_gtk_about_activate(self, menuitem, data=None):
self.aboutdialog.run()
self.aboutdialog.hide()
def on_gtk_cut_activate(self, widget):
# Get the bounds of the selected text
bounds = self.focus.get_selection_bounds()
# if the bounds of the selection are not an empty tuple,
# put the selection in the variable chars
# and copy it to the clipboard
# (get_selection_bounds returns an empty tuple if there is no selection)
# then delete the selection
if bounds:
chars = self.focus.get_chars(*bounds)
self.clipboard.set_text(chars, -1)
self.focus.delete_text(bounds[0], bounds[1])
else:
pass
def on_gtk_copy_activate(self, widget):
# Get the bounds of the selected text
bounds = self.focus.get_selection_bounds()
# if the bounds of the selection are not an empty tuple,
# put the selection in the variable chars
# and copy it to the clipboard
# (get_selection_bounds returns an empty tuple if there is no selection)
if bounds:
chars = self.focus.get_chars(*bounds)
self.clipboard.set_text(chars, -1)
else:
pass
def on_gtk_paste_activate(self, widget):
# Get the text from the clipboard
text = self.clipboard.wait_for_text()
if text is not None:
# If there's text selected in the target
# delete it and paste the contents of the clipboard
bounds = self.focus.get_selection_bounds()
if bounds:
self.focus.delete_text(bounds[0], bounds[1])
self.focus.insert_text(text, bounds[0])
# else insert the text in the current position of the cursor in the target
else:
pos = self.focus.get_position()
self.focus.insert_text(text, pos)
else:
pass
def on_entry_focus(self, widget, event):
self.focus = widget
def create_menubar(self):
self.file_menu=self.builder.get_object("menuitem1")
self.edit_menu=self.builder.get_object("menuitem2")
self.options_menu=self.builder.get_object("option")
self.help_menu=self.builder.get_object("menuitem4")
self.languages_menu=self.builder.get_object("menuitem3")
self.delay_option = self.builder.get_object("delay_option")
self.gtk_quit_menu=self.builder.get_object("gtk_quit_menu")
self.gtk_cut_menu=self.builder.get_object("gtk_cut_menu")
self.gtk_copy_menu=self.builder.get_object("gtk_copy_menu")
self.gtk_paste_menu=self.builder.get_object("gtk_paste_menu")
self.gtk_about_menu=self.builder.get_object("gtk_about_menu")
self.galician_option=self.builder.get_object("radiomenuitem1")
self.spanish_option=self.builder.get_object("radiomenuitem2")
self.english_option=self.builder.get_object("radiomenuitem3")
def set_menubar_names(self):
self.file_menu.set_label(_("_File"))
self.edit_menu.set_label(_("_Edit"))
self.options_menu.set_label(_("_Options"))
self.help_menu.set_label(_("_Help"))
self.languages_menu.set_label(_("_Languages"))
self.delay_option.set_label(_("_Delay"))
self.gtk_quit_menu.set_label(_("Quit"))
self.gtk_copy_menu.set_label(_("Copy"))
self.gtk_cut_menu.set_label(_("Cut"))
self.gtk_paste_menu.set_label(_("Paste"))
self.gtk_about_menu.set_label(_("About"))
self.galician_option.set_label(_("_Galician"))
self.spanish_option.set_label(_("_Spanish"))
self.english_option.set_label(_("_English"))
def create_login_box(self):
self.user_entry = self.builder.get_object("user_entry")
self.password_entry = self.builder.get_object("password_entry")
self.user_label=self.builder.get_object("user_label")
self.password_label=self.builder.get_object("password_label")
self.login_button=self.builder.get_object("login_button")
def set_login_box_names(self):
self.user_entry.set_placeholder_text(_("user"))
self.password_entry.set_placeholder_text(_("password"))
self.user_label.set_label(_("User"))
self.password_label.set_label(_("Password"))
self.login_button.set_label(_("Login"))
def create_about_dialog(self):
self.aboutdialog = self.builder.get_object("aboutdialog1")
self.aboutdialog.set_transient_for(self.window)
def set_about_dialog(self):
self.aboutdialog.set_comments(_("Developed for GTK 3+ and Python 3.4"))
def reset_names(self):
self.set_menubar_names()
self.set_login_box_names()
def on_radiomenuitem1_toggled(self, widget):
if widget.get_active():
self.lang_gl_ES.install()
self.reset_names()
self.window.queue_draw()
else:
pass
def on_radiomenuitem2_toggled(self, widget):
if widget.get_active():
self.lang_es_ES.install()
self.reset_names()
self.window.queue_draw()
else:
pass
def on_radiomenuitem3_toggled(self,widget):
if widget.get_active():
self.lang_en_US.install()
self.set_menubar_names()
self.window.queue_draw()
else:
pass
def set_languages(self):
WHERE_AM_I = abspath(dirname(__file__))
locale.setlocale(locale.LC_ALL, '')
locale.bindtextdomain(APP, WHERE_AM_I)
locale_path = WHERE_AM_I +'/'
self.builder.set_translation_domain(APP)
gettext.find(APP,localedir=locale_path,languages=['gl_ES'])
gettext.find(APP,localedir=locale_path,languages=['es_ES'])
gettext.find(APP,localedir=locale_path,languages=['en_US'])
gettext.install(APP,locale_path)
gettext.textdomain(APP)
gettext.bindtextdomain(APP,locale_path)
self.lang_gl_ES=gettext.translation(APP,localedir=locale_path, languages=['gl_ES'])
self.lang_es_ES=gettext.translation(APP,localedir=locale_path, languages=['es_ES'])
self.lang_en_US=gettext.translation(APP,localedir=locale_path, languages=['en_US'])
def set_signals(self):
handlers = {
"on_applicationwindow1_destroy": self.on_applicationwindow1_destroy,
"on_gtk_about_activate": self.on_gtk_about_activate,
"on_login_button_clicked": self.on_login_button_clicked,
"on_applicationwindow1_key_press_event": self.on_applicationwindow1_key_press_event,
"on_entry_focus": self.on_entry_focus,
"on_gtk_cut_activate": self.on_gtk_cut_activate,
"on_gtk_copy_activate": self.on_gtk_copy_activate,
"on_gtk_paste_activate": self.on_gtk_paste_activate,
"on_radiomenuitem1_toggled": self.on_radiomenuitem1_toggled,
"on_radiomenuitem2_toggled": self.on_radiomenuitem2_toggled,
"on_radiomenuitem3_toggled": self.on_radiomenuitem3_toggled
}
self.builder.connect_signals(handlers)
def __init__(self):
# GObject.signal_new("z_signal", Gtk.ApplicationWindow, GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ())
self.builder = Gtk.Builder()
self.builder.add_from_file("p1.glade")
self.window = self.builder.get_object("applicationwindow1")
self.set_languages()
self.create_menubar()
self.create_login_box()
self.create_about_dialog()
self.reset_names()
self.set_signals()
self.focus = None
self.finished = False
self.authentication = False
# self.statusbar = self.builder.get_object("statusbar1")
# self.context_id = self.statusbar.get_context_id("status")
# self.status_count = 0
self.message_dialog = self.builder.get_object("messagedialog1")
self.message_dialog.set_transient_for(self.window)
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.window.show_all()
if __name__ == "__main__":
GObject.threads_init()
main = UDC_Social_API()
Gtk.main()
Glade file is in pastebin, because it will exceed post size limit.
Thanks in advance for any help you could provide.
You can use GLib.idle_add
to schedule a callback to be executed by the event loop in the main thread of your program. This means it provides a safe way to schedule a GUI update from a background thread. So, you can just let your background thread run normally, let the main thread return control to the event loop, and then make the appropriate GUI updates from the background thread via GLib.idle_add
once it's done:
def verify_credentials(self, user, password):
self.delay()
if user in user_list:
if password == user_list.get(user):
self.authentication = True
else:
self.authentication = False
else:
self.authentication = False
# Schedule z_handler to be called by the event loop in the main thread.
GLib.idle_add(z_handler, None)
def z_handler(self, data=None):
if not self.authentication:
self.message_dialog.set_markup(_("User/Password incorrect\nPlease, verify login information"))
self.message_dialog.run()
self.message_dialog.hide()
return False
else:
self.window.hide()
print ("Success!")
You're actually pretty close to using this same method, you're just doing it in an awkward way - you're scheduling active_waiting
to run in the main thread, which waits until the background thread is done, and then calls z_handler
. Scheduling z_handler
directly, after the background thread is done with its work, is much simpler.