Anleitung: Desktop-Suite

Was ist das?

Ein AutoHotkey-v2-Skript, das drei Dinge in einem bündelt: eine Desktop-Leiste in der Taskleiste zum Anzeigen und Wechseln der virtuellen Windows-Desktops, ein Autotile, das Fenster automatisch in Spalten oder einem Quad-Layout anordnet, und einen Dark/Light-Umschalter für das Windows-Farbschema. Dazu kommt ein Always-on-Top-Schalter für einzelne Fenster.

Voraussetzungen

Du brauchst AutoHotkey v2 (installiert) und die VirtualDesktopAccessor.dll unter C:\Tools\VirtualDesktopAccessor\VirtualDesktopAccessor.dll. Liegt die DLL woanders, passe die Zeile DllPath := ... ganz oben im Skript an. Ohne die DLL startet das Skript nicht (es meldet sich mit einer Fehlermeldung).

Starten

Doppelklick auf desktop-suite.ahk. Es erscheint das grüne AHK-Symbol im Infobereich und links neben der Uhr die Desktop-Leiste mit den nummerierten Knöpfen. Soll das Skript bei jedem Windows-Start mitlaufen: Win+Rshell:startup → Enter, und eine Verknüpfung der Datei in diesen Ordner legen.

Nach jeder Änderung am Skript: Rechtsklick aufs AHK-Symbol im Infobereich → Reload Script.

Virtuelle Desktops

Die Leiste zeigt alle Desktops als Knöpfe; der aktive leuchtet blau. Klick auf einen Knopf wechselt dorthin. Legst du in Windows Desktops an oder löscht welche (Win+Tab), passt sich die Leiste automatisch an. Bei Vollbild-Anwendungen (Videos, Spiele) blendet sie sich selbst aus.

Kürzel Wirkung
Win + 1…9 zu Desktop 1–9 wechseln
Win + Shift + 1…9 aktives Fenster dorthin schicken, du bleibst hier
Win + Alt + 1…9 Fenster verschieben und mitspringen
Win + 0 Desktop-Leiste ein-/ausblenden

Autotile

Das Autotile ordnet alle „normalen" Fenster des Bildschirms automatisch an — minimierte Fenster, Dialoge und Fenster auf anderen Desktops bleiben unberührt. Solange es an ist, fügen sich neu geöffnete Fenster von selbst ein und geschlossene machen Platz.

Kürzel Wirkung
Strg + Shift + Win + Z Autotile an, Layout Spalten (alle nebeneinander)
Strg + Shift + Win + J Autotile an, Layout Quad (links eine große Hälfte, Rest rechts gestapelt — bei 3 Fenstern: halb + zwei Viertel)
Strg + Shift + Win + Y Autotile aus
Strg + Shift + Win + N aktives Fenster eine Position weiterschieben
Strg + Shift + Win + B aktives Fenster eine Position zurückschieben

Tipp: Im Quad-Layout bestimmt die Position 1 den großen Platz links — mit N/B schiebst du das gewünschte Fenster dorthin. Die Abstände lassen sich oben im Skript über GAP_INNER und GAP_OUTER ändern.

Dark/Light umschalten

Kürzel Wirkung
Strg + Shift + Win + O Windows-Farbschema zwischen Dunkel und Hell umschalten

