python - Sådan opbygger du en SystemTray-app til Windows?

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg arbejder som regel på et Linux-system, men jeg har en situation, hvor jeg skal skrive en klientapp, der ville køre på Windows som en serivce. Kan nogen hjælpe mig eller henvise til, hvordan man opbygger en MenuBar-app (f.eks. Som dropbox) til Windows-miljøet, som starter ved OS-opstart, og ikonet sidder i TaskBar, og ved at klikke på appikonet vises en menu.


Mit skriptsprog er python. Tak.

Bedste reference


Du gør dette ved hjælp af pywin32-modulet (Python for Windows Extensions). [8]


Eksempelkode [9]


Lignende spørgsmål


For at få det til at køre ved opstart, kan du røre rundt med tjenester, men det er faktisk meget nemmere at installere et link til exe i brugerne 'Startup Folder'.


Windows 7 og Vista


c: \ Users \ [[brugernavn]] \ AppData \ Roaming \ Microsoft \ Windows \ Startmenu \ Programmer \ Idrifttagning


Windows XP


c: \ Dokumenter og indstillinger \ [[brugernavn]] \ Start menu \ Programmer \ Idrifttagning

Andre referencer 1


Jeg ændrede SysTrayIcon.py scriptet til at arbejde i Python 3 [11]



  • Du skal installere pip install pywin32.

  • Derefter skal du køre python Scripts/pywin32\_postinstall.p -install fra din Python-mappe for at registrere dlls.

  • For at prøve scriptet skal køre, skal du have nogle * .ico-filer i din arbejdskatalog - du kan finde mange af dem i dine c: \ windows * -mapper.

  • For at skjule programmet kan du køre det via pythonw.exe.

  • Hvis du har brug for ballonmeddelelser, skal du kigge på dette indlæg: https://stackoverflow.com/a/42085439/2441026 (Plyer pakke).

  • For at have en menu med kun Afslut-knappen skal du passere menu\_options = ((None, None, None),) - (eller ændre klassen for ikke altid at tilføje menu\_optioner).







#!/usr/bin/env python
# Module     : SysTrayIcon.py
# Synopsis   : Windows System tray icon.
# Programmer : Simon Brunning - simon@brunningonline.net - modified for Python 3
# Date       : 13 February 2018
# Notes      : Based on (i.e. ripped off from) Mark Hammond's
#              win32gui\_taskbar.py and win32gui\_menu.py demos from PyWin32
'''TODO

For now, the demo at the bottom shows how to use it...'''

import os
import sys
import win32api         # package pywin32
import win32con
import win32gui\_struct
try:
    import winxpgui as win32gui
except ImportError:
    import win32gui

