moses-palmer / pystray

GNU General Public License v3.0
482 stars 59 forks source link

Can't Get A Menu to Work On Windows 10 #99

Closed csm10495 closed 3 years ago

csm10495 commented 3 years ago

Hi there,

I'm having a bit of trouble getting this to work on Windows 10. Here is some sample code:

from PIL import Image, ImageDraw, ImageColor
import pystray
import time
import threading

def create_image():
    width = 512
    height = 512
    # Generate an image and draw a pattern
    image = Image.new('RGB', (width, height), ImageColor.getrgb('green'))
    dc = ImageDraw.Draw(image)
    dc.rectangle(
        (width // 2, 0, width, height // 2),
        fill=ImageColor.getrgb('red'))
    dc.rectangle(
        (0, height // 2, width // 2, height),
        fill=ImageColor.getrgb('red'))
    return image

def on_clicked(icon, item):
    print(icon)
    print(item)

icon = pystray.Icon('test name', create_image(), menu=pystray.Menu(
    pystray.MenuItem(
        'Test Button', on_clicked)
    )
)

# Use a thread. Otherwise we can't Ctrl-C to break out of the app.
icon_run_thread = threading.Thread(target=icon.run, daemon=True)
icon_run_thread.start()

try:
    while True:
        time.sleep(.1)
except KeyboardInterrupt:
    icon.stop()

Ultimately its just supposed to be an icon in the notification center that i can click to get a menu to pop up. That menu would have one button that i can click which would print something. I get the icon, but regardless of left or right click, nothing pops up.

image

Am I missing something?

Microsoft Windows [Version 10.0.19043.1052] version = (0, 17, 3)

csm10495 commented 3 years ago

It seems to have to do with using a daemon thread. But if i don't use a daemon thread, I can't CTRL-C to exit the app.

csm10495 commented 3 years ago

Made an example that works:

from PIL import Image, ImageDraw, ImageColor
import pystray
import time
import threading

def create_image():
    width = 512
    height = 512
    # Generate an image and draw a pattern
    image = Image.new('RGB', (width, height), ImageColor.getrgb('green'))
    dc = ImageDraw.Draw(image)
    dc.rectangle(
        (width // 2, 0, width, height // 2),
        fill=ImageColor.getrgb('red'))
    dc.rectangle(
        (0, height // 2, width // 2, height),
        fill=ImageColor.getrgb('red'))
    return image

def on_clicked(icon, item):
    print(icon)
    print(item)

class IconThread(threading.Thread):
    def __init__(self, *icon_args, **icon_kwargs):
        self.icon = None
        self._icon_args = icon_args
        self._icon_kwargs = icon_kwargs

        threading.Thread.__init__(self, daemon=True)

    def run(self):
        self.icon = pystray.Icon(*self._icon_args, **self._icon_kwargs)
        self.icon.run()

    def stop(self):
        if self.icon:
            self.icon.stop()

icon_thread = IconThread('test name', create_image(), menu=pystray.Menu(
            pystray.MenuItem(
                'Test Button', on_clicked)
            )
        )
icon_thread.start()

try:
    while True:
        time.sleep(.1)
except KeyboardInterrupt:
    icon_thread.stop()

The reason the original doesn't work is that the hwnd used in the win32 calls is None. That leads to only looking for messages to the current thread... and that current thread is a different thread if i spin up a daemon thread. The example handles that by creating and running Icon in the same thread.

Leaving this here for future folks who hit this issue.

moses-palmer commented 3 years ago

Thank you for your report, and I apologise for this late reply.

I just closed #94 with the same resolution, so this should really go into the documentation.