Schaltet App- und System-Theme gleichzeitig um; Taskleiste, Explorer und die meisten Apps wechseln sofort. Ein kurzes Tooltip zeigt den neuen Zustand. Einzelne Programme mit eigener Theme-Einstellung (z. B. Browser auf „festes Theme") folgen ihrer eigenen Einstellung — stelle sie dort auf „System", wenn sie mitwechseln sollen.

Always on Top

Kürzel Wirkung
Strg + Alt + T aktives Fenster immer im Vordergrund halten (an/aus)
Shift + Rechtsklick auf eine Titelleiste dasselbe für das angeklickte Fenster

Wenn etwas klemmt

Reagieren die Kürzel nicht, prüfe, ob das Skript läuft (grünes Symbol im Infobereich) und ob du wirklich alle drei Modifier hältst (erst Strg+Shift+Win gedrückt halten, dann den Buchstaben). Meldet das Skript beim Start die fehlende DLL, stimmt der Pfad in DllPath nicht. Und wenn sich Fenster beim Kacheln überlappen, sind es meist zu viele für Spalten — dann aufs Quad wechseln (J) oder Fenster auf mehrere virtuelle Desktops verteilen.


#Requires AutoHotkey v2.0
#SingleInstance Force
CoordMode "Mouse", "Screen"

; ╔══════════════════════════════════════════════════════════════╗
; ║  Desktop-Suite:                                              ║
; ║   1) Virtuelle-Desktop-Leiste in der Taskleiste (vdesk)      ║
; ║   2) Autotile — Spalten / Quad                               ║
; ║   3) Dark/Light-Umschalter                                   ║
; ╚══════════════════════════════════════════════════════════════╝
;
;  ── Virtuelle Desktops ──────────────────────────────────────
;    Win+1..9          Desktop wechseln
;    Win+Shift+1..9    aktives Fenster dorthin schicken (du bleibst)
;    Win+Alt+1..9      Fenster verschieben UND mitspringen
;    Win+0             Desktop-Leiste ein/aus
;
;  ── Autotile (Strg+Shift+Win + Buchstabe) ───────────────────
;    Strg+Shift+Win+Z  Autotile AN, Layout Spalten
;    Strg+Shift+Win+J  Autotile AN, Layout Quad (links halb, Rest rechts)
;    Strg+Shift+Win+Y  Autotile AUS
;    Strg+Shift+Win+N  aktives Fenster eine Position weiter
;    Strg+Shift+Win+B  aktives Fenster eine Position zurueck
;
;  ── Sonstiges ───────────────────────────────────────────────
;    Strg+Shift+Win+O  Windows Dark/Light umschalten
;    Strg+Alt+T        Always on Top fuer aktives Fenster
;    Shift+Rechtsklick auf Titelleiste  ->  Always on Top

; ══════════════════════════════════════════════════════════════
; ══  TEIL 1: VIRTUELLE-DESKTOP-LEISTE                        ══
; ══════════════════════════════════════════════════════════════

; ══ Einstellungen (Leiste) ═══════════════════════════════════
DllPath  := "C:\Tools\VirtualDesktopAccessor.dll"
BTN_W    := 28            ; Breite pro Button
BTN_H    := 26            ; Hoehe
GAP      := 2             ; Abstand zwischen Buttons
PAD      := 14            ; Abstand zum Infobereich (Uhr/Icons)
COL_BG   := 0x1F1F1F      ; Panel-Hintergrund (Taskleistenton)
COL_ON   := 0x0A84FF      ; aktiver Desktop
COL_OFF  := 0x3A3A3A      ; inaktive Desktops
AUTOHIDE := true          ; Panel bei Vollbild automatisch ausblenden
; ═════════════════════════════════════════════════════════════

hDll := DllCall("LoadLibrary", "Str", DllPath, "Ptr")
if !hDll {
    MsgBox "VirtualDesktopAccessor.dll nicht gefunden:`n" DllPath
    ExitApp
}
pGoTo    := DllCall("GetProcAddress", "Ptr", hDll, "AStr", "GoToDesktopNumber", "Ptr")
pMove    := DllCall("GetProcAddress", "Ptr", hDll, "AStr", "MoveWindowToDesktopNumber", "Ptr")
pCurrent := DllCall("GetProcAddress", "Ptr", hDll, "AStr", "GetCurrentDesktopNumber", "Ptr")
pCount   := DllCall("GetProcAddress", "Ptr", hDll, "AStr", "GetDesktopCount", "Ptr")
pPin     := DllCall("GetProcAddress", "Ptr", hDll, "AStr", "PinWindow", "Ptr")

DeskCount()   => DllCall(pCount, "Int")
DeskCurrent() => DllCall(pCurrent, "Int") + 1

; ══ Kernfunktionen ═══════════════════════════════════════════
VDGo(n) {
    global pGoTo
    if (n > DeskCount()) {
        Flash("Desktop " n " existiert nicht")
        return
    }
    DllCall(pGoTo, "Int", n - 1)
}

