spyoungtech / ahk

Python wrapper for AutoHotkey with full type support. Harness the automation power of AutoHotkey with the beauty of Python.
MIT License
887 stars 66 forks source link

Window.from_pid() not working #276

Closed zSynctic closed 7 months ago

zSynctic commented 7 months ago

describe your issue

What I did was that I put a running window's pid and try to use it in from_pid but somehow it always doesn't work even though I already verified that the pid is running and I tried switching it but still

ahk.version

1.5.2

AutoHotkey version

v2

Code to reproduce the issue

from ahk import AHK
import ahk

self.ahk = AHK()
self.win = ahk.Window.from_pid(self.ahk, pid=self.selected_pid)

Traceback/Error message

Exception in Tkinter callback Traceback (most recent call last): File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2288.0_x64__qbz5n2kfra8p0\Lib\tkinterinit.py", line 1967, in call return self.func(args) ^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\customtkinter\windows\widgets\ctk_button.py", line 554, in _clicked self._command() File "D:\Python Programming\Projects\AutoClicker\ZClicker\test.py", line 47, in select_process self.win = ahk.Window.from_pid(self.ahk, pid=self.selected_pid) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\ahk_sync\window.py", line 627, in from_pid return engine.win_get(title=f'ahk_pid {pid}') ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\ahk_sync\engine.py", line 1709, in win_get resp = self._transport.function_call('AHKWinGetID', args, blocking=blocking, engine=self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\ahk_sync\transport.py", line 611, in function_call return self.send(request, engine=engine) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\ahk_sync\transport.py", line 832, in send return response.unpack() # type: ignore ^^^^^^^^^^^^^^^^^ File "C:\Users...\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\ahk\message.py", line 221, in unpack raise self._exception_type(s) ahk.message.AHKExecutionException: Error occurred in WinGetID (line 530). The error message was: Target window not found.. Specifically: ahk_pid 1956 Stack: C:\Users...\AppData\Local\Temp\python-ahk-cf5lqn_k.ahk (530) : [WinGetID] output := WinGetID(title, text, extitle, extext) C:\Users...\AppData\Local\Temp\python-ahk-cf5lqn_k.ahk (530) : [AHKWinGetID] output := WinGetID(title, text, extitle, extext) C:\Users...\AppData\Local\Temp\python-ahk-cf5lqn_k.ahk (2735) : [] pyresp := %func_name%(argsArray)

spyoungtech commented 7 months ago

Hmm. I'm not able to reproduce this issue with a window that exists. Note that when using AutoHotkey v2, the window must exist, or an error will be thrown. This is different from the AutoHotkey v1 behavior where it will instead return None.

The error message was: Target window not found.. Specifically: ahk_pid 1956

It seems like the PID you're using is not a window that exists. Can you confirm the window exists with this PID?

for win in ahk.list_windows():
    print(win.title, win.pid) # does your PID show up here?

But you can see, generally, that this does work (assuming you have notepad.exe) by running this code:

import subprocess, time
from ahk import AHK, Window
ahk = AHK(version='v2')
notepad = subprocess.Popen('notepad.exe')
time.sleep(1)
win = Window.from_pid(ahk, notepad.pid)
print(win.title)  # 'Untitled - Notepad'
zSynctic commented 7 months ago

The window actually exists, my code opens a list of all the programs running with their pids and when u click on one of them it takes the pid and uses it in from_pid but it always doesn't work for some reason

spyoungtech commented 7 months ago

Can you share minimal example code that can reproduce the issue?

zSynctic commented 7 months ago
 def select_process(self):
        # Get the selected process from the Listbox
        self.selected_index = self.process_listbox.curselection()
        if self.selected_index:
            self.selected_pid = self.get_running_processes()[self.selected_index][1]
            print(f"Selected PID: {self.selected_pid}")

            self.win = ahk.Window.from_pid(self.ahk, pid=self.selected_pid)
            if self.win is not None:
                self.getmousepos = threading.Thread(target=self.mousepos)
                self.getmousepos.start()
            else:
                print("No window found for the selected PID")

        else:
            print("No process selected")

    def mousepos(self):
        self.win.click(100, 138)
spyoungtech commented 7 months ago

Sorry, but I'm not able to run this code and it's still unclear how you're getting the process IDs of windows.

I don't think this is a problem with the library, but about how you are obtaining your PIDs or other assumptions being made around that. But without a minimal piece of code I can actually run and instructions to reproduce the issue, I can't help any further.

zSynctic commented 7 months ago

Ok I will send the full code:

from ahk import AHK
import ahk
import customtkinter
from customtkinter import END
import threading
import psutil
from CTkListbox import *

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()
        self.ahk = AHK()

        self.process_listbox = CTkListbox(self, height=30, width=200)
        self.process_listbox.grid(row=0, column=0, padx=10, pady=10)

        self.update_process_list()

        self.select_button = customtkinter.CTkButton(
            self, text="Select", command=self.select_process
        )
        self.select_button.grid(row=1, column=0, pady=10)

    def get_running_processes(self):
        # Get a list of currently running processes
        running_processes = []
        for process in psutil.process_iter(["pid", "name"]):
            pid_name = f"{process.info['pid']} {process.info['name']}"
            running_processes.append((pid_name, process.info["pid"]))
        return running_processes

    def update_process_list(self):
        # Update the list of running processes in the Listbox
        running_processes = self.get_running_processes()
        self.process_listbox.delete(0, END)
        for pid_name, _ in running_processes:
            self.process_listbox.insert(END, pid_name)

    def select_process(self):
        # Get the selected process from the Listbox
        self.selected_index = self.process_listbox.curselection()
        if self.selected_index:
            self.selected_pid = self.get_running_processes()[self.selected_index][1]
            print(f"Selected PID: {self.selected_pid}")

            self.win = ahk.Window.from_pid(self.ahk, pid=self.selected_pid)
            if self.win is not None:
                self.getmousepos = threading.Thread(target=self.mousepos)
                self.getmousepos.start()
            else:
                print("No window found for the selected PID")

        else:
            print("No process selected")

    def mousepos(self):
        self.win.click(100, 138)

    def start(self):
        self.mainloop()

if __name__ == "__main__":
    app = App()
    app.update()
    app.start()
spyoungtech commented 7 months ago

So, something to keep in mind is that not every PID, as returned by psutil.process_iter will be associated with a window that can be retrieved. Many processes have no windows or their window may be hidden. Also keep in mind you're calling get_running_processes twice, which means the list you're presenting to the user won't necessarily be the same list you're using when you pull the pid out in select_process.

I would consider replacing psutil with the AutoHotkey functionality of enumerating windows using ahk.list_windows and retrieving the id and process name from the window objects.

Maybe something like this can replace your get_running_processes method:

def get_available_windows(self):
    available_windows = []
    for win in self.ahk.list_windows():
        selection_name = f'{win.id} {win.process_name} ({win.pid})'
        available_windows.append((selection_name, win))
    return available_windows

Then you can directly pull out the Window object instead, too, and skip the step of creating the Window object from the pid.

I would also think about structuring this so you are not calling this function twice so that the list in which the user is making the selection and the list you're using to obtain the object based on their selection is guaranteed to be the same.

zSynctic commented 7 months ago

I'm trying to click while the windows is minimized so Idk why its not working here is my code after replacing it with get_running_processes:

from ahk import AHK
import ahk
import customtkinter
from customtkinter import END
import threading
import psutil
from CTkListbox import *

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()
        self.ahk = AHK()

        self.process_listbox = CTkListbox(self, height=30, width=200)
        self.process_listbox.grid(row=0, column=0, padx=10, pady=10)

        self.update_process_list()

        self.select_button = customtkinter.CTkButton(
            self, text="Select", command=self.select_process
        )
        self.select_button.grid(row=1, column=0, pady=10)

    def get_available_windows(self):
        available_windows = []
        for win in self.ahk.list_windows():
            selection_name = f"{win.id} {win.process_name} ({win.pid})"
            available_windows.append((selection_name, win))
        return available_windows

    def update_process_list(self):
        # Update the list of running processes in the Listbox
        running_processes = self.get_available_windows()
        self.process_listbox.delete(0, END)
        for pid_name, _ in running_processes:
            self.process_listbox.insert(END, pid_name)

    def select_process(self):
        # Get the selected process from the Listbox
        self.selected_index = self.process_listbox.curselection()
        if self.selected_index:
            self.selected_pid = self.get_available_windows()[self.selected_index][1]
            print(f"Selected PID: {self.selected_pid}")

            self.win = ahk.Window.from_pid(self.ahk, pid=self.selected_pid)
            if self.win is not None:
                self.getmousepos = threading.Thread(target=self.mousepos)
                self.getmousepos.start()
            else:
                print("No window found for the selected PID")

        else:
            print("No process selected")

    def mousepos(self):
        self.win.click(100, 138)

    def start(self):
        self.mainloop()

if __name__ == "__main__":
    app = App()
    app.update()
    app.start()
spyoungtech commented 7 months ago

Here's what I believe to be the fixed version of your code:

from ahk import AHK
import ahk
import customtkinter
from customtkinter import END
import threading
from CTkListbox import *

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()
        self.ahk = AHK()

        self.process_listbox = CTkListbox(self, height=30, width=200)
        self.process_listbox.grid(row=0, column=0, padx=10, pady=10)

        self._available_windows = []
        self.update_process_list()

        self.select_button = customtkinter.CTkButton(
            self, text="Select", command=self.select_process
        )
        self.select_button.grid(row=1, column=0, pady=10)

    def get_available_windows(self):
        available_windows = []
        for win in self.ahk.list_windows():
            selection_name = f"{win.id} {win.process_name} ({win.pid})"
            available_windows.append((selection_name, win))
        return available_windows

    def update_process_list(self):
        # Update the list of running processes in the Listbox
        available_windows = self.get_available_windows()
        self.process_listbox.delete(0, END)
        for selection_name, _ in available_windows:
            self.process_listbox.insert(END, selection_name)
        self._available_windows = available_windows
        return available_windows

    def select_process(self):
        # Get the selected process from the Listbox
        self.selected_index = self.process_listbox.curselection()
        if self.selected_index:
            self.win = self._available_windows[self.selected_index][1]
            if self.win.exists():
                print('Selected window process is', self.win.get_process_name())
                self.getmousepos = threading.Thread(target=self.mousepos)
                self.getmousepos.start()
            else:
                print(f"selected window {self.win.id} no longer exists")

        else:
            print("No process selected")

    def mousepos(self):
        print('mousepos running')
        self.win.click(100, 138)

    def start(self):
        self.mainloop()

if __name__ == "__main__":
    app = App()
    app.update()
    app.start()

Keep in mind, some programs may not accept input while minimized. But I can confirm this works with notepad.exe on my system.

zSynctic commented 7 months ago

Thanks this worked