import pystray
from PIL import Image, ImageDraw, ImageFont
import threading
import time
import ctypes
from ctypes import wintypes, Structure, byref, POINTER
# --- Definiciones de ctypes para la API de Windows ---
class DISPLAY_DEVICE(Structure):
_fields_ = [
("cb", wintypes.DWORD), ("DeviceName", wintypes.WCHAR * 32), ("DeviceString", wintypes.WCHAR * 128), ("StateFlags", wintypes.DWORD), ("DeviceID", wintypes.WCHAR * 128), ("DeviceKey", wintypes.WCHAR * 128), ]
# Flags para StateFlags
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001 # El dispositivo es parte del escritorio
DISPLAY_DEVICE_PRIMARY_DEVICE = 0x00000004 # El dispositivo es la pantalla principal
class MonitorDetector:
def __init__(self):
self.user32 = ctypes.windll.user32
self.last_mode = None
def get_physical_monitor_count(self):
"""
Cuenta el número de monitores físicos conectados y activos.
Esto intenta ir más allá de la enumeración lógica del escritorio.
"""
physical_monitors = set() # Usamos un conjunto para almacenar IDs de dispositivos únicos
i = 0
while True:
display_adapter = DISPLAY_DEVICE()
display_adapter.cb = ctypes.sizeof(DISPLAY_DEVICE)
# Enumerar adaptadores de pantalla
# El primer parámetro 'None' indica que estamos enumerando adaptadores.
if not self.user32.EnumDisplayDevicesW(None, i, byref(display_adapter), 0):
break # No hay más adaptadores
# Verificar si el adaptador está activo (conectado al escritorio)
if display_adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
j = 0
while True:
display_monitor = DISPLAY_DEVICE()
display_monitor.cb = ctypes.sizeof(DISPLAY_DEVICE)
# Enumerar monitores conectados a este adaptador específico
# El primer parámetro es el nombre del adaptador.
if not self.user32.EnumDisplayDevicesW(display_adapter.DeviceName, j, byref(display_monitor), 0):
break # No hay más monitores en este adaptador
# Verificar si el monitor está activo y es parte del escritorio
if display_monitor.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
# Usamos DeviceID para identificar de forma única los monitores físicos.
# DeviceID suele ser único para el hardware físico.
physical_monitors.add(display_monitor.DeviceID)
j += 1
i += 1
return len(physical_monitors)
def get_current_mode(self):
"""
Obtiene el modo actual de la pantalla (individual, extendido, clonado)
usando el conteo de monitores físicos y las métricas de pantalla virtual.
"""
try:
physical_monitor_count = self.get_physical_monitor_count()
# Obtener métricas del sistema
virtual_width = self.user32.GetSystemMetrics(78) # SM_CXVIRTUALSCREEN (Ancho de la pantalla virtual)
primary_width = self.user32.GetSystemMetrics(0) # SM_CXSCREEN (Ancho de la pantalla principal)
# Lógica para determinar el modo de pantalla
if physical_monitor_count == 1:
# Si solo se detecta un monitor físico
return 1 # Monitor único
elif physical_monitor_count > 1:
# Si hay múltiples monitores físicos detectados
if virtual_width > primary_width:
# Y el ancho virtual es mayor que el primario, es modo extendido.
return 12 # Modo extendido
else:
# Si el ancho virtual es igual al primario, pero hay múltiples monitores físicos,
# significa que comparten el mismo espacio lógico, lo que indica el modo clonado.
return 11 # Modo clonado
else:
# Caso de respaldo para escenarios inesperados (ej. 0 monitores detectados)
print(f"DEBUG: Modo desconocido. Monitores físicos detectados: {physical_monitor_count}") return self.last_mode if self.last_mode else 1 # Retorna el último modo conocido o 1 en caso de error
except Exception as e:
print(f"Error detectando el modo: {str(e)}") return self.last_mode if self.last_mode else 1 # En caso de error, retorna el último modo o 1
def create_text_icon(display_mode):
"""Crea un ícono con el número correspondiente al modo de pantalla"""
icon_size = 150
img = Image.new('RGBA', (icon_size, icon_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img)
# Asignar colores según el modo
if display_mode == 1:
circle_color = 'dodgerblue' # Azul para un solo monitor
text = "1"
elif display_mode == 11:
circle_color = 'orange' # Naranja para clonado
text = "11"
else: # display_mode == 12
circle_color = 'green' # Verde para extendido
text = "12"
# Dibujar círculo de fondo
draw.ellipse([(5, 5), (icon_size-5, icon_size-5)], fill=circle_color)
# Configurar texto
try:
font = ImageFont.truetype("arial.ttf", size=int(icon_size*0.7)) except:
font = ImageFont.load_default()
# Posicionar texto centrado
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
position = (
(icon_size - text_width) / 2,
(icon_size - text_height) / 2 # - icon_size*0.05
)
draw.text(position, text, fill="white", font=font)
return img
class TrayApp:
def __init__(self):
self.detector = MonitorDetector()
self.icon = None
self.running = True
def update_icon(self, icon=None):
"""Actualiza el ícono con el modo actual y el título."""
current_mode = self.detector.get_current_mode()
mode_text = {1: "Solo", 11: "Clonado", 12: "Extendido"}.get(current_mode, "Desconocido")
if self.icon:
# Solo actualiza si el icono ya ha sido inicializado por pystray
self.icon.icon = create_text_icon(current_mode)
self.icon.title = f"Modo: {mode_text}"
self.detector.last_mode = current_mode
def run(self):
"""Inicia la aplicación en la bandeja del sistema."""
menu = pystray.Menu(
pystray.MenuItem("Actualizar ahora", self.update_icon), pystray.MenuItem("Salir", self.stop), pystray.MenuItem("Créditos:", lambda: None), pystray.MenuItem("Manuel Cortes Isario", lambda: None) )
# Inicializa el icono con el modo actual antes de ejecutar.
initial_mode = self.detector.get_current_mode()
initial_icon_image = create_text_icon(initial_mode)
initial_mode_text = {1: "Solo", 11: "Clonado", 12: "Extendido"}.get(initial_mode, "Desconocido")
self.icon = pystray.Icon(
"monitor_tray",
icon=initial_icon_image,
title=f"Modo: {initial_mode_text}", menu=menu
)
# Inicia el hilo de actualización en segundo plano
threading.Thread(target=self.monitor_thread, daemon=True).start()
# La función setup se llama una vez que el icono está visible.
self.icon.run(self.setup)
def setup(self, icon):
"""Se ejecuta cuando el icono de la bandeja del sistema está visible."""
icon.visible = True
self.update_icon(icon) # Actualiza el icono una vez que está configurado
def monitor_thread(self):
"""Hilo para la verificación periódica del modo de pantalla."""
while self.running:
self.update_icon()
time.sleep(60) # Espera 60 segundos antes de la próxima verificación
def stop(self, icon=None, item=None):
"""Detiene la aplicación de la bandeja del sistema."""
self.running = False
if self.icon:
self.icon.stop()
if __name__ == "__main__":
app = TrayApp()
app.run()
Comentarios sobre la versión: 1.0 (0)
No hay comentarios