MoveWin(n, follow := false) {
    global pMove, pGoTo, LastWin
    if (n > DeskCount()) {
        Flash("Desktop " n " existiert nicht")
        return
    }
    hwnd := WinExist("A")
    if (hwnd = PanelHwnd())
        hwnd := LastWin
    if !hwnd
        return
    DllCall(pMove, "Ptr", hwnd, "Int", n - 1)
    if follow
        DllCall(pGoTo, "Int", n - 1)
    else
        Flash("-> Desktop " n)
}

; kleines Overlay (gemeinsam fuer alle Teile)
Flash(txt) {
    ToolTip txt
    SetTimer () => ToolTip(), -900
}

; ══ Panel ════════════════════════════════════════════════════
global Panel := "", Tabs := [], LastCount := 0, LastCur := 0
global LastWin := 0, LastPos := "", Visible := true, Manual := true

PanelHwnd() => Panel ? Panel.Hwnd : 0
PanelWidth(cnt) => cnt * BTN_W + (cnt - 1) * GAP + 2 * GAP

TabClick(n, *) {
    VDGo(n)
}

BuildPanel(cnt) {
    global Panel, Tabs, BTN_W, BTN_H, GAP, COL_BG, COL_OFF, pPin
    global LastCur, LastPos, Visible

    if Panel
        Panel.Destroy()
    Tabs := []

    Panel := Gui("-Caption +ToolWindow +AlwaysOnTop +E0x08000000 -DPIScale")
    Panel.BackColor := COL_BG
    Panel.MarginX := 0, Panel.MarginY := 0

    Loop cnt {
        Panel.SetFont("s10 Bold cFFFFFF", "Segoe UI")
        c := Panel.Add("Text", Format("x{} y{} w{} h{} Center Background{:06X}"
                                    , GAP + (A_Index-1)*(BTN_W+GAP), GAP
                                    , BTN_W, BTN_H - 2*GAP, COL_OFF), A_Index)
        c.OnEvent("Click", TabClick.Bind(A_Index))
        Tabs.Push(c)
    }

    Panel.Show(Format("NoActivate Hide w{} h{}", PanelWidth(cnt), BTN_H))
    DllCall(pPin, "Ptr", Panel.Hwnd)          ; auf allen Desktops sichtbar
    LastPos := ""
    Place(cnt)
    Visible := false
    LastCur := 0
    Refresh(true)
}

Place(cnt) {
    global Panel, BTN_H, PAD, LastPos

    hTray := WinExist("ahk_class Shell_TrayWnd")
    if !hTray
        return
    WinGetPos &tx, &ty, &tw, &th, "ahk_id " hTray

    w := PanelWidth(cnt)
    x := tx + tw - 300 - w                      ; Fallback

    hNotify := DllCall("FindWindowEx", "Ptr", hTray, "Ptr", 0
                     , "Str", "TrayNotifyWnd", "Ptr", 0, "Ptr")
    if hNotify {
        r := Buffer(16, 0)
        DllCall("GetWindowRect", "Ptr", hNotify, "Ptr", r)
        x := NumGet(r, 0, "Int") - w - PAD      ; links vom Infobereich
    }
    y := ty + (th - BTN_H) // 2

    pos := x "," y
    if (pos = LastPos)
        return
    LastPos := pos

    ; SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE
    DllCall("SetWindowPos", "Ptr", Panel.Hwnd, "Ptr", 0
          , "Int", x, "Int", y, "Int", 0, "Int", 0, "UInt", 0x0015)
}

Raise() {
    global Panel
    ; SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE, HWND_TOPMOST
    DllCall("SetWindowPos", "Ptr", Panel.Hwnd, "Ptr", -1
          , "Int", 0, "Int", 0, "Int", 0, "Int", 0, "UInt", 0x0013)
}

