Search code examples
windows-7python-3.5keydownpygletsystray

How to display an icon in the systray reflecting NumLk state


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()

Solution

  • 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.