Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
253 lines
10 KiB
Python
253 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Иконка в трее Windows: Shell_NotifyIconW без сторонних библиотек.
|
|
|
|
Скрытое окно с собственным message loop живёт в фоновом потоке;
|
|
колбэки вызываются в этом потоке — UI передаёт сюда только
|
|
потокобезопасные функции (положить сообщение в очередь Tk).
|
|
"""
|
|
|
|
import ctypes
|
|
import threading
|
|
from ctypes import wintypes
|
|
|
|
user32 = ctypes.windll.user32
|
|
shell32 = ctypes.windll.shell32
|
|
kernel32 = ctypes.windll.kernel32
|
|
|
|
WM_APP = 0x8000
|
|
WM_TRAY = WM_APP + 1 # колбэк иконки
|
|
WM_TRAY_ACT = WM_APP + 2 # ЛКМ/даблклик → показать окно
|
|
WM_TRAY_CMD = WM_APP + 3 # пункт меню (wParam = индекс 1..n)
|
|
|
|
NIM_ADD, NIM_MODIFY, NIM_DELETE = 0, 1, 2
|
|
NIF_MESSAGE, NIF_ICON, NIF_TIP, NIF_INFO = 0x1, 0x2, 0x4, 0x10
|
|
NIIF_INFO, NIIF_WARNING, NIIF_ERROR = 1, 2, 3
|
|
WM_LBUTTONUP, WM_LBUTTONDBLCLK, WM_RBUTTONUP = 0x0202, 0x0203, 0x0205
|
|
MF_STRING, MF_SEPARATOR = 0x0, 0x800
|
|
TPM_RIGHTBUTTON, TPM_RETURNCMD = 0x2, 0x100
|
|
IDI_APPLICATION = 32512
|
|
GCLP_HICON = -14
|
|
HWND_MESSAGE = wintypes.HWND(-3)
|
|
WM_NULL = 0
|
|
WM_CLOSE = 0x10
|
|
WM_DESTROY = 0x02
|
|
|
|
WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_ssize_t, wintypes.HWND, ctypes.c_uint,
|
|
wintypes.WPARAM, wintypes.LPARAM)
|
|
|
|
# сигнатуры для x64 (иначе хэндлы/указатели обрезаются до 32 бит)
|
|
user32.DefWindowProcW.restype = ctypes.c_ssize_t
|
|
user32.DefWindowProcW.argtypes = [wintypes.HWND, ctypes.c_uint,
|
|
wintypes.WPARAM, wintypes.LPARAM]
|
|
user32.CreatePopupMenu.restype = wintypes.HMENU
|
|
user32.AppendMenuW.argtypes = [wintypes.HMENU, ctypes.c_uint, wintypes.UINT,
|
|
wintypes.LPCWSTR]
|
|
user32.TrackPopupMenu.restype = ctypes.c_int
|
|
user32.TrackPopupMenu.argtypes = [wintypes.HMENU, ctypes.c_uint,
|
|
ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
wintypes.HWND, ctypes.c_void_p]
|
|
user32.DestroyMenu.argtypes = [wintypes.HMENU]
|
|
user32.GetClassLongPtrW.restype = ctypes.c_size_t
|
|
user32.GetClassLongPtrW.argtypes = [wintypes.HWND, ctypes.c_int]
|
|
user32.LoadIconW.restype = wintypes.HICON
|
|
user32.LoadIconW.argtypes = [wintypes.HINSTANCE, ctypes.c_void_p]
|
|
kernel32.GetModuleHandleW.restype = wintypes.HINSTANCE
|
|
kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
|
|
shell32.Shell_NotifyIconW.restype = wintypes.BOOL
|
|
shell32.Shell_NotifyIconW.argtypes = [ctypes.c_uint, ctypes.c_void_p]
|
|
user32.PeekMessageW.argtypes = [ctypes.POINTER(wintypes.MSG), wintypes.HWND,
|
|
ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
|
|
user32.CreateWindowExW.argtypes = [
|
|
ctypes.c_uint, wintypes.LPCWSTR, wintypes.LPCWSTR, ctypes.c_uint,
|
|
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, ctypes.c_void_p]
|
|
user32.CreateWindowExW.restype = wintypes.HWND
|
|
user32.GetCursorPos.argtypes = [ctypes.POINTER(wintypes.POINT)]
|
|
user32.PostMessageW.argtypes = [wintypes.HWND, ctypes.c_uint,
|
|
wintypes.WPARAM, wintypes.LPARAM]
|
|
|
|
|
|
class GUID(ctypes.Structure):
|
|
_fields_ = [("Data1", ctypes.c_uint32), ("Data2", ctypes.c_uint16),
|
|
("Data3", ctypes.c_uint16), ("Data4", ctypes.c_ubyte * 8)]
|
|
|
|
|
|
class NIDW(ctypes.Structure):
|
|
_fields_ = [
|
|
("cbSize", wintypes.DWORD),
|
|
("hWnd", wintypes.HWND),
|
|
("uID", wintypes.UINT),
|
|
("uFlags", wintypes.UINT),
|
|
("uCallbackMessage", wintypes.UINT),
|
|
("hIcon", wintypes.HICON),
|
|
("szTip", wintypes.WCHAR * 128),
|
|
("dwState", wintypes.DWORD),
|
|
("dwStateMask", wintypes.DWORD),
|
|
("szInfo", wintypes.WCHAR * 256),
|
|
("uTimeout", wintypes.UINT),
|
|
("szInfoTitle", wintypes.WCHAR * 64),
|
|
("dwInfoFlags", wintypes.DWORD),
|
|
("guidItem", GUID),
|
|
("hBalloonIcon", wintypes.HICON),
|
|
]
|
|
|
|
|
|
class WNDCLASSW(ctypes.Structure):
|
|
_fields_ = [
|
|
("style", ctypes.c_uint),
|
|
("lpfnWndProc", WNDPROC),
|
|
("cbClsExtra", ctypes.c_int),
|
|
("cbWndExtra", ctypes.c_int),
|
|
("hInstance", wintypes.HINSTANCE),
|
|
("hIcon", wintypes.HICON),
|
|
("hCursor", wintypes.HCURSOR),
|
|
("hbrBackground", wintypes.HBRUSH),
|
|
("lpszMenuName", wintypes.LPCWSTR),
|
|
("lpszClassName", wintypes.LPCWSTR),
|
|
]
|
|
|
|
|
|
class Tray:
|
|
"""Трей: start() → иконка, notify() → плашка, stop() → убрать."""
|
|
|
|
def __init__(self, on_activate=None, on_command=None, commands=()):
|
|
self._on_activate = on_activate or (lambda: None)
|
|
self._on_command = on_command or (lambda name: None)
|
|
self._commands = list(commands)
|
|
self._hwnd = None
|
|
self._thread = None
|
|
self._ready = threading.Event()
|
|
self._stop = threading.Event()
|
|
self._hicon = None
|
|
self._wndproc = WNDPROC(self._wnd_proc) # держим живым
|
|
|
|
# ------------------------------------------------------------- поток
|
|
def start(self, tooltip="", hicon=None):
|
|
self._hicon = hicon or user32.LoadIconW(None, IDI_APPLICATION)
|
|
self._tooltip = tooltip or "PixelLauncher"
|
|
self._thread = threading.Thread(target=self._loop, daemon=True,
|
|
name="tray")
|
|
self._thread.start()
|
|
self._ready.wait(5)
|
|
|
|
def _loop(self):
|
|
cls = "PixelLauncherTrayClass"
|
|
wc = WNDCLASSW()
|
|
wc.lpfnWndProc = self._wndproc
|
|
wc.hInstance = kernel32.GetModuleHandleW(None)
|
|
wc.lpszClassName = cls
|
|
user32.RegisterClassW(ctypes.byref(wc))
|
|
hwnd = user32.CreateWindowExW(
|
|
0, cls, "PixelLauncher", 0, 0, 0, 0, 0,
|
|
HWND_MESSAGE, None, wc.hInstance, None)
|
|
if not hwnd:
|
|
self._ready.set()
|
|
return
|
|
self._hwnd = hwnd
|
|
self._shell(NIM_ADD, NIF_MESSAGE | NIF_ICON | NIF_TIP,
|
|
callback=WM_TRAY, tip=self._tooltip, icon=self._hicon)
|
|
self._ready.set()
|
|
msg = wintypes.MSG()
|
|
while not self._stop.is_set():
|
|
# PeekMessage, чтобы периодически проверять флаг остановки
|
|
while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1):
|
|
user32.TranslateMessage(ctypes.byref(msg))
|
|
user32.DispatchMessageW(ctypes.byref(msg))
|
|
if msg.message == WM_DESTROY:
|
|
return
|
|
if not self._stop.is_set():
|
|
# усыпляемся до следующего сообщения
|
|
user32.WaitMessage()
|
|
user32.DestroyWindow(hwnd)
|
|
|
|
# ----------------------------------------------------------- колбэки
|
|
def _wnd_proc(self, hwnd, msg, wparam, lparam):
|
|
if msg == WM_TRAY:
|
|
if lparam in (WM_LBUTTONUP, WM_LBUTTONDBLCLK):
|
|
self._on_activate()
|
|
elif lparam == WM_RBUTTONUP:
|
|
self._popup(hwnd)
|
|
return 0
|
|
if msg == WM_TRAY_ACT:
|
|
self._on_activate()
|
|
return 0
|
|
if msg == WM_TRAY_CMD:
|
|
if int(wparam) == 100:
|
|
self._on_command("exit")
|
|
else:
|
|
idx = int(wparam) - 2
|
|
if 0 <= idx < len(self._commands):
|
|
self._on_command(self._commands[idx][1])
|
|
return 0
|
|
if msg == WM_DESTROY:
|
|
user32.PostQuitMessage(0)
|
|
return 0
|
|
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
|
|
def _popup(self, hwnd):
|
|
hm = user32.CreatePopupMenu()
|
|
user32.AppendMenuW(hm, MF_STRING, 1, "Показать лаунчер")
|
|
user32.AppendMenuW(hm, MF_SEPARATOR, 0, None)
|
|
for i, (label, _name) in enumerate(self._commands):
|
|
user32.AppendMenuW(hm, MF_STRING, i + 2, label)
|
|
user32.AppendMenuW(hm, MF_SEPARATOR, 0, None)
|
|
user32.AppendMenuW(hm, MF_STRING, 100, "Выход")
|
|
user32.SetForegroundWindow(hwnd)
|
|
pt = wintypes.POINT()
|
|
user32.GetCursorPos(ctypes.byref(pt))
|
|
cmd = user32.TrackPopupMenu(hm, TPM_RIGHTBUTTON | TPM_RETURNCMD,
|
|
pt.x, pt.y, 0, hwnd, None)
|
|
user32.PostMessageW(hwnd, WM_NULL, 0, 0)
|
|
user32.DestroyMenu(hm)
|
|
if cmd == 1:
|
|
user32.PostMessageW(hwnd, WM_TRAY_ACT, 0, 0)
|
|
elif cmd == 100:
|
|
user32.PostMessageW(hwnd, WM_TRAY_CMD, 100, 0)
|
|
elif cmd:
|
|
user32.PostMessageW(hwnd, WM_TRAY_CMD, cmd, 0)
|
|
|
|
# -------------------------------------------------------------- API
|
|
def _shell(self, action, flags, callback=0, tip="", icon=None,
|
|
info="", title="", niif=NIIF_INFO):
|
|
if not self._hwnd:
|
|
return
|
|
nid = NIDW()
|
|
nid.cbSize = ctypes.sizeof(NIDW)
|
|
nid.hWnd = self._hwnd
|
|
nid.uID = 1
|
|
nid.uFlags = flags
|
|
nid.uCallbackMessage = callback
|
|
nid.hIcon = icon or self._hicon
|
|
nid.szTip = (tip or "")[:127]
|
|
nid.szInfo = (info or "")[:255]
|
|
nid.szInfoTitle = (title or "")[:63]
|
|
nid.dwInfoFlags = niif
|
|
shell32.Shell_NotifyIconW(action, ctypes.byref(nid))
|
|
|
|
def notify(self, title, text, warn=False):
|
|
"""Плашка в трее (из любого потока)."""
|
|
self._shell(NIM_MODIFY, NIF_INFO, title=str(title)[:63],
|
|
info=str(text)[:255],
|
|
niif=NIIF_WARNING if warn else NIIF_INFO)
|
|
|
|
def update_tooltip(self, text):
|
|
self._shell(NIM_MODIFY, NIF_TIP, tip=str(text)[:127])
|
|
|
|
def stop(self):
|
|
"""Убирает иконку (можно из любого потока)."""
|
|
if self._stop.is_set():
|
|
return
|
|
self._stop.set()
|
|
if self._hwnd:
|
|
self._shell(NIM_DELETE, 0)
|
|
user32.PostMessageW(self._hwnd, WM_CLOSE, 0, 0)
|
|
|
|
|
|
def window_icon(hwnd):
|
|
"""HICON окна Tk для иконки трея (None — если не вышло)."""
|
|
try:
|
|
h = user32.GetClassLongPtrW(hwnd, GCLP_HICON)
|
|
return h or None
|
|
except Exception: # noqa: BLE001
|
|
return None
|