pythontkintericonsdynamically-generatedshortcut-file

Python - Having trouble dynamically creating buttons with images


I'm extremely new to Python (a couple of weeks into my self learning journey) and I've hit a wall.

The Goal:

To have a script that will read the files in the folder the script was run from, dynamically create buttons for any .lnk files it finds, extract the icon from the target .exe file and add it to the button for that link. When clicked, the button will launch the shortcut.lnk file it was pointed to when created. Effectively creating a popup menu of shortcuts in the taskbar for windows.

The Problem:

I can get it to work without the images, but as soon as I try to add an image to the tk.Button(image) it then only works for the last button it creates. I'm sure its something I'm doing wrong with the way I get the image but I just can't figure out what.

Result:

result

The above image shows the result, the first two buttons do absolutely nothing when clicked and the third works as expected. If i comment out the part that adds the image to the button, all three buttons work to open their respective links.

The Code:

import tkinter as tk
import os
import pyautogui
from win32api import GetSystemMetrics
from PIL import Image, ImageTk
import win32com.client
import win32gui
import win32ui


def execlink(linkpath):
    # print(linkpath)
    os.startfile(linkpath)


class IconExtractor:
    def __init__(self):
        self.photo = None
        self.root = None  # Keep a reference to the Tkinter root window
        self.label = None  # Keep a reference to the Label widget

    def extract_icon(self, exe_path, ico_path):
        # Load the executable file
        icon_handles, num_icons = win32gui.ExtractIconEx(exe_path, 0)

        # Check if any icons were found
        if icon_handles:
            # Select the first icon from the list
            h_icon = icon_handles[0]

            # Create a device context
            hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))

            # Create a bitmap and draw the icon into it
            hbmp = win32ui.CreateBitmap()
            hbmp.CreateCompatibleBitmap(hdc, 32, 32)  # You may adjust the size (32x32 in this case)
            hdc = hdc.CreateCompatibleDC()
            hdc.SelectObject(hbmp)
            hdc.DrawIcon((0, 0), h_icon)

            # Save the bitmap as an icon file
            hbmp.SaveBitmapFile(hdc, ico_path)

            # Release the icon resources
            win32gui.DestroyIcon(h_icon)
        else:
            print("No icons found in the executable.")

    def on_closing(self):
        # This function is called when the user closes the Tkinter window
        self.root.destroy()

    def main(self, lnk_path, ico_path):
        # Explicitly initialize the Tkinter event loop
        # root = tk.Tk()
        # root.withdraw()  # Hide the root window

        # lnk_path = r''
        # ico_path = r''

        # Get the target path from the .lnk file
        shell = win32com.client.Dispatch("WScript.Shell")
        shortcut = shell.CreateShortCut(lnk_path)
        target_path = shortcut.TargetPath

        if os.path.exists(target_path):
            # Extract the icon from the executable
            self.extract_icon(target_path, ico_path)

            # Check if the .ico file was generated successfully
            if os.path.exists(ico_path):
                # Open the ICO file using Pillow
                with Image.open(ico_path) as img:
                    # Convert the image to a format compatible with PhotoImage
                    img = img.convert("RGBA")
                    self.photo = ImageTk.PhotoImage(img)

                # # Create the main window
                # self.root = tk.Toplevel(root)
                #
                # # Create a button to activate the .lnk file with the icon
                # button = tk.Button(self.root, text="Activate", compound=tk.LEFT, image=self.photo,
                #                    command=self.activate_lnk)
                # button.pack(side=tk.LEFT)
                #
                # # Bind the closing event
                # self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
                #
                # # Run the Tkinter event loop
                # self.root.mainloop()
            # else:
            #     print("Failed to generate .ico file.")
        else:
            print("Failed to retrieve the target path.")


class LinksMenu:

    def __init__(self):

        self.MainWindow = tk.Tk()
        self.MainWindow.geometry("100x40+" + str(pyautogui.position()[0]) + "+100")
        self.MainWindow.overrideredirect(True)
        self.MainWindow.bind("<KeyPress>", self.closing)

        self.btnframe = tk.Frame(self.MainWindow)
        self.btnframe.configure(bg='')
        self.btnframe.columnconfigure(0, weight=1)

        count = 1

        files = [File for File in os.listdir('.') if os.path.isfile(File)]

        for File in files:
            if File[-4:] == ".lnk":
                GetImage.main(os.path.realpath(File),
                              os.path.dirname(os.path.realpath(File)) + '\\' + File[0:-4] + '.ico')
                newbutton = (tk.Button
                             (self.btnframe,
                              name=File[0:-4].lower() + 'btn',
                              text=File[0:-4],
                              compound=tk.LEFT,
                              image=GetImage.photo,
                              font=('Arial', 14),
                              bg='DodgerBlue4',
                              command=lambda m=os.path.realpath(File): [execlink(m), self.close_after_choice()]))
                newbutton.grid(padx=2,
                               pady=2,
                               row=count,
                               column=0,
                               sticky=tk.W+tk.E)
                count += 1

        self.btnframe.pack(fill='x')

        self.MainWindow.geometry("300x"
                                 + str(count*45)
                                 + "+"
                                 + str(pyautogui.position()[0])
                                 + "+"
                                 + str(GetSystemMetrics(1)-(count*45)))
        self.MainWindow.configure(bg='')
        self.MainWindow.mainloop()

    def closing(self, event):
        if event.state == 8 and event.keysym == "Escape":
            self.MainWindow.destroy()

    def close_after_choice(self):
        self.MainWindow.destroy()


if __name__ == "__main__":
    GetImage = IconExtractor()
    LinksMenu()

I've tried:

Full disclosure, I used ChatGPT to help with the code for the icon extraction.


Solution

  • It is because you have use the same variable to store the references of those images, so only the last one will be kept and the others are garbage collected.

    Save the image reference of each button using an attribute of that button:

    newbutton.photo = GetImage.photo