Загрузка данных
import tkinter as tk
from tkinter import colorchooser
import ctypes
import threading
import time
import win32api
import win32gui
import win32con
import keyboard
from pynput import mouse
# Включаем DPI Awareness, чтобы координаты мыши (физические пиксели)
# точно совпадали с координатами оконного менеджера (логические пиксели)
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
ctypes.windll.user32.SetProcessDPIAware()
class GameOverlayApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("Оверлей")
self.root.geometry("320x180")
self.root.attributes("-topmost", True)
self.root.resizable(False, False)
# Состояния: IDLE (ожидание), LISTENING (ждем клики), DRAWN (линия нарисована)
self.state = 'IDLE'
self.points = []
self.hotkey = 'f4'
# Настройки линии
self.line_thickness = tk.IntVar(value=3)
self.line_color = '#ff0000'
self.line_id = None
self.mouse_listener = None
self.setup_ui()
self.setup_overlay()
# Регистрация глобального хоткея
keyboard.add_hotkey(self.hotkey, self.on_hotkey_pressed)
def setup_ui(self):
"""Интерфейс настроек."""
frame = tk.Frame(self.root, padx=10, pady=10)
frame.pack(fill=tk.BOTH, expand=True)
# Толщина
tk.Label(frame, text="Толщина линии:").grid(row=0, column=0, sticky="w", pady=5)
tk.Spinbox(frame, from_=1, to=20, textvariable=self.line_thickness, width=5).grid(row=0, column=1, sticky="w", pady=5)
# Цвет
tk.Label(frame, text="Цвет линии:").grid(row=1, column=0, sticky="w", pady=5)
self.btn_color = tk.Button(frame, bg=self.line_color, width=5, command=self.choose_color)
self.btn_color.grid(row=1, column=1, sticky="w", pady=5)
# Хоткей
tk.Label(frame, text="Горячая клавиша:").grid(row=2, column=0, sticky="w", pady=5)
self.btn_rebind = tk.Button(frame, text=f"Изменить ({self.hotkey})", command=self.rebind_hotkey)
self.btn_rebind.grid(row=2, column=1, sticky="ew", pady=5)
# Статус
self.lbl_status = tk.Label(frame, text="Статус: Ожидание хоткея", fg="black", font=("Arial", 9, "bold"))
self.lbl_status.grid(row=3, column=0, columnspan=2, pady=10)
def setup_overlay(self):
"""Создание невидимого кликабельного (click-through) оверлея поверх всех экранов."""
self.overlay = tk.Toplevel(self.root)
self.overlay.overrideredirect(True)
self.overlay.attributes("-topmost", True)
# Задаем хромакей-цвет для прозрачности (используем мадженту, чтобы не конфликтовать с черными линиями)
trans_color = '#ff00ff'
self.overlay.config(bg=trans_color)
self.overlay.attributes("-transparentcolor", trans_color)
# Захватываем весь виртуальный экран (для поддержки нескольких мониторов)
self.v_width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
self.v_height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
self.v_x = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
self.v_y = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
self.overlay.geometry(f"{self.v_width}x{self.v_height}+{self.v_x}+{self.v_y}")
self.canvas = tk.Canvas(self.overlay, width=self.v_width, height=self.v_height,
bg=trans_color, highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
# === МАГИЯ WIN32 API ===
# Делаем окно полностью проницаемым для мыши (Click-Through)
self.overlay.update()
hwnd = self.overlay.winfo_id()
ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
ex_style | win32con.WS_EX_LAYERED | win32con.WS_EX_TRANSPARENT)
def choose_color(self):
color = colorchooser.askcolor(initialcolor=self.line_color, title="Выберите цвет")
if color[1]:
self.line_color = color[1]
self.btn_color.config(bg=self.line_color)
def rebind_hotkey(self):
"""Интерактивное переназначение клавиши без блокировки GUI."""
self.btn_rebind.config(text="Нажмите клавишу...", state=tk.DISABLED)
threading.Thread(target=self._wait_for_new_hotkey, daemon=True).start()
def _wait_for_new_hotkey(self):
keyboard.unhook_all_hotkeys()
new_key = keyboard.read_key()
self.hotkey = new_key
# Небольшой слип, чтобы отжатие клавиши не вызвало мгновенный триггер
time.sleep(0.2)
keyboard.add_hotkey(self.hotkey, self.on_hotkey_pressed)
# Возвращаем выполнение в основной поток tkinter
self.root.after(0, self._finish_rebind, new_key)
def _finish_rebind(self, new_key):
self.btn_rebind.config(text=f"Изменить ({new_key})", state=tk.NORMAL)
def on_hotkey_pressed(self):
"""Обработчик хоткея. Вызывается из потока keyboard, поэтому делегируем в main loop."""
self.root.after(0, self.handle_state_machine)
def handle_state_machine(self):
"""Логика переключения состояний по хоткею."""
if self.state == 'IDLE':
self.start_listening()
elif self.state == 'LISTENING':
# Отмена выбора, если передумали
self.stop_listening()
elif self.state == 'DRAWN':
# Стираем линию при повторном нажатии
self.clear_line()
def start_listening(self):
self.state = 'LISTENING'
self.points = []
self.lbl_status.config(text="Статус: Ожидание 2-х кликов ЛКМ...", fg="blue")
# Запускаем пассивный хук мыши (не блокирует оригинальные клики)
self.mouse_listener = mouse.Listener(on_click=self.on_mouse_click)
self.mouse_listener.start()
def stop_listening(self):
self.state = 'IDLE'
if self.mouse_listener:
self.mouse_listener.stop()
self.mouse_listener = None
self.lbl_status.config(text="Статус: Ожидание хоткея", fg="black")
def on_mouse_click(self, x, y, button, pressed):
"""Коллбэк хука мыши. Запускается в фоновом потоке pynput."""
if button == mouse.Button.left and pressed:
self.points.append((x, y))
if len(self.points) == 2:
# Отрисовка в основном потоке GUI
self.root.after(0, self.draw_line)
return False # Остановка хука pynput
def draw_line(self):
self.state = 'DRAWN'
if self.line_id:
self.canvas.delete(self.line_id)
p1, p2 = self.points
# Корректируем глобальные координаты под локальные координаты Canvas
# (Особенно важно для мультимониторных систем с отрицательными координатами)
c_x1, c_y1 = p1[0] - self.v_x, p1[1] - self.v_y
c_x2, c_y2 = p2[0] - self.v_x, p2[1] - self.v_y
self.line_id = self.canvas.create_line(
c_x1, c_y1, c_x2, c_y2,
fill=self.line_color,
width=self.line_thickness.get(),
capstyle=tk.ROUND
)
self.lbl_status.config(text="Статус: Линия нарисована", fg="green")
if self.mouse_listener:
self.mouse_listener.stop()
self.mouse_listener = None
def clear_line(self):
self.state = 'IDLE'
if self.line_id:
self.canvas.delete(self.line_id)
self.line_id = None
self.lbl_status.config(text="Статус: Ожидание хоткея", fg="black")
def run(self):
self.root.mainloop()
if __name__ == "__main__":
app = GameOverlayApp()
app.run()