; ── Vollbild-Erkennung ───────────────────────────────────────
; Wie die Shell: aktives Fenster deckt den ganzen Monitor ab
; und ist weder Desktop noch Taskleiste.
IsFullscreen() {
    hwnd := WinExist("A")
    if !hwnd || (hwnd = PanelHwnd())
        return false

    cls := WinGetClass(hwnd)
    if (cls = "WorkerW" || cls = "Progman" || cls = "Shell_TrayWnd")
        return false

    r := Buffer(16, 0)
    if !DllCall("GetWindowRect", "Ptr", hwnd, "Ptr", r)
        return false
    wx := NumGet(r, 0, "Int"), wy := NumGet(r, 4, "Int")
    wr := NumGet(r, 8, "Int"), wb := NumGet(r, 12, "Int")

    ; Monitor unter diesem Fenster (MONITOR_DEFAULTTONEAREST)
    hMon := DllCall("MonitorFromWindow", "Ptr", hwnd, "UInt", 2, "Ptr")
    mi := Buffer(40, 0)
    NumPut("UInt", 40, mi, 0)
    if !DllCall("GetMonitorInfo", "Ptr", hMon, "Ptr", mi)
        return false
    mx := NumGet(mi, 4, "Int"), my := NumGet(mi, 8, "Int")
    mr := NumGet(mi, 12, "Int"), mb := NumGet(mi, 16, "Int")

    return (wx <= mx) && (wy <= my) && (wr >= mr) && (wb >= mb)
}

SetVisible(show) {
    global Panel, Visible
    if (show = Visible)
        return
    Visible := show
    if show {
        Panel.Show("NoActivate")
        Raise()
    } else
        Panel.Hide()
}

Paint(idx, active) {
    global Tabs, COL_ON, COL_OFF
    c := Tabs[idx]
    c.SetFont(active ? "s10 Bold cFFFFFF" : "s10 Norm cA0A0A0", "Segoe UI")
    c.Opt("+Background" Format("{:06X}", active ? COL_ON : COL_OFF))
    c.Redraw()
}

Refresh(force := false) {
    global Tabs, LastCur, LastCount, LastWin, Panel
    global AUTOHIDE, Manual, Visible

    if (h := WinExist("A")) && (h != PanelHwnd())
        LastWin := h

    cnt := DeskCount()
    if (cnt != LastCount) {
        LastCount := cnt
        BuildPanel(cnt)
        return
    }

    ; sichtbar = manuell an UND (kein Autohide ODER kein Vollbild)
    SetVisible(Manual && (!AUTOHIDE || !IsFullscreen()))
    if !Visible
        return

    Place(cnt)
    Raise()

    cur := DeskCurrent()
    if (cur = LastCur && !force)
        return
    LastCur := cur

    Loop Tabs.Length
        Paint(A_Index, A_Index = cur)

    ; RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW
    DllCall("RedrawWindow", "Ptr", Panel.Hwnd, "Ptr", 0, "Ptr", 0, "UInt", 0x0185)
}

LastCount := DeskCount()
BuildPanel(LastCount)
SetTimer Refresh, 250

; ══════════════════════════════════════════════════════════════
; ══  TEIL 2: AUTOTILE (Spalten / Quad)                       ══
; ══════════════════════════════════════════════════════════════

; ══ Einstellungen (Autotile) ═════════════════════════════════
global GAP_INNER := 8         ; Abstand zwischen Fenstern (px)
global GAP_OUTER := 8         ; Abstand zum Bildschirmrand (px)
global Layout    := "columns" ; Startlayout: "columns" oder "grid"
; ═════════════════════════════════════════════════════════════

global AutoTile := false, LastSig := "", Tiling := false
global TileOrder := []

; ══ Fenster-Filter: was darf gekachelt werden? ═══════════════
IsManageable(hwnd) {
    try {
        if !hwnd
            return false
        if !DllCall("IsWindowVisible", "Ptr", hwnd)
            return false

        ; auf anderem virtuellen Desktop / versteckt (UWP) -> ueberspringen
        cloaked := 0
        DllCall("dwmapi\DwmGetWindowAttribute", "Ptr", hwnd
              , "UInt", 14, "Int*", &cloaked, "UInt", 4)   ; DWMWA_CLOAKED
        if cloaked
            return false

        if (WinGetTitle(hwnd) = "")
            return false
        if (WinGetMinMax(hwnd) = -1)                        ; minimiert
            return false

        style := WinGetStyle(hwnd)
        ex    := WinGetExStyle(hwnd)

        if (ex & 0x80)                                      ; WS_EX_TOOLWINDOW
            return false
        ; Fenster mit Owner (Dialoge/Popups) raus, ausser echte AppWindows
        owner := DllCall("GetWindow", "Ptr", hwnd, "UInt", 4, "Ptr")  ; GW_OWNER
        if (owner && !(ex & 0x40000))                       ; WS_EX_APPWINDOW
            return false
        ; nur groessenveraenderbare Fenster kacheln (fixe Dialoge floaten lassen)
        if !(style & 0x40000)                               ; WS_THICKFRAME
            return false

        static skip := Map("Progman",1, "WorkerW",1, "Shell_TrayWnd",1
                         , "Shell_SecondaryTrayWnd",1, "Windows.UI.Core.CoreWindow",1)
        if skip.Has(WinGetClass(hwnd))
            return false

        return true
    }
    return false
}

