xpra icon
Bug tracker and wiki

Ticket #1434: systray-ctypes-guid-v2.patch

File systray-ctypes-guid-v2.patch, 14.1 KB (added by Antoine Martin, 3 years ago)

mostly working patch - mask needs fixing

  • xpra/platform/win32/gui.py

     
    187187
    188188def get_native_tray_classes():
    189189    try:
    190         #from xpra.platform.win32.win32_tray import Win32Tray
    191         #return [Win32Tray]
    192         pass
     190        from xpra.platform.win32.win32_tray import Win32Tray
     191        return [Win32Tray]
    193192    except ImportError as e:
    194193        log("no native tray", exc_info=True)
    195194        log.warn("Warning: cannot load native win32 tray")
  • xpra/platform/win32/win32_NotifyIcon.py

     
    77# Low level support for the "system tray" on MS Windows
    88# Based on code from winswitch, itself based on "win32gui_taskbar demo"
    99
     10import struct
    1011import ctypes
    11 from ctypes.wintypes import HWND, UINT, POINT, HICON, BOOL, DWORD, HBITMAP, WCHAR
     12from ctypes.wintypes import HWND, UINT, POINT, HICON, BOOL, DWORD, HBITMAP, WCHAR, LONG, WORD, HANDLE, INT, HDC
    1213
    13 from xpra.util import csv
     14from xpra.util import csv, XPRA_APP_ID
    1415from xpra.platform.win32 import constants as win32con
    1516from xpra.platform.win32.common import GUID, WNDCLASSEX, WNDPROC
    1617from xpra.log import Logger
     
    1819
    1920log("loading ctypes NotifyIcon functions")
    2021
     22sprintf = ctypes.cdll.msvcrt.sprintf
     23
    2124user32 = ctypes.windll.user32
    2225GetSystemMetrics = user32.GetSystemMetrics
    2326GetCursorPos = user32.GetCursorPos
     
    3235LoadImage = user32.LoadImageW
    3336CreateIconIndirect = user32.CreateIconIndirect
    3437GetDC = user32.GetDC
     38GetDC.argtypes = [HWND]
     39GetDC.restype = HDC
    3540ReleaseDC = user32.ReleaseDC
    3641DestroyWindow = user32.DestroyWindow
    3742PostQuitMessage = user32.PostQuitMessage
     
    4550CreateCompatibleBitmap = gdi32.CreateCompatibleBitmap
    4651SelectObject = gdi32.SelectObject
    4752SetPixelV = gdi32.SetPixelV
     53DeleteDC = gdi32.DeleteDC
     54CreateDIBSection = gdi32.CreateDIBSection
     55CreateBitmap = gdi32.CreateBitmap
     56DeleteObject = gdi32.DeleteObject
    4857
     58
     59def GetProductInfo(dwOSMajorVersion=5, dwOSMinorVersion=0, dwSpMajorVersion=0, dwSpMinorVersion=0):
     60    GetProductInfo = kernel32.GetProductInfo
     61    PDWORD = ctypes.POINTER(DWORD)
     62    GetProductInfo.argtypes = [DWORD, DWORD, DWORD, DWORD, PDWORD]
     63    GetProductInfo.restype  = BOOL
     64    product_type = DWORD(0)
     65    v = GetProductInfo(dwOSMajorVersion, dwOSMinorVersion, dwSpMajorVersion, dwSpMinorVersion, ctypes.byref(product_type))
     66    log("GetProductInfo(%i, %i, %i, %i)=%i product_type=%s", dwOSMajorVersion, dwOSMinorVersion, dwSpMajorVersion, dwSpMinorVersion, v, product_type)
     67    return bool(v)
     68#win7 is actually 6.1:
     69ISWIN7ORHIGHER = GetProductInfo(6, 1)
     70
    4971class ICONINFO(ctypes.Structure):
    50     __fields__ = [
     72    _fields_ = [
    5173        ('fIcon',       BOOL),
    5274        ('xHotspot',    DWORD),
    5375        ('yHotspot',    DWORD),
     
    5779CreateIconIndirect.restype = HICON
    5880CreateIconIndirect.argtypes = [ctypes.POINTER(ICONINFO)]
    5981
    60 class NOTIFYICONDATA(ctypes.Structure):
    61     _fields_ = [
     82if ISWIN7ORHIGHER:
     83    MAX_TIP_SIZE = 128
     84else:
     85    MAX_TIP_SIZE = 64
     86
     87SZTIP = WCHAR * 128
     88
     89NOTIFYICONDATA_fields= [
    6290        ("cbSize",              DWORD),
    6391        ("hWnd",                HWND),
    6492        ("uID",                 UINT),
     
    6593        ("uFlags",              UINT),
    6694        ("uCallbackMessage",    UINT),
    6795        ("hIcon",               HICON),
    68         ("szTip",               WCHAR * 64),
     96        ("szTip",               SZTIP),
    6997        ("dwState",             DWORD),
    7098        ("dwStateMask",         DWORD),
    7199        ("szInfo",              WCHAR * 256),
     
    76104        ("hBalloonIcon",        HICON),
    77105    ]
    78106
     107if ISWIN7ORHIGHER:
     108    #full:
     109    class NOTIFYICONDATA(ctypes.Structure):
     110        _fields_ = NOTIFYICONDATA_fields[:]
     111else:
     112    #winxp: V2
     113    class NOTIFYICONDATA(ctypes.Structure):
     114        _fields_ = NOTIFYICONDATA_fields[:-2]
     115
    79116shell32 = ctypes.windll.shell32
    80117Shell_NotifyIcon = shell32.Shell_NotifyIcon
    81118Shell_NotifyIcon.restype = ctypes.wintypes.BOOL
    82119Shell_NotifyIcon.argtypes = [ctypes.wintypes.DWORD, ctypes.POINTER(NOTIFYICONDATA)]
    83120
     121BI_BITFIELDS = 0x00000003
     122class CIEXYZ(ctypes.Structure):
     123    _fields_ = [
     124        ('ciexyzX', DWORD),
     125        ('ciexyzY', DWORD),
     126        ('ciexyzZ', DWORD),
     127    ]
     128class CIEXYZTRIPLE(ctypes.Structure):
     129    _fields_ = [
     130        ('ciexyzRed',   CIEXYZ),
     131        ('ciexyzBlue',  CIEXYZ),
     132        ('ciexyzGreen', CIEXYZ),
     133    ]
     134class BITMAPV5HEADER(ctypes.Structure):
     135    _fields_ = [
     136        ('bV5Size',             DWORD),
     137        ('bV5Width',            LONG),
     138        ('bV5Height',           LONG),
     139        ('bV5Planes',           WORD),
     140        ('bV5BitCount',         WORD),
     141        ('bV5Compression',      DWORD),
     142        ('bV5SizeImage',        DWORD),
     143        ('bV5XPelsPerMeter',    LONG),
     144        ('bV5YPelsPerMeter',    LONG),
     145        ('bV5ClrUsed',          DWORD),
     146        ('bV5ClrImportant',     DWORD),
     147        ('bV5RedMask',          DWORD),
     148        ('bV5GreenMask',        DWORD),
     149        ('bV5BlueMask',         DWORD),
     150        ('bV5AlphaMask',        DWORD),
     151        ('bV5CSType',           DWORD),
     152        ('bV5Endpoints',        CIEXYZTRIPLE),
     153        ('bV5GammaRed',         DWORD),
     154        ('bV5GammaGreen',       DWORD),
     155        ('bV5GammaBlue',        DWORD),
     156        ('bV5Intent',           DWORD),
     157        ('bV5ProfileData',      DWORD),
     158        ('bV5ProfileSize',      DWORD),
     159        ('bV5Reserved',         DWORD),
     160    ]
    84161
     162CreateDIBSection.restype = HBITMAP
     163CreateDIBSection.argtypes = [HANDLE, ctypes.POINTER(BITMAPV5HEADER), UINT, ctypes.POINTER(ctypes.c_void_p), HANDLE, DWORD]
     164
     165CreateBitmap.restype = HBITMAP
     166CreateBitmap.argtypes = [INT, INT, UINT, UINT, ctypes.POINTER(ctypes.c_void_p)]
     167
     168XPRA_GUID = GUID()
     169XPRA_GUID.Data1 = 0x67b3efa2
     170XPRA_GUID.Data2 = 0xe470
     171XPRA_GUID.Data3 = 0x4a5f
     172XPRA_GUID.Data4 = (0xb6, 0x53, 0x6f, 0x6f, 0x98, 0xfe, 0x60, 0x81)
     173
    85174FALLBACK_ICON = LoadIcon(0, win32con.IDI_APPLICATION)
    86175
    87176#constants found in win32gui:
     
    135224            WM_XBUTTONDBLCLK            : [(4, 1), (4, 0)],
    136225            }
    137226
     227def roundup(n, m):
     228    return (n + m - 1) & ~(m - 1)
    138229
     230
    139231class win32NotifyIcon(object):
    140232
    141233    #we register the windows event handler on the class,
     
    178270        if not r:
    179271            raise Exception("Shell_NotifyIcon failed to ADD")
    180272
    181     def make_nid(self, flags, version_timeout=5000):
     273    def make_nid(self, flags):
    182274        nid = NOTIFYICONDATA()
    183275        nid.cbSize = ctypes.sizeof(NOTIFYICONDATA)
    184276        nid.hWnd = self.hwnd
    185         nid.uID = 20+self.app_id
    186277        nid.uCallbackMessage = win32con.WM_MENUCOMMAND
    187278        nid.hIcon = self.current_icon
    188         nid.szTip = self.title
     279        #don't ask why we have to use sprintf to get what we want:
     280        title = self.title[:MAX_TIP_SIZE-1]
     281        sprintf(ctypes.byref(nid,NOTIFYICONDATA.szTip.offset), title)
    189282        nid.dwState = 0
    190283        nid.dwStateMask = 0
    191         nid.uVersion = version_timeout
    192284        #balloon notification bits:
    193285        #szInfo
    194286        #uTimeout
    195287        #szInfoTitle
    196288        #dwInfoFlags
    197         #guidItem
    198289        #hBalloonIcon
     290        if ISWIN7ORHIGHER:
     291            #flags |= NIF_SHOWTIP
     292            if self.app_id==XPRA_APP_ID:
     293                nid.guidItem = XPRA_GUID
     294                flags |= NIF_GUID
     295            else:
     296                nid.uID = self.app_id
     297            nid.uVersion = 4
     298        else:
     299            nid.uVersion = 3
     300        nid.uFlags = flags
    199301        log("make_nid(..)=%s tooltip='%s', app_id=%i, actual flags=%s", nid, title, self.app_id, csv([v for k,v in NIF_FLAGS.items() if k&flags]))
    200302        return nid
    201303
     
    203305        if not self.hwnd:
    204306            return
    205307        try:
    206             nid = self.make_nid(0, 0)
     308            nid = self.make_nid(0)
    207309            log("delete_tray_window(..) calling Shell_NotifyIcon(NIM_DELETE, %s)", nid)
    208310            Shell_NotifyIcon(NIM_DELETE, nid)
    209311        except Exception as e:
     
    235337
    236338    def set_icon_from_data(self, pixels, has_alpha, w, h, rowstride, options={}):
    237339        #this is convoluted but it works..
    238         log("set_icon_from_data%s", ("%s pixels" % len(pixels), has_alpha, w, h, rowstride))
    239         from PIL import Image, ImageOps           #@UnresolvedImport
     340        log("set_icon_from_data%s", ("%s pixels" % len(pixels), has_alpha, w, h, rowstride, options))
     341        from PIL import Image       #@UnresolvedImport
    240342        if has_alpha:
    241             rgb_format = "RGBA"
     343            img_format = "RGBA"
    242344        else:
    243             rgb_format = "RGB"
    244         img = Image.frombuffer(rgb_format, (w, h), pixels, "raw", rgb_format, 0, 1)
     345            img_format = "RGBX"
     346        rgb_format = options.get("rgb_format", "RGBA")
     347        img = Image.frombuffer(img_format, (w, h), pixels, "raw", rgb_format, rowstride, 1)
     348        assert img, "failed to load image from buffer (%i bytes for %ix%i %s)" % (len(pixels), w, h, rgb_format)
    245349        #apparently, we have to use SM_CXSMICON (small icon) and not SM_CXICON (regular size):
    246         size = GetSystemMetrics(win32con.SM_CXSMICON)
    247         if w!=h or w!=size:
    248             img = img.resize((size, size), Image.ANTIALIAS)
    249         if has_alpha:
    250             #extract alpha channel as mask into an inverted "L" channel image:
    251             alpha = img.tobytes("raw", "A")
    252             mask = Image.frombytes("L", img.size, alpha)
    253             mask = ImageOps.invert(mask)
    254             #strip alpha from pixels:
    255             img = img.convert("RGB")
    256         else:
    257             #no alpha: just use image as mask:
    258             mask = img
     350        icon_w = GetSystemMetrics(win32con.SM_CXSMICON)
     351        icon_h = GetSystemMetrics(win32con.SM_CYSMICON)
     352        if w!=icon_w or h!=icon_h:
     353            log("resizing tray icon to %ix%i", icon_w, icon_h)
     354            img = img.resize((w, h), Image.ANTIALIAS)
    259355
    260         def img_to_bitmap(image, pixel_value):
    261             hdc = CreateCompatibleDC(0)
    262             dc = GetDC(0)
    263             hbm = CreateCompatibleBitmap(dc, size, size)
    264             hbm_save = SelectObject(hdc, hbm)
    265             for x in range(size):
    266                 for y in range(size):
    267                     pixel = image.getpixel((x, y))
    268                     v = pixel_value(pixel)
    269                     SetPixelV(hdc, x, y, v)
    270             SelectObject(hdc, hbm_save)
    271             ReleaseDC(self.hwnd, hdc)
    272             ReleaseDC(self.hwnd, dc)
    273             return hbm
     356        header = BITMAPV5HEADER()
     357        header.bV5Size = ctypes.sizeof(BITMAPV5HEADER)
     358        header.bV5Width = icon_w
     359        header.bV5Height = -icon_h
     360        header.bV5Planes = 1
     361        header.bV5BitCount = 32
     362        header.bV5Compression = BI_BITFIELDS
     363        header.bV5RedMask = 0x000000ff
     364        header.bV5GreenMask = 0x0000ff00
     365        header.bV5BlueMask = 0x00ff0000
     366        header.bV5AlphaMask = 0xff000000
    274367
    275         hicon = FALLBACK_ICON
     368        bitmap = 0
     369        mask = 0
    276370        try:
    277             def rgb_pixel(pixel):
    278                 r, g, b = pixel[:3]
    279                 return r+g*256+b*256*256
    280             bitmap = img_to_bitmap(img, rgb_pixel)
    281             if mask is img:
    282                 mask_bitmap = bitmap
    283             else:
    284                 #mask is in "L" mode, so we get the pixel value directly from getpixel(x, y)
    285                 def mask_pixel(l):
    286                     return l+l*256+l*256*256
    287                 mask_bitmap = img_to_bitmap(mask, mask_pixel)
    288             if mask_bitmap:
    289                 pyiconinfo = (True, 0, 0, mask_bitmap, bitmap)
    290                 hicon = CreateIconIndirect(pyiconinfo)
    291                 log("CreateIconIndirect(%s)=%s", pyiconinfo, hicon)
    292                 if hicon==0:
    293                     hicon = FALLBACK_ICON
    294             self.do_set_icon(hicon)
    295             UpdateWindow(self.hwnd)
    296             self.reset_function = (self.set_icon_from_data, pixels, has_alpha, w, h, rowstride, options)
    297         except:
    298             log.error("error setting icon", exc_info=True)
     371            hdc = GetDC(None)
     372            dataptr = ctypes.c_void_p()
     373            log("GetDC()=%#x", hdc)
     374            bitmap = CreateDIBSection(hdc, ctypes.byref(header), win32con.DIB_RGB_COLORS, ctypes.byref(dataptr), None, 0)
     375            ReleaseDC(None, hdc)
     376            assert dataptr and bitmap, "failed to create DIB section"
     377            log("CreateDIBSection(..) got bitmap=%#x, dataptr=%s", int(bitmap), dataptr)
     378
     379            img_bytes = img.tobytes()
     380            img_data = ctypes.create_string_buffer(img_bytes)
     381            ctypes.memmove(dataptr, ctypes.byref(img_data), icon_w*4*icon_h)
     382
     383            assert w%2==0, "cannot handle odd widths yet"
     384            log("img_bytes: %i, %ix%i*4=%i", len(img_bytes), icon_w, icon_h, 4*icon_w*icon_h)
     385            rgbx_array = struct.unpack("@%s" % ("I"*icon_w*icon_h), img_bytes[:4*icon_w*icon_h])
     386            bitmask_data = b"".join([chr(rgbx & 0xff) for rgbx in rgbx_array])
     387            bitmask = ctypes.c_char_p(bitmask_data)
     388            bitmask_ptr = ctypes.cast(ctypes.byref(bitmask), ctypes.c_void_p)
     389            mask = CreateBitmap(icon_w, icon_h, 1, 1, bitmask_ptr)
     390            log("CreateBitmap(..)=%#x", int(mask))
     391
     392            iconinfo = ICONINFO()
     393            iconinfo.fIcon = True
     394            iconinfo.hbmMask = mask
     395            iconinfo.hbmColor = bitmap
     396            hicon = CreateIconIndirect(ctypes.byref(iconinfo))
     397            log("CreateIconIndirect()=%s", hicon)
     398            if not hicon:
     399                raise ctypes.WinError(ctypes.get_last_error())
     400        except Exception:
     401            log.error("Error: failed to set tray icon", exc_info=True)
     402            hicon = FALLBACK_ICON
    299403        finally:
    300             #DeleteDC(dc)
    301             if hicon!=FALLBACK_ICON:
    302                 DestroyIcon(hicon)
     404            if mask:
     405                DeleteObject(mask)
     406            if bitmap:
     407                DeleteObject(bitmap)
     408        self.do_set_icon(hicon)
     409        UpdateWindow(self.hwnd)
     410        self.reset_function = (self.set_icon_from_data, pixels, has_alpha, w, h, rowstride)
    303411
    304412    def LoadImage(self, iconPathName, fallback=FALLBACK_ICON):
    305413        v = fallback