Skip to content

How Tray Visibility Works

Mikhail Deynekin edited this page Oct 16, 2026 · 1 revision

How Tray Visibility Works

Windows decides whether a notification-area icon sits on the taskbar or inside the overflow flyout by reading one registry value per icon. This page documents that store, why naive tweaks decay, which legacy switches still matter, which registry areas are dangerous, and how TrayIconGuard v8 reconciles everything. Every statement is traced to the studied source tree (124e849) or to the repository's own mechanics canon, docs/HOW-IT-WORKS.md, which was introduced with the v7 rewrite and carried into v8.


Contents


The store Windows actually uses

Since Windows 10 1703 (build 15063), per-icon notification-area visibility lives in:

HKEY_CURRENT_USER\Control Panel\NotifyIconSettings\{<icon-id>}

Each tray registration gets one subkey — usually a GUID, occasionally a shell name. TrayIconGuard reads and writes exactly this location (src/core/trayicons.cpp:14):

Value Type Meaning
IsPromoted REG_DWORD 1 = icon on the taskbar, 0 = icon in the overflow flyout. The Windows Settings per-icon toggle reads and writes this same value.
ExecutablePath REG_SZ Owning executable, e.g. C:\Program Files\App\app.exe.
IconResource REG_SZ Icon resource, e.g. C:\…\app.exe,-101.
IconSnapshot REG_BINARY Last rendered bitmap of the icon.
LastNotificationAddedTime REG_QWORD FILETIME (UTC) of the last notification.

The app derives the friendly name it displays from the executable's FileDescription version-resource field, falling back to the file name (src/core/trayicons.cpp:40-68), and expands environment variables in ExecutablePath before doing so.

Note

There is no documented public API for this store: it is observed shell state. That is why the repository documents it explicitly and why the watcher reconciles rather than assumes.

The decay problem

Explorer (re)creates these subkeys every time an application registers its tray icon — at application start, after updates, after crashes, at logon. On (re)registration Windows writes IsPromoted = 0 unless it considers the icon recently interactive. Consequences:

  1. A one-shot registry tweak (or a .reg file, or a script run once) stops working within days: newly updated applications reappear hidden.
  2. User reports match: applications whose updaters restart frequently are the classic offenders.
  3. The durable fix is a watcher: observe the subtree, debounce burst writes, re-promote.
sequenceDiagram
    participant App as Application
    participant Ex as Explorer
    participant Reg as NotifyIconSettings
    participant G as TrayIconGuard watcher
    App->>Ex: Shell_NotifyIcon (register)
    Ex->>Reg: recreate subkey, IsPromoted=0
    Reg-->>G: registry change notification
    G->>G: debounce (default 400 ms)
    G->>Reg: IsPromoted=1 (unless excluded/user-hidden)
    Ex->>Ex: tray refresh shows the icon
Loading

Applying changes live

Explorer normally picks up external IsPromoted writes without a restart — icons move on the next tray refresh. When it does not (observed on some Windows 11 24H2+ builds, after fast user switching, or when Explorer cached the layout), a shell restart applies everything immediately. TrayIconGuard offers a guarded restart (src/core/explorer.cpp:30-69): terminate every explorer.exe, wait up to 8 s for the shell's own auto-restart, start explorer.exe manually if it did not come back, then wait up to 4 s for the shell window. The optional auto-restart in the guard adds a cooldown (default 60 s, floor 10 s) and only fires after two consecutive passes that had to fix something, to avoid restart flapping (src/core/guard.cpp).

The legacy switch: EnableAutoTray

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer  →  EnableAutoTray (DWORD)

0 means “show all icons” for the Windows XP–7 era Hide inactive icons checkbox. Modern Windows largely ignores it, but writing 0 is harmless and still helps a few legacy shell components. TrayIconGuard therefore keeps it strictly as a belt-and-suspenders measure (settings key guard.legacyBelt, default on; src/core/legacy.cpp:22-27), never as the primary mechanism. Removing the value hands control back to Windows (--restore-defaults does exactly that).

System icon policies: HideSCA values

The volume, network and power system icons can be force-hidden by policy values HideSCAVolume, HideSCANetwork, HideSCAPower equal to 1. Their correct home is:

HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer

Pre-7.0 artifacts of this repository wrote them one path segment higher (without Policies), where they have no effect at all; the project's own audit notice documents this (see Legacy Script Editions). TrayIconGuard counts stale =1 values for the Dashboard and removes them on demand or during --promote-all (src/core/legacy.cpp:34-50).

Pre-1703 icon cache: TrayNotify

HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify
  → IconStreams, PastIconsStream (binary caches)

Older Windows 10 builds cached per-icon preferences here. Deleting the two values forces a rebuild and clears phantom or stale icons; a shell restart afterwards is recommended. On Windows 11 the key is frequently absent. TrayIconGuard exposes this only as an explicit repair (Guard page → Reset legacy tray cache, src/core/legacy.cpp:57-64), never automatically.

Danger zone: StuckRects3

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3\Settings

This binary blob stores resolution- and monitor-specific taskbar layout state. Overwriting it with a canned value — which the v6-era .reg file did — corrupts the taskbar layout on other displays. TrayIconGuard v8 never touches it; the repository's pitfall list states the rule explicitly. If a guide tells you to paste a StuckRects3 hex blob, close the guide.

What a reconcile pass does

One pass of the watcher (src/core/guard.cpp, RunPass) works as follows:

  1. Rescan the store subtree into a snapshot (subkeys → entries with IsPromoted, executable, friendly name, exclusion match).
  2. For each entry compute the desired state from the current visibility mode:
    • full — every icon promoted;
    • compact — only TrayIconGuard's own icon promoted;
    • custom (default) — promote unless the executable matches an exclusion wildcard or the icon id is in the user-hidden list.
  3. Write IsPromoted only where the current value differs; count changes; log an event line and emit a UI/tray notification (N|name1, name2, truncated at 160 characters).
  4. If anything changed and auto-restart is enabled with the cooldown elapsed and two consecutive fixing passes, restart Explorer.
  5. Re-arm the registry notification (the wait is one-shot per signal).

Warning

In the published 8.3.0/8.4.0 source the pass is reached only through the poll timer or a stop signal: the armed registry event is never included in the wait set, so with the default pollMs: 0 the loop blocks forever after arming. The defect, its fix and a verification recipe are documented in The Guard Watcher; the pass logic itself is exactly as described above and is what the patched build executes on every notification.


See also

Clone this wiki locally