MonOf(hwnd) => DllCall("MonitorFromWindow", "Ptr", hwnd, "UInt", 2, "Ptr") ; NEAREST

; Arbeitsbereich eines Monitors (ohne Taskleiste)
WorkArea(hMon) {
    mi := Buffer(40, 0), NumPut("UInt", 40, mi, 0)
    DllCall("GetMonitorInfo", "Ptr", hMon, "Ptr", mi)
    return {l: NumGet(mi,20,"Int"), t: NumGet(mi,24,"Int")
          , r: NumGet(mi,28,"Int"), b: NumGet(mi,32,"Int")}
}

; DWM-Rahmenversatz: echter Fensterrahmen vs. sichtbare Kante
FrameOffsets(hwnd) {
    ow := Buffer(16,0), fw := Buffer(16,0)
    DllCall("GetWindowRect", "Ptr", hwnd, "Ptr", ow)
    if DllCall("dwmapi\DwmGetWindowAttribute", "Ptr", hwnd
             , "UInt", 9, "Ptr", fw, "UInt", 16)            ; EXTENDED_FRAME_BOUNDS
        return {l:0, t:0, r:0, b:0}
    return { l: NumGet(fw,0,"Int") - NumGet(ow,0,"Int")
           , t: NumGet(fw,4,"Int") - NumGet(ow,4,"Int")
           , r: NumGet(ow,8,"Int") - NumGet(fw,8,"Int")
           , b: NumGet(ow,12,"Int") - NumGet(fw,12,"Int") }
}

; Fenster an sichtbarer Zielposition platzieren (Versatz rausgerechnet)
PlaceWindow(hwnd, x, y, w, h) {
    try {
        if (WinGetMinMax(hwnd) = 1)                          ; maximiert -> erst normal
            WinRestore(hwnd)
        o := FrameOffsets(hwnd)
        DllCall("SetWindowPos", "Ptr", hwnd, "Ptr", 0
              , "Int", x - o.l, "Int", y - o.t
              , "Int", w + o.l + o.r, "Int", h + o.t + o.b
              , "UInt", 0x14)                                ; NOZORDER | NOACTIVATE
    }
}

; ══ Layouts ══════════════════════════════════════════════════
LayColumns(n, a) {
    global GAP_INNER, GAP_OUTER
    rects := []
    L := a.l+GAP_OUTER, T := a.t+GAP_OUTER, R := a.r-GAP_OUTER, B := a.b-GAP_OUTER
    colW := (R - L - (n-1)*GAP_INNER) / n
    Loop n {
        cx := L + (A_Index-1)*(colW+GAP_INNER)
        rects.Push({x:Round(cx), y:T, w:Round(colW), h:B-T})
    }
    return rects
}

; "Quad": erstes Fenster = linke Haelfte (Master),
; die restlichen gestapelt in der rechten Haelfte.
;   2 Fenster -> zwei Haelften
;   3 Fenster -> links halb, rechts zwei Viertel
;   4 Fenster -> links halb, rechts drei Zeilen
LayGrid(n, a) {
    global GAP_INNER, GAP_OUTER
    rects := []
    if (n = 0)
        return rects
    L := a.l+GAP_OUTER, T := a.t+GAP_OUTER, R := a.r-GAP_OUTER, B := a.b-GAP_OUTER
    totalW := R - L, totalH := B - T
    if (n = 1) {
        rects.Push({x:L, y:T, w:totalW, h:totalH})
        return rects
    }
    masterW := (totalW - GAP_INNER) / 2
    rects.Push({x:L, y:T, w:Round(masterW), h:totalH})   ; Master links
    stackN := n - 1
    stackX := L + masterW + GAP_INNER
    stackW := totalW - masterW - GAP_INNER
    rowH   := (totalH - (stackN-1)*GAP_INNER) / stackN
    Loop stackN {
        ry := T + (A_Index-1)*(rowH+GAP_INNER)
        rects.Push({x:Round(stackX), y:Round(ry), w:Round(stackW), h:Round(rowH)})
    }
    return rects
}

