#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