moses-palmer / pystray

GNU General Public License v3.0
463 stars 57 forks source link

Updating Icon #73

Closed Bob1011941 closed 3 years ago

Bob1011941 commented 3 years ago

This is my first time contributing to any git hub so I apologize if this is not the right place to put this. I came up with a solution that doesn't crash and I thought I'd share.

My goal for my project was to update an image frequently. A temperature reading. The documentation says to use icon.icon to change the image but I found that after exactly 3332 calls, it would hang up and stop working.

Hello 3330
Hello 3331
Hello 3332
Exception in thread Thread-1:
Traceback (most recent call last):
  File "D:\project\Python\lib\site-packages\pystray\_win32.py", line 81, in _update_icon
    self._assert_icon_handle()
  File "D:\project\Python\lib\site-packages\pystray\_win32.py", line 342, in _assert_icon_handle
    win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE)
  File "D:\project\Python\lib\site-packages\pystray\_util\win32.py", line 201, in _err
    raise ctypes.WinError()
OSError: [WinError 0] The operation completed successfully.

I think the problem has to do with _update_icon() calling _assert_icon_handle(). _assert_icon_handle() seems to create a new ico image, set that to _icon_handle then _update_icon() updates the actual icon on the task bar. The creating of that ico image seems to become unstable after 3332 calls. Why? I have no idea but here is my work around.

from pystray import _win32
from pystray._util import win32, serialized_image
from PIL import Image, ImageDraw, ImageFont

class myIcon(_win32.Icon):
    def __init__(self, name, **kwargs):
        super().__init__(name, **kwargs)

    # Redefinition of this because I want to use logic for controlling some things
    def _update_icon(self):
        self._message(
            win32.NIM_MODIFY,
            win32.NIF_ICON,
            hIcon=self._icon_handle)
        self._icon_valid = True

    def _assert_icon_handle(self, value=None):
        if value is not None:
            self.icon = value
        with serialized_image(self.icon, 'ICO') as icon_path:
            return(win32.LoadImage(
                None,
                icon_path,
                win32.IMAGE_ICON,
                0,
                0,
                win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE))

    def setIcon(self, value):
        self._icon_handle = value
        self._update_icon()
        self.visible = True

font = ImageFont.truetype(r"C:\Windows\Fonts\Calibri.ttf", 61)

def createImage(val):
    image = Image.new('RGBA', (64, 64), (128, 0, 0, 0))
    draw = ImageDraw.Draw(image)
    draw.text((0, 0), str(val), font=font)
    return image

def callback(icon):
    hello = 0
    i = 0
    while True:
        icon.setIcon(imageList[i])
        hello += 1
        i += 1
        if i >= 99:
            i = 0
        print("Hello " + str(hello))

ico = myIcon("My System Tray")

imageList = []
for i in range(100):
    imageList.append(ico._assert_icon_handle(createImage(i)))

ico.run(setup=callback)

I redefined _update_icon() to not call _assert_icon_handle(), changed _assert_icon_handle() to return the created ico and now use setIcon(value) where value is that returned ico.

In my example, I create a list of ico's that are pictures of numbers from 0 to 99 and in the call back function, use icon.setIcon(imageList[i]) to set the value to the already created ico. This avoids creating an unnecessary amount of ico files which I think was causing problems.

I'm happy to provide more info if needed. :)

moses-palmer commented 3 years ago

Thank you for your contribution!

I have never experienced this error, but upon inspection of serialized_image, I noticed that the file handle was never closed.

I have pushed a fix to fixup-fd-leak. Would you mind testing it for your use-case?

Bob1011941 commented 3 years ago

I installed a fresh virtual environment of python 3.6. I downloaded a zip of your fixed library and used pip to install it to the new environment. I then used this code.

from PIL import Image, ImageDraw, ImageFont
import pystray

font = ImageFont.truetype(r"C:\Windows\Fonts\Calibri.ttf", 61)

def createImage(val):
    image = Image.new('RGBA', (64, 64), (128, 0, 0, 0))
    draw = ImageDraw.Draw(image)
    draw.text((0, 0), str(val), font=font)
    return image

def callback(icon):
    hello = 0
    while True:
        icon.icon = createImage(hello)
        print("Hello " + str(hello))
        hello += 1
        icon.visible = True

ico = pystray.Icon("My System Tray")
ico.run(setup=callback)

It then spat this out when I tried to run it.

(venv) D:\PystrayTest>python Main.py
Hello 0
Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Python_Interpreters\Python36\lib\threading.py", line 916, in _bootstrap_inner
    self.run()
  File "C:\Python_Interpreters\Python36\lib\threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "D:\PystrayTest\venv\lib\site-packages\pystray\_base.py", line 186, in setup_handler
    setup(self)
  File "Main.py", line 21, in callback
    icon.visible = True
  File "D:\PystrayTest\venv\lib\site-packages\pystray\_base.py", line 161, in visible
    self._update_icon()
  File "D:\PystrayTest\venv\lib\site-packages\pystray\_win32.py", line 81, in _update_icon
    self._assert_icon_handle()
  File "D:\PystrayTest\venv\lib\site-packages\pystray\_win32.py", line 342, in _assert_icon_handle
    win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE)
  File "C:\Python_Interpreters\Python36\lib\contextlib.py", line 88, in __exit__
    next(self.gen)
  File "D:\PystrayTest\venv\lib\site-packages\pystray\_util\__init__.py", line 47, in serialized_image
    os.close(fd)
OSError: [Errno 9] Bad file descriptor

I get the same thing on a python 3.9 environment.

Here is the process I went through to install it.

(venv) D:\PystrayTest>python -m pip install pystray-fixup-fd-leak.zip
Processing d:\pystraytest\pystray-fixup-fd-leak.zip
Collecting Pillow (from pystray==0.17.1)
  Using cached https://files.pythonhosted.org/packages/c6/ab/6a1d607a245cd878bc0f939314b56ffd9e978170583bc5b62f4c418a9a60/Pillow-8.0.1-cp36-cp36m-win_amd64.whl
Collecting six (from pystray==0.17.1)
  Using cached https://files.pythonhosted.org/packages/ee/ff/48bde5c0f013094d729fe4b0316ba2a24774b3ff1c52d924a8a4cb04078a/six-1.15.0-py2.py3-none-any.whl
Installing collected packages: Pillow, six, pystray
  Running setup.py install for pystray ... done
Successfully installed Pillow-8.0.1 pystray-0.17.1 six-1.15.0
moses-palmer commented 3 years ago

Oh, sorry about that.

I did a thorough dive through the Windows backend, and discovered that the icons loaded are never released, so the process will eventually run out of GDI resources.

After adding the relevant code, your code, modified to not use a modified icon, runs without crashing. I have merged these changes and released pystray 0.17.2.