; ══ Reihenfolge stabil halten ════════════════════════════════
; Bestehende Fenster behalten ihren Platz, neue kommen ans Ende,
; geschlossene fallen raus.
RebuildOrder() {
    global TileOrder
    managed := Map()
    for h in WinGetList()
        if IsManageable(h)
            managed[h] := true
    kept := []
    for h in TileOrder
        if managed.Has(h) {
            kept.Push(h), managed.Delete(h)
        }
    for h in WinGetList()                                    ; neue anhaengen
        if managed.Has(h) {
            kept.Push(h), managed.Delete(h)
        }
    TileOrder := kept
}

; Set-Signatur (sortiert) -> nur bei Fensterwechsel neu kacheln,
; nicht bei jedem Fokuswechsel
ManagedSig() {
    s := ""
    for h in WinGetList()
        if IsManageable(h)
            s .= h "`n"
    return Sort(RTrim(s, "`n"), "N")
}

; ══ Kernaktion ═══════════════════════════════════════════════
Tile() {
    global TileOrder, Tiling, Layout
    if Tiling
        return
    Tiling := true
    try {
        RebuildOrder()
        groups := Map()                                      ; nach Monitor gruppieren
        for h in TileOrder {
            m := MonOf(h)
            if !groups.Has(m)
                groups[m] := []
            groups[m].Push(h)
        }
        for m, list in groups {
            wa := WorkArea(m)
            n  := list.Length
            rects := (Layout = "grid") ? LayGrid(n, wa) : LayColumns(n, wa)
            Loop n
                PlaceWindow(list[A_Index], rects[A_Index].x, rects[A_Index].y
                                         , rects[A_Index].w, rects[A_Index].h)
        }
    }
    Tiling := false
}

; ══ Fenster tauschen (monitor-lokal, mit Umlauf) ═════════════
SwapFocused(dir) {
    global TileOrder
    hwnd := WinExist("A")
    if !hwnd || !IsManageable(hwnd)
        return
    RebuildOrder()
    hMon := MonOf(hwnd)
    subIdx := []                                             ; Indizes dieses Monitors
    for i, h in TileOrder
        if (MonOf(h) = hMon)
            subIdx.Push(i)
    if (subIdx.Length < 2)
        return
    p := 0
    for k, i in subIdx
        if (TileOrder[i] = hwnd) {
            p := k
            break
        }
    if !p
        return
    q := Mod(p - 1 + dir + subIdx.Length, subIdx.Length) + 1   ; mit Umlauf
    a := subIdx[p], b := subIdx[q]
    tmp := TileOrder[a], TileOrder[a] := TileOrder[b], TileOrder[b] := tmp
    Tile()
    WinActivate(hwnd)
}

; ══ Auto-Retile-Schleife ═════════════════════════════════════
TileTick() {
    global AutoTile, LastSig, Tiling
    if !AutoTile || Tiling
        return
    if ((s := ManagedSig()) != LastSig) {
        LastSig := s
        Tile()
    }
}

; ══ Steuerung ════════════════════════════════════════════════
StartTiling(mode) {
    global Layout, AutoTile, LastSig
    Layout := mode
    AutoTile := true
    LastSig := ManagedSig()
    Tile()
    SetTimer TileTick, 350            ; ab jetzt automatisch nachkacheln
    Flash("Autotile AN — " (mode = "grid" ? "Quad" : "Spalten"))
}

StopTiling() {
    global AutoTile
    AutoTile := false
    SetTimer TileTick, 0
    Flash("Autotile AUS")
}

