Files
kosta2014p ec6f13ddcc PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
2026-10-18 22:44:19 +07:00

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