class SysTrayIcon(object):
    '''TODO'''
    QUIT = 'QUIT'
    SPECIAL\_ACTIONS = [QUIT]

    FIRST\_ID = 1023

    def \_\_init\_\_(self,
                 icon,
                 hover\_text,
                 menu\_options,
                 on\_quit=None,
                 default\_menu\_index=None,
                 window\_class\_name=None,):

        self.icon = icon
        self.hover\_text = hover\_text
        self.on\_quit = on\_quit

        menu\_options = menu\_options + (('Quit', None, self.QUIT),)
        self.\_next\_action\_id = self.FIRST\_ID
        self.menu\_actions\_by\_id = set()
        self.menu\_options = self.\_add\_ids\_to\_menu\_options(list(menu\_options))
        self.menu\_actions\_by\_id = dict(self.menu\_actions\_by\_id)
        del self.\_next\_action\_id


        self.default\_menu\_index = (default\_menu\_index or 0)
        self.window\_class\_name = window\_class\_name or "SysTrayIconPy"

        message\_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): self.restart,
                       win32con.WM\_DESTROY: self.destroy,
                       win32con.WM\_COMMAND: self.command,
                       win32con.WM\_USER+20 : self.notify,}
        # Register the Window class.
        window\_class = win32gui.WNDCLASS()
        hinst = window\_class.hInstance = win32gui.GetModuleHandle(None)
        window\_class.lpszClassName = self.window\_class\_name
        window\_class.style = win32con.CS\_VREDRAW | win32con.CS\_HREDRAW;
        window\_class.hCursor = win32gui.LoadCursor(0, win32con.IDC\_ARROW)
        window\_class.hbrBackground = win32con.COLOR\_WINDOW
        window\_class.lpfnWndProc = message\_map # could also specify a wndproc.
        classAtom = win32gui.RegisterClass(window\_class)
        # Create the Window.
        style = win32con.WS\_OVERLAPPED | win32con.WS\_SYSMENU
        self.hwnd = win32gui.CreateWindow(classAtom,
                                          self.window\_class\_name,
                                          style,
                                          0,
                                          0,
                                          win32con.CW\_USEDEFAULT,
                                          win32con.CW\_USEDEFAULT,
                                          0,
                                          0,
                                          hinst,
                                          None)
        win32gui.UpdateWindow(self.hwnd)
        self.notify\_id = None
        self.refresh\_icon()

        win32gui.PumpMessages()

    def \_add\_ids\_to\_menu\_options(self, menu\_options):
        result = []
        for menu\_option in menu\_options:
            option\_text, option\_icon, option\_action = menu\_option
            if callable(option\_action) or option\_action in self.SPECIAL\_ACTIONS:
                self.menu\_actions\_by\_id.add((self.\_next\_action\_id, option\_action))
                result.append(menu\_option + (self.\_next\_action\_id,))
            elif non\_string\_iterable(option\_action):
                result.append((option\_text,
                               option\_icon,
                               self.\_add\_ids\_to\_menu\_options(option\_action),
                               self.\_next\_action\_id))
            else:
                print('Unknown item', option\_text, option\_icon, option\_action)
            self.\_next\_action\_id += 1
        return result

    def refresh\_icon(self):
        # Try and find a custom icon
        hinst = win32gui.GetModuleHandle(None)
        if os.path.isfile(self.icon):
            icon\_flags = win32con.LR\_LOADFROMFILE | win32con.LR\_DEFAULTSIZE
            hicon = win32gui.LoadImage(hinst,
                                       self.icon,
                                       win32con.IMAGE\_ICON,
                                       0,
                                       0,
                                       icon\_flags)
        else:
            print("Can't find icon file - using default.")
            hicon = win32gui.LoadIcon(0, win32con.IDI\_APPLICATION)

        if self.notify\_id: message = win32gui.NIM\_MODIFY
        else: message = win32gui.NIM\_ADD
        self.notify\_id = (self.hwnd,
                          0,
                          win32gui.NIF\_ICON | win32gui.NIF\_MESSAGE | win32gui.NIF\_TIP,
                          win32con.WM\_USER+20,
                          hicon,
                          self.hover\_text)
        win32gui.Shell\_NotifyIcon(message, self.notify\_id)

    def restart(self, hwnd, msg, wparam, lparam):
        self.refresh\_icon()

    def destroy(self, hwnd, msg, wparam, lparam):
        if self.on\_quit: self.on\_quit(self)
        nid = (self.hwnd, 0)
        win32gui.Shell\_NotifyIcon(win32gui.NIM\_DELETE, nid)
        win32gui.PostQuitMessage(0) # Terminate the app.

    def notify(self, hwnd, msg, wparam, lparam):
        if lparam==win32con.WM\_LBUTTONDBLCLK:
            self.execute\_menu\_option(self.default\_menu\_index + self.FIRST\_ID)
        elif lparam==win32con.WM\_RBUTTONUP:
            self.show\_menu()
        elif lparam==win32con.WM\_LBUTTONUP:
            pass
        return True

    def show\_menu(self):
        menu = win32gui.CreatePopupMenu()
        self.create\_menu(menu, self.menu\_options)
        #win32gui.SetMenuDefaultItem(menu, 1000, 0)

        pos = win32gui.GetCursorPos()
        # See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus\_0hdi.asp
        win32gui.SetForegroundWindow(self.hwnd)
        win32gui.TrackPopupMenu(menu,
                                win32con.TPM\_LEFTALIGN,
                                pos[0],
                                pos[1],
                                0,
                                self.hwnd,
                                None)
        win32gui.PostMessage(self.hwnd, win32con.WM\_NULL, 0, 0)

    def create\_menu(self, menu, menu\_options):
        for option\_text, option\_icon, option\_action, option\_id in menu\_options[::-1]:
            if option\_icon:
                option\_icon = self.prep\_menu\_icon(option\_icon)

            if option\_id in self.menu\_actions\_by\_id:                
                item, extras = win32gui\_struct.PackMENUITEMINFO(text=option\_text,
                                                                hbmpItem=option\_icon,
                                                                wID=option\_id)
                win32gui.InsertMenuItem(menu, 0, 1, item)
            else:
                submenu = win32gui.CreatePopupMenu()
                self.create\_menu(submenu, option\_action)
                item, extras = win32gui\_struct.PackMENUITEMINFO(text=option\_text,
                                                                hbmpItem=option\_icon,
                                                                hSubMenu=submenu)
                win32gui.InsertMenuItem(menu, 0, 1, item)

    def prep\_menu\_icon(self, icon):
        # First load the icon.
        ico\_x = win32api.GetSystemMetrics(win32con.SM\_CXSMICON)
        ico\_y = win32api.GetSystemMetrics(win32con.SM\_CYSMICON)
        hicon = win32gui.LoadImage(0, icon, win32con.IMAGE\_ICON, ico\_x, ico\_y, win32con.LR\_LOADFROMFILE)

        hdcBitmap = win32gui.CreateCompatibleDC(0)
        hdcScreen = win32gui.GetDC(0)
        hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico\_x, ico\_y)
        hbmOld = win32gui.SelectObject(hdcBitmap, hbm)
        # Fill the background.
        brush = win32gui.GetSysColorBrush(win32con.COLOR\_MENU)
        win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush)
        # unclear if brush needs to be feed.  Best clue I can find is:
        # "GetSysColorBrush returns a cached brush instead of allocating a new
        # one." - implies no DeleteObject
        # draw the icon
        win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico\_x, ico\_y, 0, 0, win32con.DI\_NORMAL)
        win32gui.SelectObject(hdcBitmap, hbmOld)
        win32gui.DeleteDC(hdcBitmap)

        return hbm

    def command(self, hwnd, msg, wparam, lparam):
        id = win32gui.LOWORD(wparam)
        self.execute\_menu\_option(id)

    def execute\_menu\_option(self, id):
        menu\_action = self.menu\_actions\_by\_id[id]      
        if menu\_action == self.QUIT:
            win32gui.DestroyWindow(self.hwnd)
        else:
            menu\_action(self)

def non\_string\_iterable(obj):
    try:
        iter(obj)
    except TypeError:
        return False
    else:
        return not isinstance(obj, str)

# Minimal self test. You'll need a bunch of ICO files in the current working
# directory in order for this to work...
if \_\_name\_\_ == '\_\_main\_\_':
    import itertools, glob

    icons = itertools.cycle(glob.glob('*.ico'))
    hover\_text = "SysTrayIcon.py Demo"
    def hello(sysTrayIcon): print("Hello World.")
    def simon(sysTrayIcon): print("Hello Simon.")
    def switch\_icon(sysTrayIcon):
        sysTrayIcon.icon = next(icons)
        sysTrayIcon.refresh\_icon()
    menu\_options = (('Say Hello', next(icons), hello),
                    ('Switch Icon', None, switch\_icon),
                    ('A sub-menu', next(icons), (('Say Hello to Simon', next(icons), simon),
                                                  ('Switch Icon', next(icons), switch\_icon),
                                                 ))
                   )
    def bye(sysTrayIcon): print('Bye, then.')

    SysTrayIcon(next(icons), hover\_text, menu\_options, on\_quit=bye, default\_menu\_index=1)