I want to use MPD's idle feature to wait for any changes and then display them in the GTK GUI using Python. The problem is that the GUI seems to block and become unresponsive when I use MPD's idle feature (when changing songs the GTK window becomes unresponsive). When I remove self.mpd.idle() it works, but then the function keeps getting run all the time which I find unnecessary.
What is the best way to solve this?
Not working
My initial approach:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
from mpd import MPDClient
class GUI:
def __init__(self):
self.mpd = MPDClient()
self.mpd.timeout = 10
self.mpd.connect("localhost", 6600)
self.window = Gtk.Window()
self.window.connect("delete-event", Gtk.main_quit)
self.window.show_all()
GLib.idle_add(self.get_current_song)
Gtk.main()
def get_current_song(self):
self.mpd.idle()
print(self.mpd.currentsong())
return True
app = GUI()
Not working
My second approach using this. Still getting the same results.
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
from mpd import MPDClient
import threading
class GUI:
def __init__(self):
self.mpd = MPDClient()
self.mpd.timeout = 1
self.mpd.connect("localhost", 6600)
self.window = Gtk.Window()
self.window.connect("delete-event", Gtk.main_quit)
self.window.show_all()
self.thread = threading.Thread(target=self.idle_loop)
self.thread.daemon = True
self.thread.start()
Gtk.main()
def get_songs(self):
print(self.mpd.currentsong())
self.mpd.idle()
return True
def idle_loop(self):
GLib.idle_add(self.get_songs)
app = GUI()
WORKING
Leaving out the GLib.idle_add() function seems to be a solution. But I don't know if this is the "proper" way. It feels wrong not knowing why GLib.idle_add() was messing it up and not using it since it's mentioned in the documentation.
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
from mpd import MPDClient
import threading
class GUI:
def __init__(self):
self.mpd = MPDClient()
self.mpd.timeout = 1
self.mpd.connect("localhost", 6600)
self.window = Gtk.Window()
self.window.connect("delete-event", Gtk.main_quit)
self.window.show_all()
self.thread = threading.Thread(target=self.get_songs)
self.thread.daemon = True
self.thread.start()
Gtk.main()
def get_songs(self):
self.mpd.idle()
print(self.mpd.currentsong())
app = GUI()
Let us use the threading module here.
As described in this article : https://pygobject.readthedocs.io/en/latest/guide/threading.html, we can make the following code:
import gi
from threading import Thread
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
from mpd import MPDClient
class GUI:
def __init__(self):
self.mpd = MPDClient()
self.mpd.timeout = 10
self.mpd.connect("localhost", 6600)
self.window = Gtk.Window()
self.window.connect("delete-event", Gtk.main_quit)
self.window.show_all()
thread = Thread(target=self.get_current_song, args=())
thread.start()
Gtk.main()
def get_current_song(self):
self.mpd.idle()
print(self.mpd.currentsong())
return True
app = GUI()
Related
I have a multi-window Gtk application, which is an installer.
During the installation process, which takes some time, I want to show a Window with a label to notify the user that the installation is in progress.
So I tried to bind the respective method to the show event.
However, that causes the appearance of the window to be delayed until the the method finishes, after which the next window is immediately shown.
The result is, that the previous window shows, then the screen goes blank for the duration of the actual installation and then the final window is shown.
I boiled the issue down to the fact, that the show event is obviously triggered, before the window is actually shown.
Here's a minimal snipped to clarify my issue.
The window shows after the call to sleep(), not before.
#! /usr/bin/env python3
from time import sleep
from gi import require_version
require_version('Gtk', '3.0')
from gi.repository import Gtk
class GUI(Gtk.ApplicationWindow):
def __init__(self):
"""Initializes the GUI."""
super().__init__(title='Gtk Window')
self.set_position(Gtk.WindowPosition.CENTER)
self.grid = Gtk.Grid()
self.add(self.grid)
self.label = Gtk.Label()
self.label.set_text('Doing stuff')
self.grid.attach(self.label, 0, 0, 1, 1)
self.connect('show', self.on_show)
def on_show(self, *args):
print('Doing stuff.')
sleep(3)
print('Done stuff.')
def main() -> None:
"""Starts the GUI."""
win = GUI()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()
if __name__ == '__main__':
main()
How can I achieve, that the window shows before the method on_show() is called?
The desired program flow is
Show window
run installation
hide window (and show next one)
without any user interaction.
I fixed it by using multiprocessing :
from ast import Gt
from time import sleep
from multiprocessing import Process
from gi import require_version
require_version('Gtk', '3.0')
from gi.repository import Gtk
class GUI(Gtk.ApplicationWindow):
def __init__(self):
"""Initializes the GUI."""
super().__init__(title='Gtk Window')
self.set_position(Gtk.WindowPosition.CENTER)
self.grid = Gtk.Grid()
self.add(self.grid)
self.label = Gtk.Label()
self.label.set_text('Doing stuff')
self.grid.attach(self.label, 0, 0, 1, 1)
def on_show():
print('Doing stuff.')
sleep(3)
print('Done stuff.')
def main() -> None:
"""Starts the GUI."""
win = GUI()
win.connect('destroy', Gtk.main_quit)
win.show_all()
p1 = Process(target = on_show)
p1.start()
Gtk.main()
if __name__ == '__main__':
main()
Basically, what was happening is that Gtk.main() renders your app while show_all() just packs the widgets, so putting a sleep func before it delays the render.
another method is to use threading (which also provides you an easy way to implemant a progress bar) :
from ast import Gt
from time import sleep
import threading
from gi import require_version
require_version('Gtk', '3.0')
from gi.repository import Gtk
class GUI(Gtk.ApplicationWindow):
def __init__(self):
"""Initializes the GUI."""
super().__init__(title='Gtk Window')
self.set_position(Gtk.WindowPosition.CENTER)
self.grid = Gtk.Grid()
self.add(self.grid)
self.label = Gtk.Label()
self.label.set_text('Doing stuff')
self.grid.attach(self.label, 0, 0, 1, 1)
thread = threading.Thread(target=self.on_show)
thread.daemon = True
thread.start()
def on_show(self, *args):
print('Doing stuff.')
sleep(3)
print('Done stuff.')
def main() -> None:
"""Starts the GUI."""
win = GUI()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()
if __name__ == '__main__':
main()
Further informations on threads
My problem is summed in title. When I call method setHtml on instance of QtWebPageRenderer, SIGILL signal is emitted and my application goes down.
I'm aware that this issue is caused by bad Qt5 dynamic library but I installed it with:
sudo pip install PyQt5 --only-binary PyQt5
sudo pip install PyQtWebEngine --only-binary PyQtWebEngine
so I thought I will get correct precompiled library. When I tried to install PyQt5 without --only-binary, I always ended with some strange compilation error. Something like qmake is not in PATH even though it is and I'm able to call qmake from shell.
So my question is, how to make PyQt5 running on Fedora 31 without any SIGILLs.
EDIT:
Following code can replicate the issue. That information about SIGILL is little inaccurate because first signal is actually SIGTRAP, after I hit continue in gdb, I got SIGILL. This hints that Qt is actually trying to say something to me, although in not very intuitive way.
After some playing around with it, I found that without thread, its ok. Does this mean that Qt forces user to use QThread and not python threads? Or it means that I can't call methods of Qt objects outside of thread where event loop is running?
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
self.web_view.load(QtCore.QUrl("https://www.worldometers.info/coronavirus/"))
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()
You have to have several restrictions:
A QObject is not thread-safe so when creating "web_view" in the main thread then it is not safe to modify it in the secondary thread
Since the QWebEnginePage tasks run asynchronously then you need a Qt eventloop.
So if you want to use python's Thread class then you must implement both conditions:
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
def run(self):
# The QWebEnginePage was created in a new thread and
# that thread has an eventloop
loop = QtCore.QEventLoop()
web_view = WebView()
web_view.load(QtCore.QUrl("https://www.worldometers.info/coronavirus/"))
loop.exec_()
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
runner = Runner()
runner.start()
app.exec_()
if __name__ == "__main__":
main()
In reality QThread and threading.Thread() are native thread handlers of the OS, so in practical terms it can be said that QThread is a threading.Thread() + QObject with an eventloop running on the secondary thread.
On the other hand, if your objective is to call a function from a thread to which it does not belong, then you should use asynchronous methods as pointed out in this answer.
In this case the simplest is to use pyqtSlot + QMetaObject:
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
#QtCore.pyqtSlot(QtCore.QUrl)
def load(self, url):
QWebEnginePage.load(self, url)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
url = QtCore.QUrl("https://www.worldometers.info/coronavirus/")
QtCore.QMetaObject.invokeMethod(
self.web_view,
"load",
QtCore.Qt.QueuedConnection,
QtCore.Q_ARG(QtCore.QUrl, url),
)
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()
Or functools.partial() + QTimer
from functools import partial
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
wrapper = partial(
self.web_view.load,
QtCore.QUrl("https://www.worldometers.info/coronavirus/"),
)
QtCore.QTimer.singleShot(0, wrapper)
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()
I'm trying to migrate open source application written in PyQt4 to Qt5 (PyQt5 or PySide2) and I have a problem with migration of QtGui.QX11EmbedContainer to Qt5. It looks that XEmbed container created with QtWidgets.QWidget.createWindowContainer cannot grab keyboard properly.
In Qt4 it was possible to grab keyboard in such way (for simplicity I'll attach xterm):
import sys
from PyQt4 import QtGui, QtCore
def clientEmbed():
container.grabKeyboard()
print("Keyboard grabbed")
def clientClosed():
container.releaseKeyboard()
print("Keyboard released")
app = QtGui.QApplication(sys.argv)
mainWidget = QtGui.QWidget()
container = QtGui.QX11EmbedContainer(mainWidget)
container.clientIsEmbedded.connect(clientEmbed)
container.clientClosed.connect(clientClosed)
vBoxLayout = QtGui.QVBoxLayout()
vBoxLayout.addWidget(container)
mainWidget.setLayout(vBoxLayout)
process = QtCore.QProcess(container)
winId = container.winId()
mainWidget.show()
process.start("xterm", "-into {} -xrm xterm*.allowSendEvents:true".format(winId).split())
sys.exit(app.exec_())
In this case any key event was sent to xterm, but in Qt5 analogous code doesn't work properly and xterm doesn't receives any key events:
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
app = QtWidgets.QApplication(sys.argv)
def processStarted():
container.grabKeyboard()
print("Process started")
mainWidget = QtWidgets.QWidget()
mainWidget.showMaximized()
window = QtGui.QWindow()
container = QtWidgets.QWidget.createWindowContainer(window, mainWidget)
vBoxLayout = QtWidgets.QVBoxLayout()
vBoxLayout.addWidget(container)
mainWidget.setLayout(vBoxLayout)
mainWidget.show()
winId = int(window.winId())
process = QtCore.QProcess(container)
process.started.connect(processStarted)
process.start("xterm", "-into {} -xrm xterm*.allowSendEvents:true".format(winId).split())
sys.exit(app.exec_())
At this time I'm considering migration from Qt4 to GTK3 which plays nice with grabbing keyboard on XEmbed, but it will be more time consuming. For example analogous working code in PyGTK:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
from subprocess import Popen
def plugged_added(plugged_socket):
Gdk.Seat.grab(seat, plugged_socket.get_window(), Gdk.SeatCapabilities.KEYBOARD, True)
print("xterm attached")
def plugged_removed(plugged_socket):
Gdk.Seat.ungrab(seat)
print("xterm detached")
seat = Gdk.Display.get_default().get_default_seat()
window = Gtk.Window(title="Xterm embed")
socket = Gtk.Socket()
window.add(socket)
sock_id = str(socket.get_id())
socket.connect("plug-added", plugged_added)
socket.connect("plug-removed", plugged_removed)
cmd = ["xterm", '-xrm', "xterm*.allowSendEvents: true", "-into", sock_id]
Popen(cmd)
socket.show()
window.show()
window.connect("destroy", Gtk.main_quit)
Gtk.main()
So does anybody have idea how to achieve grabbing keyboard in Qt5 like it is possible in Qt4 or PyGTK3? I'm also opened to solutions including xlib, xcb etc.
Hello I'm need to show a sequnce of images whe the user make click in a button, I wrote the next code but only show me the las image... any idea what is wrong?
#!/usr/bin/python3
import os
import time
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class GridWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="NEOTECH")
self.grid = Gtk.Grid()
self.add(self.grid)
self.btnStartTest=Gtk.Button("Iniciar Prueba")
self.btnStartTest.connect("clicked",self.StartTest)
self.image = Gtk.Image()
self.image.set_from_file("logo-NeoTech.png")
self.grid.add(self.btnStartTest)
self.grid.attach(self.image,0,1,1,1)
def StartTest(self,widget):
self.image.set_from_file("gato3.jpg")
time.sleep(2)
self.image.set_from_file("gato4.jpg")
print("fin")
win = GridWindow()
win.set_position(Gtk.WindowPosition.CENTER)
win.set_default_size(1000,480)
win.set_type_hint(Gdk.WindowTypeHint.MENU)
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
Don't use time.sleep() in a GUI program; it blocks the GUI mainloop and makes it unresponsive.
Use GLib.timeout_add instead:
from gi.repository import Gtk, Gdk, GLib
class GridWindow(Gtk.Window):
def StartTest(self,widget):
self.image.set_from_file("gato3.jpg")
GLib.timeout_add(2000, self.show_last_image)
def show_last_image(self):
self.image.set_from_file("gato4.jpg")
print("fin")
I am trying to load a webkit view on a different thread than main thread for gtk.
I see the example PyGTK, Threads and WebKit
I slightly modify for support PyGObject and GTK3:
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import WebKit
import threading
import time
# Use threads
Gdk.threads_init()
class App(object):
def __init__(self):
window = Gtk.Window()
webView = WebKit.WebView()
window.add(webView)
window.show_all()
#webView.load_uri('http://www.google.com') # Here it works on main thread
self.window = window
self.webView = webView
def run(self):
Gtk.main()
def show_html(self):
print 'show html'
time.sleep(1)
print 'after sleep'
# Update widget in main thread
GLib.idle_add(self.webView.load_uri, 'http://www.google.com') # Here it doesn't work
app = App()
thread = threading.Thread(target=app.show_html)
thread.start()
app.run()
Gtk.main()
The result it is a empty window and "after sleep" print is never executed. The idle_add call doesn't work. The only work part is the call commented on main thread.
I need GLib.threads_init() before gdk's.
Just like this:
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import WebKit
import threading
import time
# Use threads
GLib.threads_init()
class App(object):
def __init__(self):
window = Gtk.Window()
webView = WebKit.WebView()
window.add(webView)
window.show_all()
#webView.load_uri('http://www.google.com') # Here it works on main thread
self.window = window
self.webView = webView
def run(self):
Gtk.main()
def show_html(self):
print 'show html'
time.sleep(1)
print 'after sleep'
# Update widget in main thread
GLib.idle_add(self.webView.load_uri, 'http://www.google.com') # Here it doesn't work
app = App()
thread = threading.Thread(target=app.show_html)
thread.start()
app.run()
Gtk.main()