Dear members of our community,
The following is a simple wx.app with only a text control. When this text control is edited a large process starts. If one edits the control slowly, the editing feels normal. However, if the human is a fast typer, the editor stalls. I think the solution is to kill the previous call of the function self.VeryLargeProcess().
How do you do it?
Is there a better way to do this?
import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "My Frame")
main_panel = wx.Panel(self)
static_box = wx.StaticBox( main_panel, -1, "Text control" )
static_box.SetFont( wx.Font( 15, wx.ROMAN, wx.NORMAL, wx.NORMAL ) )
main_sizer = wx.StaticBoxSizer( static_box, wx.VERTICAL )
text_ctrl = wx.TextCtrl( main_panel, wx.ID_ANY, "Edit me, a large process will start", wx.DefaultPosition, [200,100], wx.TE_MULTILINE )
main_sizer.Add( text_ctrl, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
main_panel.SetSizer( main_sizer )
main_sizer.SetSizeHints( self )
self.Bind(wx.EVT_TEXT, self.VeryLargeProcess, text_ctrl)
def VeryLargeProcess(self, event):
# kill previous self.VeryLargeProcess()
for i in range(0, 3000000):
a = i
if __name__ == '__main__':
app = wx.App()
frame = MyFrame()
frame.Show()
app.MainLoop()
If the process takes a "long time" to run, then you need to put that process in a separate thread or your GUI's responsiveness will slow or stop. The wxPython wiki has a good article on the various ways to use threads and I wrote a tutorial on the topic as well.
Related
all.
I'd like to be able to switch between multiple screens. Meaning, the first one is the main, then when with a button or an external switch is activated I can see the page #2, in that one I may have an other button to return to the first one, or going to #3, etc. Cause I have a main screen for a big RPM meter, but I may want to see instead all three meter on the same page, or view the raw data in an other page, or go to the set-up page or elsewhere in the future development. I'm using the full screen space for my graphic. Maybe something like "hide" or "show" a page with an event of some kind. I have a single class script for every pages so far, but unable to group them in a single one. Thanks for your help
I wrote about this concept several years ago here. I went ahead an reproduced the example from that article:
import wx
import wx.grid as gridlib
class PanelOne(wx.Panel):
""""""
def __init__(self, parent):
"""Constructor"""
wx.Panel.__init__(self, parent=parent)
txt = wx.TextCtrl(self)
class PanelTwo(wx.Panel):
""""""
def __init__(self, parent):
"""Constructor"""
wx.Panel.__init__(self, parent=parent)
grid = gridlib.Grid(self)
grid.CreateGrid(25,12)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(grid, 0, wx.EXPAND)
self.SetSizer(sizer)
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY,
"Panel Switcher Tutorial")
self.panel_one = PanelOne(self)
self.panel_two = PanelTwo(self)
self.panel_two.Hide()
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.panel_one, 1, wx.EXPAND)
self.sizer.Add(self.panel_two, 1, wx.EXPAND)
self.SetSizer(self.sizer)
menubar = wx.MenuBar()
fileMenu = wx.Menu()
switch_panels_menu_item = fileMenu.Append(wx.ID_ANY,
"Switch Panels",
"Some text")
self.Bind(wx.EVT_MENU, self.onSwitchPanels,
switch_panels_menu_item)
menubar.Append(fileMenu, '&File')
self.SetMenuBar(menubar)
def onSwitchPanels(self, event):
""""""
if self.panel_one.IsShown():
self.SetTitle("Panel Two Showing")
self.panel_one.Hide()
self.panel_two.Show()
else:
self.SetTitle("Panel One Showing")
self.panel_one.Show()
self.panel_two.Hide()
self.Layout()
# Run the program
if __name__ == "__main__":
app = wx.App(False)
frame = MyForm()
frame.Show()
app.MainLoop()
The basic idea here is to Hide() one panel and Show() another. You might also want to look at the Notebook controls that wxPython provides as they have a similar functionality.
I am testing on windows 7, 64-bit, MSYS2 Mingw64 shell (the shell start command is C:\msys64\msys2_shell.cmd -use-full-path -mingw64); here I have installed via pacman: mingw-w64-x86_64-python2-2.7.13-1, mingw-w64-x86_64-wxWidgets-3.0.2-17 and mingw-w64-x86_64-wxPython-3.0.2.0-6.
Consider this code, which only has a title label, button and target label; when button is clicked, the label should change from "X" to "1":
import wx #, wx.html
import sys, os
from threading import Thread
# disable buffering (Windows)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
class Frame(wx.Frame):
def __init__(self, *args, **kwds):
kwds["style"] = wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.label = wx.StaticText(self, wx.ID_ANY, "Click the button to change label below: ")
self.bt_main = wx.Button(self, label="Click ME")
self.bt_main.Bind(wx.EVT_BUTTON, self.BtnClickHandler)
self.label2 = wx.StaticText(self, wx.ID_ANY, "XX")
sizer_vmain_app = wx.BoxSizer(wx.VERTICAL)
sizer_vmain_app.Add(self.label, proportion=0, flag=wx.EXPAND, border=0)
sizer_vmain_app.Add(self.bt_main, proportion=0, flag=0, border=0)
sizer_vmain_app.Add(self.label2, proportion=0, flag=0, border=0)
self.SetSizer(sizer_vmain_app)
self.Layout()
def BtnClickHandler(self, event):
testThread = Thread(target=self.DoBtnClick)
testThread.start()
testThread.join()
def DoBtnClick(self):
print("BtnClickHandler ")
myval = int("1")
self.label2.SetLabel(str(myval))
if __name__ == "__main__":
app = wx.PySimpleApp(0)
wx.InitAllImageHandlers()
app_frame = Frame(None, wx.ID_ANY, "")
app.SetTopWindow(app_frame)
app_frame.Show()
app.MainLoop()
When I run this code as is, then the application freezes when it comes to the self.label2.SetLabel(str(myval)).
However, if I avoid the threading, and use this function instead:
def BtnClickHandler(self, event):
# testThread = Thread(target=self.DoBtnClick)
# testThread.start()
# testThread.join()
self.DoBtnClick()
... then everything works fine. Note that I call this script by running python test.py in the MSYS2 Mingw64 shell.
So, is it possible to run this code with threading on Windows, and if so, how? (Otherwise, running it with threading under Linux works without problems)
The problem is that wxWidgets library is not thread-safe.
What it means is that you can't access GUI elements from the secondary thread. GUI access is supported from the main thread only - the one on which you created the application object.
You can't call methods affecting the GUI from any thread other than the main one. Instead, post an event to the main thread asking it to perform the required operation on the worker thread behalf.
In C++ the simplest way to do it, by far, is to use CallAfter() with a lambda, e.g. you could just do CallAfter([=](){ label2->SetLabel(myval); }) in the thread code. Unfortunately I don't know if this is available from Python.
My computer doesn't have any way of letting me know if my NumLk is on or off, so I am trying to add an icon in my systray that will changed depending on the state of my NumLk. This .py will always be running when my computer is on.
So far I was able to mix 3 codes and I am able to display the icon in the systray but it doesn't get updated when the state of NumLk change. Actually if I press NumLk twice, I still get the same icon (the on one) and I get this error:
QCoreApplication::exec: The event loop is already running
File "\systray_icon_NumLk_on_off.py", line 21, in on_key_press
main(on)
File "\systray_icon_NumLk_on_off.py", line 46, in main
sys.exit(app.exec_())
SystemExit: -1
My code may not be the best way to do it, so any alternative is welcome! Here is what I came up so far:
#####get the state of NumLk key
from win32api import GetKeyState
from win32con import VK_NUMLOCK
#how to use: print(GetKeyState(VK_NUMLOCK))
#source: http://stackoverflow.com/questions/21160100/python-3-x-getting-the-state-of-caps-lock-num-lock-scroll-lock-on-windows
#####Detect if NumLk is pressed
import pyglet
from pyglet.window import key
window = pyglet.window.Window()
#source: http://stackoverflow.com/questions/28324372/detecting-a-numlock-capslock-scrlock-keypress-keyup-in-python
on=r'on.png'
off=r'off.png'
#window.event
def on_key_press(symbol, modifiers):
if symbol == key.NUMLOCK:
if GetKeyState(VK_NUMLOCK):
#print(GetKeyState(VK_NUMLOCK))#should be 0 and 1 but
main(on)
else:
main(off)
#window.event
def on_draw():
window.clear()
### display icon in systray
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
#source: http://stackoverflow.com/questions/893984/pyqt-show-menu-in-a-system-tray-application - add answer PyQt5
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def __init__(self, icon, parent=None):
QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
menu = QtWidgets.QMenu(parent)
exitAction = menu.addAction("Exit")
self.setContextMenu(menu)
def main(image):
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
trayIcon = SystemTrayIcon(QtGui.QIcon(image), w)
trayIcon.show()
sys.exit(app.exec_())
if __name__ == '__main__':
pyglet.app.run()
The reason for QCoreApplication::exec: The event loop is already running is actually because you're trying to start app.run() twice. Qt will notice there's already an instance running and throw this exception. When instead, what you want to do is just swap the icon in the already running instance.
Your main problem here is actually the mix of libraries to solve one task if you ask me.
Rather two tasks, but using Qt5 for the graphical part is fine tho.
The way you use Pyglet is wrong from the get go.
Pyglet is intended to be a highly powerful and effective graphics library where you build a graphics engine ontop of it. For instance if you're making a game or a video-player or something.
The way you use win32api is also wrong because you're using it in a graphical window that only checks the value when a key is pressed inside that window.
Now, if you move your win32api code into a Thread (a QtThread to be precise) you can check the state no matter if you pressed your key inside your graphical window or not.
import sys
import win32api
import win32con
from PyQt5 import QtCore, QtGui, QtWidgets
from threading import Thread, enumerate
from time import sleep
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def __init__(self, icon, parent=None):
QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
menu = QtWidgets.QMenu(parent)
exitAction = menu.addAction("Exit")
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(QtWidgets.qApp.quit)
self.setContextMenu(menu)
class KeyCheck(QtCore.QThread):
def __init__(self, mainWindow):
QtCore.QThread.__init__(self)
self.mainWindow = mainWindow
def run(self):
main = None
for t in enumerate():
if t.name == 'MainThread':
main = t
break
while main and main.isAlive():
x = win32api.GetAsyncKeyState(win32con.VK_NUMLOCK)
## Now, GetAsyncKeyState returns three values,
## 0 == No change since last time
## -3000 / 1 == State changed
##
## Either you use the positive and negative values to figure out which state you're at.
## Or you just swap it, but if you just swap it you need to get the startup-state correct.
if x == 1:
self.mainWindow.swap()
elif x < 0:
self.mainWindow.swap()
sleep(0.25)
class GUI():
def __init__(self):
self.app = QtWidgets.QApplication(sys.argv)
self.state = True
w = QtWidgets.QWidget()
self.modes = {
True : SystemTrayIcon(QtGui.QIcon('on.png'), w),
False : SystemTrayIcon(QtGui.QIcon('off.png'), w)
}
self.refresh()
keyChecker = KeyCheck(self)
keyChecker.start()
sys.exit(self.app.exec_())
def swap(self, state=None):
if state is not None:
self.state = state
else:
if self.state:
self.state = False
else:
self.state = True
self.refresh()
def refresh(self):
for mode in self.modes:
if self.state == mode:
self.modes[mode].show()
else:
self.modes[mode].hide()
GUI()
Note that I don't do Qt programming often (every 4 years or so).
So this code is buggy at it's best. You have to press Ctrl+C + Press "Exit" in your menu for this to stop.
I honestly don't want to put more time and effort in learning how to manage threads in Qt or how to exit the application properly, it's not my area of expertis. But this will give you a crude working example of how you can swap the icon in the lower corner instead of trying to re-instanciate the main() loop that you did.
When I minimize the application window on Windows XP and restore it later, the dock will be hidden. This has to do with view menu which has toggles to set visibility and of course is connected by signals.
I hope this will save someone a few hours of debugging.
Here is a full functional example with both wrong and right code:
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
class Ui_QMainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.resize(200, 200)
self.menubar = QtGui.QMenuBar(self)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 27))
self.menuMenu = QtGui.QMenu(self.menubar)
self.setMenuBar(self.menubar)
self.dock = QtGui.QDockWidget(self)
self.dock.setObjectName("dock")
self.dockContents = QtGui.QWidget()
self.dockContents.setObjectName("dockContents")
self.dock.setWidget(self.dockContents)
self.addDockWidget(QtCore.Qt.DockWidgetArea(4), self.dock)
self.action = QtGui.QAction(self)
self.action.setCheckable(True)
self.action.setChecked(True)
self.action.setObjectName("action")
self.menuMenu.addAction(self.action)
self.menubar.addAction(self.menuMenu.menuAction())
self.setWindowTitle("Example of dock remaining minimized")
self.menuMenu.setTitle("Menu")
self.dock.setWindowTitle("I'm a dock")
self.action.setText("Dock visibility")
if True:
# This is NOT working on Windows XP.
# Minimize the window and restore again, the dock is gone.
# Other than that it works.
QtCore.QObject.connect(self.action,
QtCore.SIGNAL("toggled(bool)"),
self.dock.setVisible)
QtCore.QObject.connect(self.dock,
QtCore.SIGNAL("visibilityChanged(bool)"),
self.action.setChecked)
else:
# This DOES work, but boy it looks nasty, writing useless
# per dock is not nice.
QtCore.QObject.connect(self.action,
QtCore.SIGNAL("triggered()"),
self.toggle_dock)
QtCore.QObject.connect(self.dock,
QtCore.SIGNAL("visibilityChanged(bool)"),
self.action.setChecked)
def toggle_dock(self):
self.dock.setVisible(not self.dock.isVisible())
def main():
app = QtGui.QApplication(sys.argv)
ui = Ui_QMainWindow()
ui.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
There is a much simpler way to do this, using QDock.toggleViewAction. This function returns a ready-made action that handles the checked state automatically.
So your code would become simply:
self.action = self.dock.toggleViewAction()
self.action.setObjectName("action")
self.menuMenu.addAction(self.action)
self.menubar.addAction(self.menuMenu.menuAction())
self.setWindowTitle("Example of dock remaining minimized")
self.menuMenu.setTitle("Menu")
self.dock.setWindowTitle("I'm a dock")
self.action.setText("Dock visibility")
and you can then get rid of all the signal handling.
I want to change the text displayed in my GUI at specific time intervals. After a lot of approaches, I find that, specifically to my requirements, I must use time.sleep() instead of wx.Timer, but time.sleep() freeze the complete GUI. Here's an example of my code:
import wx
import time
DWELL_TIMES = [1, 2, 1, 3]
SCREEN_STRINGS = ['nudge nudge', 'wink wink', 'I bet she does', 'say no more!']
class DM1(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
panel = wx.Panel(self)
text_display = wx.StaticText(panel, pos = (400, 150))
for dwell_time in DWELL_TIMES:
text_display.SetLabel(SCREEN_STRINGS[dwell_time])
time.sleep(float(DWELL_TIMES[dwell_time]))
app = wx.App()
DM1Frame = DM1(None, size = (800, 600))
DM1Frame.Center()
DM1Frame.Show()
app.MainLoop()
Does somebody know why this happen, and how to make the GUI doesn't block?
I guess that Threading could help me, doesn't it? If it does, which is the correct way to put threads inside this code? Is there an alternative to Threading?
Thanks a lot!
As mentioned by others, wx.CallAfter and wx.CallLater are your friends. Study them and learn them. Here is a complete, working example using wx.CallLater. I included other refactoring as I saw fit.
import wx
DATA = [
(1, 'nudge nudge'),
(2, 'wink wink'),
(1, 'I bet she does'),
(3, 'say no more!'),
]
class Frame(wx.Frame):
def __init__(self):
super(Frame, self).__init__(None)
panel = wx.Panel(self)
self.text = wx.StaticText(panel)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.AddStretchSpacer(1)
sizer.Add(self.text, 0, wx.ALIGN_CENTER)
sizer.AddStretchSpacer(1)
panel.SetSizer(sizer)
self.index = 0
self.update()
def update(self):
duration, label = DATA[self.index]
self.text.SetLabel(label)
self.index = (self.index + 1) % len(DATA)
wx.CallLater(int(duration * 1000), self.update)
if __name__ == '__main__':
app = wx.App(None)
frame = Frame()
frame.SetTitle('Example')
frame.SetSize((400, 300))
frame.Center()
frame.Show()
app.MainLoop()
If you look at the documentation for time.sleep(), you see that it basically blocks execution of that thread for the specified interval. The problem is that currently your GUI has only a single thread, so if you block the thread then you block ALL execution in that thread. This means, as you've experienced, that the GUI is unusable during the sleep.
Even using threading, the time.sleep() call can't be in the same thread as the GUI, thus trying to get your GUI to refresh after the sleep is over will be very complicated. Beyond that, it's basically reimplementing wx.Timer! No use redoing something that's already been done for you.
It seems to me that your question should be less "how do I make sleeps work?" and more "Why isn't wx.Timer working properly?" Please explain the problem you're having with wx.Timer in detail. Why won't it work? Maybe post some code. My guess is you probably aren't binding the wx.EVT_TIMER properly. Take a look at this tutorial.
Which is the correct way to put threads inside this code?
Although using wx.Timer is the correct solution to this simplified example, if your real goal is to know how to use a worker thread to do long tasks and give updates to your main GUI without freezing your whole application, here's how:
import wx
import threading
import time
class WorkerThread(threading.Thread):
DWELL_TIMES = [1, 2, 1, 3]
SCREEN_STRINGS = ['nudge nudge', 'wink wink', 'I bet she does', 'say no more!']
def __init__(self, window):
threading.Thread.__init__(self)
self.window = window
def run(self):
for i in range(len(WorkerThread.DWELL_TIMES)):
wx.CallAfter(self.window.set_text, WorkerThread.SCREEN_STRINGS[i])
time.sleep(float(WorkerThread.DWELL_TIMES[i]))
wx.CallAfter(self.window.close)
class DM1(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
panel = wx.Panel(self)
self.text_display = wx.StaticText(panel, pos = (400, 150))
self.kickoff_work()
def kickoff_work(self):
t = WorkerThread(self)
t.start()
def set_text(self, text):
self.text_display.SetLabel(text)
def close(self):
self.Close()
app = wx.App()
DM1Frame = DM1(None, size = (800, 600))
DM1Frame.Center()
DM1Frame.Show()
app.MainLoop()
You might try making a global variable that gets the time when it first starts, then having a second variable get the current time and see if the two times are far enough apart to work. Something like this:
When the text changes to something new,
global timestart
timestart = gettime()
Then, where you check if you are changing the code,
timestop = gettime()
if timestop - timestart >= timebetweenchanges:
change code
I don't understand why you can't use a timer for this. They seem to be made for the exact purpose you need them for. As acattle mentioned already, I wrote a tutorial on the subject.
He is completely right though. Using time.sleep() will freeze the GUI because it blocks wx's main event loop. If you absolutely HAVE to use time.sleep() (which I doubt), then you can use a thread. I wrote a tutorial on that subject too. In fact, I actually use time.sleep() in that example.
I might suggest you go use wx.CallLater. Refer to official doc: http://wxpython.org/docs/api/wx.CallLater-class.html
A convenience class for wx.Timer, that calls the given callable object
once after the given amount of milliseconds, passing any positional or
keyword args. The return value of the callable is availbale after it
has been run with the GetResult method.
If you don't need to get the return value or restart the timer then
there is no need to hold a reference to this object. It will hold a
reference to itself while the timer is running (the timer has a
reference to self.Notify) but the cycle will be broken when the timer
completes, automatically cleaning up the wx.CallLater object.
Possible further reference can be found in this question: Using wx.CallLater in wxPython