xpra icon
Bug tracker and wiki

Ticket #1434: systray-ctypes-guid.patch

File systray-ctypes-guid.patch, 12.2 KB (added by Antoine Martin, 4 years ago)

work in progress converting to ctypes, adding guid support, direct pixel to bitmap function, etc

  • xpra/platform/win32/win32_NotifyIcon.py

     
    88# Based on code from winswitch, itself based on "win32gui_taskbar demo"
    99
    1010import ctypes
    11 from ctypes.wintypes import HWND, UINT, POINT, HICON, BOOL, DWORD, HBITMAP, WCHAR
     11from ctypes.wintypes import HWND, UINT, POINT, HICON, BOOL, DWORD, HBITMAP, WCHAR, LONG, WORD, HANDLE, INT, HDC
    1212
    13 from xpra.util import csv
     13from xpra.util import csv, XPRA_APP_ID
    1414from xpra.platform.win32 import constants as win32con
    1515from xpra.platform.win32.common import GUID, WNDCLASSEX, WNDPROC
    1616from xpra.log import Logger
     
    3232LoadImage = user32.LoadImageW
    3333CreateIconIndirect = user32.CreateIconIndirect
    3434GetDC = user32.GetDC
     35GetDC.argtypes = [HWND]
     36GetDC.restype = HDC
    3537ReleaseDC = user32.ReleaseDC
    3638DestroyWindow = user32.DestroyWindow
    3739PostQuitMessage = user32.PostQuitMessage
     
    4547CreateCompatibleBitmap = gdi32.CreateCompatibleBitmap
    4648SelectObject = gdi32.SelectObject
    4749SetPixelV = gdi32.SetPixelV
     50DeleteDC = gdi32.DeleteDC
     51CreateDIBSection = gdi32.CreateDIBSection
     52CreateBitmap = gdi32.CreateBitmap
     53DeleteObject = gdi32.DeleteObject
    4854
     55
     56def GetProductInfo(dwOSMajorVersion=5, dwOSMinorVersion=0, dwSpMajorVersion=0, dwSpMinorVersion=0):
     57    GetProductInfo = kernel32.GetProductInfo
     58    PDWORD = ctypes.POINTER(DWORD)
     59    GetProductInfo.argtypes = [DWORD, DWORD, DWORD, DWORD, PDWORD]
     60    GetProductInfo.restype  = BOOL
     61    product_type = DWORD(0)
     62    v = GetProductInfo(dwOSMajorVersion, dwOSMinorVersion, dwSpMajorVersion, dwSpMinorVersion, ctypes.byref(product_type))
     63    log("GetProductInfo(%i, %i, %i, %i)=%i product_type=%s", dwOSMajorVersion, dwOSMinorVersion, dwSpMajorVersion, dwSpMinorVersion, v, product_type)
     64    return bool(v)
     65#win7 is actually 6.1:
     66ISWIN7ORHIGHER = GetProductInfo(6, 1)
     67
    4968class ICONINFO(ctypes.Structure):
    5069    __fields__ = [
    5170        ('fIcon',       BOOL),
     
    5776CreateIconIndirect.restype = HICON
    5877CreateIconIndirect.argtypes = [ctypes.POINTER(ICONINFO)]
    5978
    60 class NOTIFYICONDATA(ctypes.Structure):
    61     _fields_ = [
     79if ISWIN7ORHIGHER:
     80    MAX_TIP_SIZE = 128
     81else:
     82    MAX_TIP_SIZE = 64
     83
     84NOTIFYICONDATA_fields= [
    6285        ("cbSize",              DWORD),
    6386        ("hWnd",                HWND),
    6487        ("uID",                 UINT),
     
    6588        ("uFlags",              UINT),
    6689        ("uCallbackMessage",    UINT),
    6790        ("hIcon",               HICON),
    68         ("szTip",               WCHAR * 64),
     91        ("szTip",               WCHAR * MAX_TIP_SIZE),
    6992        ("dwState",             DWORD),
    7093        ("dwStateMask",         DWORD),
    7194        ("szInfo",              WCHAR * 256),
     
    7699        ("hBalloonIcon",        HICON),
    77100    ]
    78101
     102if ISWIN7ORHIGHER:
     103    #full:
     104    class NOTIFYICONDATA(ctypes.Structure):
     105        _fields_ = NOTIFYICONDATA_fields[:]
     106else:
     107    #winxp: V2
     108    class NOTIFYICONDATA(ctypes.Structure):
     109        _fields_ = NOTIFYICONDATA_fields[:-2]
     110
    79111shell32 = ctypes.windll.shell32
    80112Shell_NotifyIcon = shell32.Shell_NotifyIcon
    81113Shell_NotifyIcon.restype = ctypes.wintypes.BOOL
    82114Shell_NotifyIcon.argtypes = [ctypes.wintypes.DWORD, ctypes.POINTER(NOTIFYICONDATA)]
    83115
     116BI_BITFIELDS = 0x00000003
     117class CIEXYZ(ctypes.Structure):
     118    _fields_ = [
     119        ('ciexyzX', DWORD),
     120        ('ciexyzY', DWORD),
     121        ('ciexyzZ', DWORD),
     122    ]
     123class CIEXYZTRIPLE(ctypes.Structure):
     124    _fields_ = [
     125        ('ciexyzRed',   CIEXYZ),
     126        ('ciexyzBlue',  CIEXYZ),
     127        ('ciexyzGreen', CIEXYZ),
     128    ]
     129class BITMAPV5HEADER(ctypes.Structure):
     130    _fields_ = [
     131        ('bV5Size',             DWORD),
     132        ('bV5Width',            LONG),
     133        ('bV5Height',           LONG),
     134        ('bV5Planes',           WORD),
     135        ('bV5BitCount',         WORD),
     136        ('bV5Compression',      DWORD),
     137        ('bV5SizeImage',        DWORD),
     138        ('bV5XPelsPerMeter',    LONG),
     139        ('bV5YPelsPerMeter',    LONG),
     140        ('bV5ClrUsed',          DWORD),
     141        ('bV5ClrImportant',     DWORD),
     142        ('bV5RedMask',          DWORD),
     143        ('bV5GreenMask',        DWORD),
     144        ('bV5BlueMask',         DWORD),
     145        ('bV5AlphaMask',        DWORD),
     146        ('bV5CSType',           DWORD),
     147        ('bV5Endpoints',        CIEXYZTRIPLE),
     148        ('bV5GammaRed',         DWORD),
     149        ('bV5GammaGreen',       DWORD),
     150        ('bV5GammaBlue',        DWORD),
     151        ('bV5Intent',           DWORD),
     152        ('bV5ProfileData',      DWORD),
     153        ('bV5ProfileSize',      DWORD),
     154        ('bV5Reserved',         DWORD),
     155    ]
    84156
     157CreateDIBSection.restype = HBITMAP
     158CreateDIBSection.argtypes = [HANDLE, ctypes.POINTER(BITMAPV5HEADER), UINT, ctypes.POINTER(ctypes.c_void_p), HANDLE, DWORD]
     159
     160CreateBitmap.restype = HBITMAP
     161CreateBitmap.argtypes = [INT, INT, UINT, UINT, ctypes.POINTER(ctypes.c_void_p)]
     162
     163XPRA_GUID = GUID()
     164XPRA_GUID.Data1 = 0x67b3efa2
     165XPRA_GUID.Data2 = 0xe470
     166XPRA_GUID.Data3 = 0x4a5f
     167XPRA_GUID.Data4 = (0xb6, 0x53, 0x6f, 0x6f, 0x98, 0xfe, 0x60, 0x81)
     168
    85169FALLBACK_ICON = LoadIcon(0, win32con.IDI_APPLICATION)
    86170
    87171#constants found in win32gui:
     
    178262        if not r:
    179263            raise Exception("Shell_NotifyIcon failed to ADD")
    180264
    181     def make_nid(self, flags, version_timeout=5000):
     265    def make_nid(self, flags):
    182266        nid = NOTIFYICONDATA()
    183267        nid.cbSize = ctypes.sizeof(NOTIFYICONDATA)
    184268        nid.hWnd = self.hwnd
    185         nid.uID = 20+self.app_id
    186269        nid.uCallbackMessage = win32con.WM_MENUCOMMAND
    187270        nid.hIcon = self.current_icon
    188         nid.szTip = self.title
     271        title = self.title[:MAX_TIP_SIZE-1]
     272        #nid.szTip = title.encode("utf-8")
     273        nid.szTip = title
     274        log.warn("szTip=%s (%s)", nid.szTip, type(nid.szTip))
    189275        nid.dwState = 0
    190276        nid.dwStateMask = 0
    191         nid.uVersion = version_timeout
    192277        #balloon notification bits:
    193278        #szInfo
    194279        #uTimeout
    195280        #szInfoTitle
    196281        #dwInfoFlags
    197         #guidItem
    198282        #hBalloonIcon
     283        if ISWIN7ORHIGHER:
     284            #flags |= NIF_SHOWTIP
     285            if self.app_id==XPRA_APP_ID:
     286                nid.guidItem = XPRA_GUID
     287                flags |= NIF_GUID
     288            else:
     289                nid.uID = self.app_id
     290            nid.uVersion = 4
     291        else:
     292            nid.uVersion = 3
     293        nid.uFlags = flags
    199294        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]))
    200295        return nid
    201296
     
    235330
    236331    def set_icon_from_data(self, pixels, has_alpha, w, h, rowstride, options={}):
    237332        #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
     333        log("set_icon_from_data%s", ("%s pixels" % len(pixels), has_alpha, w, h, rowstride, options))
     334        from PIL import Image       #@UnresolvedImport
    240335        if has_alpha:
    241             rgb_format = "RGBA"
     336            img_format = "RGBA"
    242337        else:
    243             rgb_format = "RGB"
    244         img = Image.frombuffer(rgb_format, (w, h), pixels, "raw", rgb_format, 0, 1)
     338            img_format = "RGBX"
     339        rgb_format = options.get("rgb_format", "RGBA")
     340        img = Image.frombuffer(img_format, (w, h), pixels, "raw", rgb_format, rowstride, 1)
     341        assert img, "failed to load image from buffer (%i bytes for %ix%i %s)" % (len(pixels), w, h, rgb_format)
    245342        #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)
     343        icon_w = GetSystemMetrics(win32con.SM_CXSMICON)
     344        icon_h = GetSystemMetrics(win32con.SM_CYSMICON)
     345        if w!=icon_w or h!=icon_h:
     346            log("resizing tray icon to %ix%i", icon_w, icon_h)
     347            img = img.resize((w, h), Image.ANTIALIAS)
     348
     349        header = BITMAPV5HEADER()
     350        header.bV5Size = ctypes.sizeof(BITMAPV5HEADER)
     351        header.bV5Width = icon_w
     352        header.bV5Height = icon_h
     353        header.bV5Planes = 1
     354        header.bV5BitCount = 32
     355        header.bV5Compression = BI_BITFIELDS
     356        header.bV5RedMask = 0x00ff0000
     357        header.bV5GreenMask = 0x0000ff00
     358        header.bV5BlueMask = 0x000000ff
    249359        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")
     360            header.bV5AlphaMask = 0xff000000
    256361        else:
    257             #no alpha: just use image as mask:
    258             mask = img
     362            header.bV5AlphaMask = 0x00000000
    259363
    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
     364        bitmap = 0
     365        mask = 0
     366        try:
     367            hdc = GetDC(None)
     368            dataptr = ctypes.c_void_p()
     369            log("GetDC()=%#x", hdc)
     370            bitmap = CreateDIBSection(hdc, ctypes.byref(header), win32con.DIB_RGB_COLORS, ctypes.byref(dataptr), None, 0)
     371            ReleaseDC(None, hdc)
     372            assert dataptr and bitmap, "failed to create DIB section"
     373            log("CreateDIBSection(..) got bitmap=%#x, dataptr=%s", int(bitmap), dataptr)
    274374
    275         hicon = FALLBACK_ICON
    276         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)
     375            img_bytes = img.tobytes()
     376            img_data = ctypes.create_string_buffer(img_bytes)
     377            ctypes.memmove(dataptr, ctypes.byref(img_data), icon_w*4*icon_h)
     378
     379            mask = CreateBitmap(icon_w, icon_h, 1, 1, None)
     380            log("CreateBitmap(..)=%#x", int(mask))
     381
     382            iconinfo = ICONINFO()
     383            iconinfo.fIcon = True
     384            iconinfo.hbmMask = mask
     385            iconinfo.hbmColor = bitmap
     386            hicon = CreateIconIndirect(ctypes.byref(iconinfo))
     387            log("CreateIconIndirect()=%s", hicon)
     388            if not hicon:
     389                raise ctypes.WinError(ctypes.get_last_error())
     390        except Exception:
     391            log.error("Error: failed to set tray icon", exc_info=True)
     392            hicon = FALLBACK_ICON
    299393        finally:
    300             #DeleteDC(dc)
    301             if hicon!=FALLBACK_ICON:
    302                 DestroyIcon(hicon)
     394            if mask:
     395                DeleteObject(mask)
     396            if bitmap:
     397                DeleteObject(bitmap)
     398        self.do_set_icon(hicon)
     399        UpdateWindow(self.hwnd)
     400        self.reset_function = (self.set_icon_from_data, pixels, has_alpha, w, h, rowstride)
    303401
    304402    def LoadImage(self, iconPathName, fallback=FALLBACK_ICON):
    305403        v = fallback