Skip to content
Back to skills

Pyside6 Overlay Window

ASecurity

Build Windows desktop overlay widgets with PySide6 — transparent frameless always-on-top windows with alpha masks, click-through on transparent regions, drag/throw physics, multi-monitor support, syst

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
ai-agentspythongoreact

Works with

  • cli

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 6, 2026

npx -y skills add Kairos-ai-agent/kairos-code --skill pyside6-overlay-window --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pyside6 Overlay Window?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pyside6 Overlay Window
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kairos-ai-agent-pyside6-overlay-window/badge)](https://www.skillsdirectory.com/skills/kairos-ai-agent-pyside6-overlay-window)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: "pyside6-overlay-window"
description: "Build Windows desktop overlay widgets with PySide6 — transparent frameless always-on-top windows with alpha masks, click-through on transparent regions, drag/throw physics, multi-monitor support, syst"
priority: 0.5
imported-from: "hermes"
source-path: "hermes/skills/.archive/pyside6-overlay-window/SKILL.md"
---
# PySide6 Overlay Window

Class: building **Windows desktop overlay widgets** with PySide6 — transparent, frameless, always-on-top, with click-through on transparent regions, drag/throw physics, and optional autonomous behaviors.

Covers the technique core that all these widgets share: a pet, a sticky note, a floating clock, a Pomodoro timer, a HUD, a screen watermark — they all need the same "transparent frameless always-on-top window with alpha mask" foundation.

---

## When to load this skill

Trigger when the user asks for:
- 桌面宠物 / desktop pet / mascot / desktop companion
- 悬浮便签 / sticky note / floating note
- 桌面时钟 / floating clock / 番茄钟 / Pomodoro overlay
- 屏幕水印 / screen watermark / HUD / overlay HUD
- 截图工具浮窗 / screenshot widget / quick-capture
- 屏幕录制器浮窗 / screen recorder widget
- 任何"始终在最上层、可拖动、半透明"的桌面 widget

Don't load this for: full-screen GUI apps (regular `QMainWindow` is enough), web-based desktop apps (use `pyinstaller-desktop-packaging`), tray-only utilities without visible windows.

---

## Core technique: transparent window with mask

**The right way:**

```python
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QBitmap, QImage, QPainter
from PySide6.QtWidgets import QWidget

class Overlay(QWidget):
    def __init__(self):
        super().__init__()
        # CRITICAL: do NOT use Qt.WindowTransparentForInput — it defeats setMask
        self.setWindowFlags(
            Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
        )
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setAttribute(Qt.WA_ShowWithoutActivating)

        self._pix = QPixmap("pet.png")
        self.resize(self._pix.size())
        self._build_mask()

    def _build_mask(self):
        # Use alpha channel — only opaque pixels are clickable
        img = self._pix.toImage().convertToFormat(QImage.Format_Alpha8)
        self.setMask(QBitmap.fromImage(img))

    def paintEvent(self, ev):
        p = QPainter(self)
        p.drawPixmap(self.rect(), self._pix)
```

**Critical pitfalls:**

1. **`Qt.WindowTransparentForInput` defeats `setMask()`** — the flag tells Qt "ignore ALL mouse events on this widget", which is stronger than what mask does. Don't combine them. Use mask alone for click-through on transparent pixels.

2. **`setWindowFlag(...)` AFTER `show()` silently hides the widget.** Qt will re-show it, but the user sees a flicker. Always set all flags in `__init__` and never modify them later. If you need to toggle something at runtime, do it before the first `show()`.

3. **`setMask(QBitmap.fromImage(alpha_channel))` is the right pattern.** Don't use `QPixmap.mask()` — it's deprecated for click-through. Get the alpha channel from `toImage().convertToFormat(QImage.Format_Alpha8)`.

4. **`Tool` flag** removes the entry from the taskbar (good for pets). Use `SplashScreen` instead if you want it visible in the taskbar.

5. **Mask scaling:** if the window size changes (zoom, scale), call `_build_mask()` again with the new size. Scaled pixmap → new alpha image → new mask.

---

## Drag + throw physics

For widgets that get dragged and (optionally) thrown on release:

```python
import time, math
from PySide6.QtCore import Qt
from PySide6.QtGui import QMouseEvent

class DraggableOverlay(QWidget):
    def __init__(self):
        super().__init__()
        self._press_pos = None
        self._press_global = None
        self._drag_samples = []  # (t, x, y) — sampled for velocity

    def mousePressEvent(self, ev: QMouseEvent):
        if ev.button() == Qt.LeftButton:
            self._press_pos = ev.position().toPoint()
            self._press_global = ev.globalPosition().toPoint()
            self._drag_samples = [(time.time(),
                                   ev.globalPosition().x(),
                                   ev.globalPosition().y())]

    def mouseMoveEvent(self, ev: QMouseEvent):
        if self._press_pos is not None:
            delta = ev.globalPosition().toPoint() - self._press_global
            self.move(self.pos() + delta)
            self._drag_samples.append((time.time(),
                                       ev.globalPosition().x(),
                                       ev.globalPosition().y()))
            # Keep only the most recent 200ms for velocity estimation
            self._drag_samples = [(t, x, y) for (t, x, y) in self._drag_samples
                                  if time.time() - t < 0.2]

    def mouseReleaseEvent(self, ev: QMouseEvent):
        if self._press_pos is not None:
            self._drag_samples.append((time.time(),
                                       ev.globalPosition().x(),
                                       ev.globalPosition().y()))
            t0, x0, y0 = self._drag_samples[0]
            t1, x1, y1 = self._drag_samples[-1]
            dt = max(0.001, t1 - t0)
            vx = (x1 - x0) / dt
            vy = (y1 - y0) / dt
            speed = math.hypot(vx, vy)
            if speed > 200:  # threshold — slow drags don't throw
                self._throw(vx, vy)
            self._press_pos = None

    def _throw(self, vx, vy):
        # Hand off to a physics tick (gravity, bounce, friction)
        self.vx, self.vy = vx, vy
        # ... start a QTimer that applies gravity per tick
```

The 200ms sample window is the sweet spot: enough to filter jitter, short enough to feel responsive.

---

## Multi-monitor screen box

```python
from PySide6.QtGui import QGuiApplication
from PySide6.QtCore import QRect

def get_screen_box():
    """Union of all monitors' available geometry."""
    virt = QRect()
    for s in QGuiApplication.screens():
        virt = virt.united(s.availableGeometry())
    return virt.left(), virt.top(), virt.right(), virt.bottom()
```

**Pitfall:** `primaryScreen()` only gives ONE monitor. For pets/widgets that can move across monitors, always union `screens()`. Recompute occasionally (every few seconds in your tick) — monitor arrangement changes when the user docks/un-docks.

---

## State machine for autonomous behaviors

Pet/widget behaviors (idle → walk → sleep → drag → throw → ...) are best modeled as an enum + tick:

```python
from enum import Enum
import random

class State(Enum):
    IDLE = "idle"
    WALK = "walk"
    SLEEP = "sleep"

class PetOverlay(DraggableOverlay):
    def __init__(self):
        super().__init__()
        self._state = State.IDLE
        self._state_t = 0.0
        self._next_action_t = random.uniform(4, 10)

        from PySide6.QtCore import QTimer
        self._timer = QTimer(self)
        self._timer.timeout.connect(self._tick)
        self._timer.start(33)  # ~30 FPS

    def _tick(self):
        dt = 0.033
        self._state_t += dt
        if self._state == State.IDLE and self._state_t > self._next_action_t:
            self._enter_walk()

    def _enter_walk(self):
        self._state = State.WALK
        self._state_t = 0.0
        # ... pick a target, start moving
```

State transitions in dedicated `_enter_X()` methods keeps the tick readable. Add `on_state_change` signal so the UI can react (e.g., show Z-bubble when entering SLEEP).

---

## System tray integration

```python
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
from PySide6.QtGui import QIcon

tray = QSystemTrayIcon()
tray.setIcon(QIcon("icon.png"))
tray.setToolTip("My Overlay")

menu = QMenu()
menu.addAction("Show All", lambda: [w.show() for w in overlays])
menu.addAction("Hide All", lambda: [w.hide() for w in overlays])
menu.addSeparator()
menu.addAction("Quit", QApplication.instance().quit)
tray.setContextMenu(menu)
tray.show()
```

**Pitfall:** `app.setQuitOnLastWindowClosed(False)` — otherwise closing all overlay windows will quit the app before user can re-show via tray.

**Auto-show tray notification on first launch** with `tray.showMessage("Title", "Right-click for menu", QSystemTrayIcon.Information, 1500)`.

---

## Image alpha processing: white-bg → transparent

For pets/widgets where the user uploads PNG/JPG with a white background:

```python
import numpy as np
from PIL import Image, ImageFilter

def remove_white_bg(img: Image.Image, threshold=235, feather=2) -> Image.Image:
    if img.mode != "RGBA":
        img = img.convert("RGBA")
    arr = np.array(img, dtype=np.int16)
    r, g, b = arr[..., 0], arr[..., 1], arr[..., 2]
    whiteness = np.minimum(np.minimum(r, g), b)  # min channel = whiteness

    # whiteness >= threshold → alpha 0 (white)
    # whiteness <= (255 - (255-threshold)) → alpha 255 (clearly not white)
    white_band = max(1, 255 - threshold)
    alpha = np.clip((threshold - whiteness) * (255 / white_band), 0, 255).astype(np.uint8)

    if feather > 0:
        # Gaussian blur on alpha → smooths the edge so it doesn't look jagged
        alpha_img = Image.fromarray(alpha, mode="L").filter(
            ImageFilter.GaussianBlur(radius=feather)
        )
        alpha = np.array(alpha_img, dtype=np.uint8)

    out = np.array(img, dtype=np.uint8)  # MUST be uint8, not int16
    out[..., 3] = alpha
    return Image.fromarray(out, mode="RGBA")
```

Then `img.getbbox()` gives the tight crop, add a few px padding, scale to standard size.

**Pitfall:** `Image.fromarray(arr, mode="RGBA")` does NOT accept `int16`. Convert back to `uint8` first (the original int16 was only for the subtraction math).

---

## Frame sequence detection

For sprite animations where users upload `pet_001.png`, `pet_002.png`, ... in the same folder:

```python
import re
from pathlib import Path

_FRAME_NUMBER_RE = re.compile(r"^(.+?)_(\d+)(?:_([a-zA-Z]+))?$")

def detect_frame_sequence(path: str) -> list[str]:
    p = Path(path)
    parent = p.parent
    suffix = p.suffix
    stem = p.stem
    m = _FRAME_NUMBER_RE.match(stem)
    if not m:
        return []
    prefix = m.group(1)
    siblings = []
    for f in parent.iterdir():
        if f.suffix.lower() != suffix.lower():
            continue
        sm = _FRAME_NUMBER_RE.match(f.stem)
        if sm and sm.group(1) == prefix:
            try:
                siblings.append((int(sm.group(2)), str(f)))
            except ValueError:
                pass
    if len(siblings) < 2:
        return []
    siblings.sort()
    return [s[1] for s in siblings]
```

Sort by integer frame number, not lex (`pet_10.png` would otherwise sort before `pet_2.png`).

---

## Speech bubble widget

Separate small widget positioned above the pet. Use `Qt.Tool` + `Qt.WindowStaysOnTopHint` + `Qt.WindowTransparentForInput` so clicks pass through to the pet:

```python
class Bubble(QWidget):
    def __init__(self, text):
        super().__init__()
        self.setWindowFlags(
            Qt.Tool | Qt.FramelessWindowHint
            | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput
        )
        self.setAttribute(Qt.WA_TranslucentBackground)
        # ... paint rounded rect + triangle pointing down
```

**Pitfall:** `QFontMetrics.boundingRect(QRectF, ...)` does NOT exist in PySide6. Use the int-coord overload: `fm.boundingRect(0, 0, max_w, 1000, Qt.TextWordWrap, text)`.

---

## Persistence

```python
import json
from pathlib import Path

class AppConfig:
    def __init__(self, path):
        self.path = Path(path)
        self.data = self._load()

    def _load(self):
        if self.path.exists():
            return json.loads(self.path.read_text(encoding="utf-8"))
        return self._default()

    def save(self):
        self.path.write_text(json.dumps(self.data, indent=2), encoding="utf-8")

    def _default(self):
        return {"version": 1, "pets": [], "global_behaviors": {}}
```

Save every 5s in a `QTimer`, plus once on quit. Don't save on every property change — that's IO thrash.

---

## Autostart on Windows (registry)

```python
import winreg

REG = r"Software\Microsoft\Windows\CurrentVersion\Run"

def set_autostart(enable: bool, app_name="MyOverlay"):
    key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG)
    if enable:
        # Use pythonw (no console) for GUI apps
        exe = sys.executable.replace("python.exe", "pythonw.exe")
        if not Path(exe).exists():
            exe = sys.executable
        winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ,
                          f'"{exe}" "{script_path}" --minimized')
    else:
        try: winreg.DeleteValue(key, app_name)
        except FileNotFoundError: pass
    winreg.CloseKey(key)
```

`HKCU\...\Run` is user-level (no admin needed). Use `pythonw` to avoid the cmd window.

---

## Python launcher script (run.bat) pitfalls

When the project has a `run.bat` for the user to double-click, **don't just call `where pythonw`** — the user's PATH may put a hermes venv Python 3.11 first, but your deps are compiled for 3.13. Pick explicitly:

```bat
@echo off
setlocal EnableDelayedExpansion
set "PYEXE="

REM Priority 1: MS Store Python 3.13
set "CAND=%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe"
if exist "!CAND!" set "PYEXE=!CAND!"

REM Priority 2: System Python 3.13/3.12/3.11 at common paths
if "!PYEXE!"=="" (
    for %%V in (313 312 311) do (
        for %%P in ("C:\Python%%V\python.exe" "C:\Program Files\Python%%V\python.exe") do (
            if exist %%P set "PYEXE=%%~P" & goto :found
        )
    )
)

REM Priority 3: py launcher
if "!PYEXE!"=="" (
    py -3.13 -c "import sys; print(sys.executable)" >nul 2>&1 && (
        for /f "delims=" %%V in ('py -3.13 -c "import sys; print(sys.executable)"') do set "PYEXE=%%V"
    )
)

:found
start "" "!PYEXE!" "%~dp0main.py" %*
```

**Also:** if you `pip install --target D:/pylib`, set `PYTHONPATH=D:/pylib` in the bat to find the deps (this avoids the Windows long-path error from installing into MS Store Python's site-packages).

---

## Bundled resources

- **`templates/overlay_window.py`** — minimal draggable + masked overlay. Copy and customize for any "always-on-top semi-transparent" widget.
- **`templates/state_machine_pet.py`** — full pet with state machine, drag/throw, speech bubble, tray. The `桌面宠物` project's core.
- **`references/desktop-pet-case-study.md`** — concrete worked example from the actual project (multi-pet manager, persistence, settings dialog, frame animations).
- **`references/image-alpha-pipeline.md`** — white-bg→transparent + autocrop + scale, with PIL/numpy pitfalls.
- **`references/multi-monitor-and-physics.md`** — screen union, throw physics math, bounce/friction tuning.
- **`scripts/smoke_test_overlay.py`** — drop-in smoke test that spawns an overlay, runs 5s of ticks, checks `isVisible()` and `mask()` is non-empty. Run after building to catch Qt setup bugs early.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…