【Win / Mac】Pythonで開いているアプリとフォーカスされているアプリのパスを取得する

# 目標

```python
# フォーカスされているアプリを取得
get_active_app_path()  # "アプリケーションのパス"

# 開いている全てのアプリを取得
get_open_apps()  # {"アプリケーション名": "アプリケーションのパス", ...}
```

# 環境構築

```sh:Windows
pip install pywin32 psutil
```

```sh:Mac
pip install pyobjc
```

まとめてインストールするにはrequirements.txtを作成します。

```sh:requirements.txt
pyobjc; sys_platform == 'darwin'
pywin32; sys_platform == 'win32'
psutil; sys_platform == 'win32'
```
```sh
pip install -r requirements.txt
```

# コード

### フォーカスされているアプリ

```python
import sys

if sys.platform == "win32":
    import win32gui
    import win32process
    from psutil import Process, NoSuchProcess

    def get_active_app_path() -> str | None:
        hwnd = win32gui.GetForegroundWindow()
        _, pid = win32process.GetWindowThreadProcessId(hwnd)
        if pid <= 0:
            return None
        try:
            path = Process(pid).exe()
        except NoSuchProcess:
            return None
        return path

elif sys.platform == "darwin":
    from AppKit import NSWorkspace

    def get_active_app_path() -> str:
        apps = NSWorkspace.sharedWorkspace().activeApplication()
        path = apps["NSApplicationPath"]
        return path

```

### 開いているアプリ一覧

```python
import sys

if sys.platform == "darwin":
    from AppKit import NSWorkspace

    def get_open_apps() -> dict[str, str]:
        apps = NSWorkspace.sharedWorkspace().launchedApplications()
        data = {app["NSApplicationName"]: app["NSApplicationPath"] for app in apps} # 因みに、app["NSApplicationProcessIdentifier"] でプロセスIDを取得できる
        return data

elif sys.platform == "win32":
    import win32gui
    import win32process
    import win32con
    import ctypes
    from ctypes.wintypes import DWORD, RECT
    import pythoncom
    from win32com.client import GetObject
    from psutil import Process

    class Win32Window:
        def __init__(self, hWnd: int):
            self._hWnd = hWnd
        @property
        def title(self) -> str:
            name = win32gui.GetWindowText(self._hWnd)
            if isinstance(name, bytes):
                name = name.decode()
            return name or ""
        @property
        def path(self) -> str:
            _, pid = win32process.GetWindowThreadProcessId(self._hWnd)
            return Process(pid).exe()
        def getHandle(self) -> int:
            return self._hWnd
    def _find_window_handles(parent: int | None = None, window_class: str | None = None, title: str | None = None, onlyVisible: bool = True) -> list[int]:
        handle_list: list[int] = []
        def findit(hwnd: int, _) -> bool:
            if window_class and window_class != win32gui.GetClassName(hwnd):
                return True
            if title and title != win32gui.GetWindowText(hwnd):
                return True
            if not onlyVisible or (onlyVisible and win32gui.IsWindowVisible(hwnd)):
                handle_list.append(hwnd)
            return True
        if not parent:
            parent = win32gui.GetDesktopWindow()
        win32gui.EnumChildWindows(parent, findit, None)
        return handle_list
    def _findMainWindowHandles() -> list[tuple[int, int]]:
        class TITLEBARINFO(ctypes.Structure):
            _fields_ = [
                ("cbSize", DWORD),
                ("rcTitleBar", RECT),
                ("rgstate", DWORD * 6)
            ]
        def win_enum_handler(hwnd: int, ctx: any):
            if not win32gui.IsWindowVisible(hwnd):
                return
            title_info = TITLEBARINFO()
            title_info.cbSize = ctypes.sizeof(title_info)
            ctypes.windll.user32.GetTitleBarInfo(hwnd, ctypes.byref(title_info))
            isCloaked = ctypes.c_int(0)
            DWMWA_CLOAKED = 14
            ctypes.windll.dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, ctypes.byref(isCloaked), ctypes.sizeof(isCloaked))
            title = win32gui.GetWindowText(hwnd)
            if win32gui.IsWindowVisible(hwnd) and title != "" and isCloaked.value == 0:
                if not (title_info.rgstate[0] & win32con.STATE_SYSTEM_INVISIBLE):
                    handle_list.append((hwnd, win32process.GetWindowThreadProcessId(hwnd)[1]))
        handle_list: list[tuple[int, int]] = []
        win32gui.EnumWindows(win_enum_handler, None)
        return handle_list
    def _get_all_apps():
        pythoncom.CoInitialize() # メインスレッド外で実行する場合のエラー回避
        WMI = GetObject("winmgmts:")
        mainWindows = [w[1] for w in _findMainWindowHandles()]
        return [(p.Properties_("ProcessID").Value, p.Properties_("Name").Value) for p in WMI.InstancesOf("Win32_Process") if p.Properties_("ProcessID").Value in mainWindows]
    def __remove_bad_windows(windows: list[int] | None) -> list[Win32Window]:
        outList = []
        if windows is not None:
            for window in windows:
                try:
                    outList.append(Win32Window(window))
                except:
                    pass
        return outList

    def get_open_apps() -> dict[str, str]:
        process_list = _get_all_apps()
        result: dict[str, str] = {}
        for win in __remove_bad_windows(_find_window_handles()):
            pID = win32process.GetWindowThreadProcessId(win.getHandle())
            for item in process_list:
                appPID = item[0]
                appName = str(item[1])
                if appPID == pID[1]:
                    if not appName in result:
                        result[appName] = win.path # 因みに、win.title でウィンドウのタイトルを取得できる
                    break
        return result
```

# 解説

割愛（リクエストがあれば書きます）

# 謝辞

上記のコードの中には私が書いたものだけではなく、ネットを彷徨って見つけたコードのコピペも紛れています。それらのページを見つけることができなかったので出典がありません。原作者の方ごめんなさい。
もしコピペ元だと思われるページを見つけた方は教えていただけるとありがたいです。