Skip to main content

Find the empty gap in the Windows taskbar and place a top-most window in it, across monitors and DPI.

Project description

taskbargap

Platform: Windows 10/11 PyPI CI CodeQL OpenSSF Scorecard License: MIT

Find the empty stretch of the Windows taskbar, the gap between your app buttons and the system tray, and put a top-most window in it. Correctly, across multiple monitors and per-monitor DPI scaling.

Windows leaves that strip unused, so a small always-on widget there costs you no screen space. Doing it properly turns out to be fiddly. You have to detect the gap across two generations of taskbar internals, handle per-monitor DPI and the difference between physical and logical pixels, keep the window fitted as apps open and close, and end up with a top-most tool window that owns its input without grabbing a taskbar button of its own. This library does that, and nothing else.

Windows 10/11 only. It is built on Win32 APIs, and there is no macOS or Linux build. The package imports anywhere, but calling into it off Windows raises NotWindowsError.

Install

pip install taskbargap

The only dependency is the standard library (ctypes). It writes nothing at all: no files, no registry keys, no config, no logs. It makes no network calls. See SECURITY.md for the complete list of Win32 calls it makes.

This is an alpha, and it says so on the tin. Before you rely on it, read what has and hasn't been validated, which is specific about the configurations it has never run on.

Quickstart

import taskbargap

taskbargap.enable_dpi_awareness()          # call once, before creating windows

gap = taskbargap.find_gap()                # -> Gap | None
if gap:
    print(gap.left, gap.right, gap.width, gap.scale)

# Place a window (by HWND) into the gap, right-aligned near the tray:
taskbargap.place(my_hwnd, align="right", margin=12)

To keep it fitted as the taskbar changes, when apps open and close, when Explorer restarts, when the resolution changes:

watcher = taskbargap.GapWatcher(my_hwnd, align="right")
watcher.start()          # places it now, then re-fits on change and re-asserts top-most
# ...
watcher.stop()

start() places the window immediately, on your thread, and returns. The polling happens on a daemon thread afterwards. Pass on_change=fn if you want to be told when the gap moves, including when it gets too narrow to fit, which is usually when an app wants to get out of the way.

API

Object Purpose
find_gap() -> Gap | None Detect the empty taskbar gap on the primary monitor. None if there isn't a usable one.
place(hwnd, *, align="right", margin=12, min_width=160, width=None, height=None, gap=None, nonblocking=False) -> bool Size and position an existing window inside the gap as a top-most tool window. width defaults to filling the gap, height to the taskbar's own height. False means it did nothing: no gap, min_width didn't fit, or Windows refused the move.
GapWatcher(hwnd, *, align, margin, min_width, width, height, interval=1.0, on_change=None) Background watcher that keeps the window fitted and top-most as the taskbar changes. .start() / .stop() -> bool, or use it as a context manager.
enable_dpi_awareness() -> bool Opt into per-monitor-v2 DPI awareness. Falls back gracefully on old Windows.
Gap Frozen dataclass: left, right, top, bottom (physical px), scale (DPI factor), monitor (HMONITOR), measured, plus width / height.
NotWindowsError Raised by any Win32 call when you're not on Windows.

Units, and the DPI contract

Every coordinate you get back is a physical pixel in the coordinate space your process actually sees. Gap.scale is the divisor for toolkits that scale window position by DPI, such as pywebview and WinForms:

x_logical = round(gap.left / gap.scale)

Call enable_dpi_awareness() before you create any window. Without it Windows virtualises coordinates (a 3840px screen at 175% looks 2194px wide) and a window aimed at the gap lands somewhere else.

Whatever you do, scale describes the space your process addresses windows in, rather than the monitor's spec sheet. That means 1.0 for a DPI-unaware process, the system DPI for a system-aware one, and the taskbar monitor's own factor only for a per-monitor-aware one. The distinction is not academic: GetDpiForWindow will happily report the taskbar's real 175% to an unaware caller that sees a 2194px desktop, and dividing by that number puts the window a third of the screen away from where it belongs. The library stays self-consistent in every mode. It is only in per-monitor mode that you get real screen pixels and an unscaled window.

What it deliberately does not do

  • No metrics, rendering, or UI. You bring the window, it does the placement.
  • No decision about whether to be visible. Fullscreen-hide and yielding to a crowded taskbar are app policy, and find_gap() gives you the facts to decide.
  • No cross-platform panels. Windows taskbar only.

Honest limitations

  • When the button strip can't be measured. The app-button edge is read from the taskbar's own ReBarWindow32 and MSTaskListWClass windows. On stock Windows 11 (build 26200) those exist and track the buttons in both left-aligned and centred layouts, measured here both ways, so the common cases are the measured ones. Where a shell doesn't host them, find_gap() falls back to assuming the buttons end 30% across the bar, and sets measured=False. Be clear-eyed about that fallback. It is a guess inherited from the app this code was extracted from, it has never run on a real machine, and it can name a left edge that still has buttons on it. Check Gap.measured if covering a button would matter to you.
  • Rects are sanity-checked, so detection degrades rather than lies. A taskbar child reporting a dead or off-bar rectangle, which happens while Explorer is restarting, is ignored rather than believed, and you get the heuristic with measured=False. A plausible edge that leaves no room is respected: a genuinely full taskbar returns None, because inventing a gap there would cover buttons.
  • Primary taskbar only. Secondary monitors get their own Shell_SecondaryTrayWnd bars, and v0.1 reads the primary Shell_TrayWnd. The gap you get is on whichever monitor that taskbar is on, at that monitor's DPI.
  • Horizontal taskbars only. A taskbar docked left or right, which Windows 10 allows, has no horizontal gap worth speaking of, so find_gap() returns None rather than guess.
  • The watcher needs your app to pump messages. It posts its moves rather than sending them, so a busy UI thread can never block it. The flip side is that a re-fit only lands the next time your app processes messages. Normal GUI apps do that constantly, but a wedged one won't move.
  • Auto-hide taskbars are not special-cased. You get the gap of the bar wherever it currently is, mid-slide included.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taskbargap-0.1.0.tar.gz (30.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taskbargap-0.1.0-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file taskbargap-0.1.0.tar.gz.

File metadata

  • Download URL: taskbargap-0.1.0.tar.gz
  • Upload date:
  • Size: 30.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for taskbargap-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5a35b0b1f25da092b690190e0f403915e0f5ba10e1afd741e16b191237417096
MD5 150556d201623319e3e811c471b1e15c
BLAKE2b-256 ae89ff972ee637c7efb88c308684b7d58349d6bad65aae3468eb9610adab01c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for taskbargap-0.1.0.tar.gz:

Publisher: publish.yml on paone9/taskbargap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file taskbargap-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: taskbargap-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for taskbargap-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38a7bfc4c7e5827faea93a4d6501b697bc9a5b563a6c79a4a277c0b940ed43d3
MD5 febaa9f4d91db0f378a476f9dcc84c28
BLAKE2b-256 afd8ab14b1f8afc3dd4306a857f5117ad7a38336e6fca0aed785efb9f0214f01

See more details on using hashes here.

Provenance

The following attestation bundles were made for taskbargap-0.1.0-py3-none-any.whl:

Publisher: publish.yml on paone9/taskbargap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page