; ══════════════════════════════════════════════════════════════
; ══  TEIL 3: DARK/LIGHT-UMSCHALTER                           ══
; ══════════════════════════════════════════════════════════════
; Schaltet App- UND System-Theme um und benachrichtigt alle
; laufenden Programme (kein Neustart/Abmelden noetig).
ToggleTheme() {
    key := "HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
    cur := 1
    try cur := RegRead(key, "AppsUseLightTheme")
    val := cur ? 0 : 1                                   ; 0 = Dark, 1 = Light
    try {
        RegWrite val, "REG_DWORD", key, "AppsUseLightTheme"
        RegWrite val, "REG_DWORD", key, "SystemUsesLightTheme"
    } catch {
        Flash("Theme-Umschaltung fehlgeschlagen (Registry)")
        return
    }
    ; WM_SETTINGCHANGE an alle Fenster -> Apps uebernehmen sofort
    DllCall("SendMessageTimeout", "Ptr", 0xFFFF, "UInt", 0x1A
          , "Ptr", 0, "Str", "ImmersiveColorSet"
          , "UInt", 2, "UInt", 500, "Ptr*", 0)           ; SMTO_ABORTIFHUNG
    Flash(val ? "Theme: LIGHT" : "Theme: DARK")
}

; ══════════════════════════════════════════════════════════════
; ══  TEIL 4: ALWAYS ON TOP                                   ══
; ══════════════════════════════════════════════════════════════
ToggleAOT(hwnd := 0) {
    if !hwnd
        hwnd := WinExist("A")
    if !hwnd
        return
    isTop := WinGetExStyle(hwnd) & 0x8      ; WS_EX_TOPMOST
    WinSetAlwaysOnTop -1, hwnd
    Flash(isTop ? "Always on Top: AUS" : "Always on Top: AN")
}

OverTitleBar() {
    MouseGetPos &mx, &my, &win
    if !win
        return false
    lp := ((my & 0xFFFF) << 16) | (mx & 0xFFFF)
    return SendMessage(0x0084, 0, lp, , "ahk_id " win) = 2   ; WM_NCHITTEST -> HTCAPTION
}

; ══════════════════════════════════════════════════════════════
; ══  TASTEN                                                  ══
; ══════════════════════════════════════════════════════════════

; ── Desktop-Leiste ein/aus (Notausgang) ──────────────────────
#0:: {
    global Manual
    Manual := !Manual
    Flash("Desktop-Leiste: " (Manual ? "AN" : "AUS"))
    Refresh(true)
}

; ── Desktop wechseln ─────────────────────────────────────────
#1::VDGo(1)
#2::VDGo(2)
#3::VDGo(3)
#4::VDGo(4)
#5::VDGo(5)
#6::VDGo(6)
#7::VDGo(7)
#8::VDGo(8)
#9::VDGo(9)

; ── Fenster verschieben (du bleibst) ─────────────────────────
#+1::MoveWin(1)
#+2::MoveWin(2)
#+3::MoveWin(3)
#+4::MoveWin(4)
#+5::MoveWin(5)
#+6::MoveWin(6)
#+7::MoveWin(7)
#+8::MoveWin(8)
#+9::MoveWin(9)

; ── Fenster verschieben UND mitspringen ──────────────────────
#!1::MoveWin(1, true)
#!2::MoveWin(2, true)
#!3::MoveWin(3, true)
#!4::MoveWin(4, true)
#!5::MoveWin(5, true)
#!6::MoveWin(6, true)
#!7::MoveWin(7, true)
#!8::MoveWin(8, true)
#!9::MoveWin(9, true)

; ── Autotile (Strg+Shift+Win + Buchstabe) ────────────────────
^+#z::StartTiling("columns")   ; Spalten  – Autotile an
^+#j::StartTiling("grid")      ; Quad     – Autotile an
^+#y::StopTiling()             ; alles aus
^+#n::SwapFocused(1)           ; aktives Fenster eine Position weiter
^+#b::SwapFocused(-1)          ; ... und zurueck

; ── Dark/Light ───────────────────────────────────────────────
^+#o::ToggleTheme()            ; Strg+Shift+Win+O

; ── Always on Top ────────────────────────────────────────────
^!t::ToggleAOT()               ; Strg+Alt+T

#HotIf OverTitleBar()
+RButton:: {
    MouseGetPos , , &win
    ToggleAOT(win)
}
#HotIf