quyennv.com

Senior DevOps Engineer · Healthcare, Fanance

Detecting…

Building a Lightweight Windows System Monitor Widget in C++

#windows#c++#systems#architecture#desktop#monitoring

0

This post walks through the architecture of a lightweight Windows system monitor widget (SysMonitor) that I built in C++. The goal is a transparent “macOS-style” dock widget for Windows that shows CPU, memory, disk, network, IP, and weather — with minimal CPU and RAM usage.

Instead of using heavy frameworks, the project is written with raw Win32 API and GDI+, and is packaged as a single executable that you can leave running all day.


source code: https://github.com/quyennguyenvan/mac-dock-widget

1. Goals and constraints

When you design a desktop widget like this, the constraints matter:

  • Very low CPU and memory footprint
    The widget should be “always on” without stealing noticeable resources from games, IDEs, or VMs.

  • No external dependencies
    Only the Windows SDK and system DLLs (no .NET runtime, Qt, Electron, etc.).

  • Always-visible but non-intrusive
    A transparent, topmost window that doesn’t clutter the taskbar or Alt+Tab list.

  • Single executable
    Easy to distribute, copy to a USB stick, or add to auto-start without an installer.

These goals drive the architecture: small C++ modules compiled into one Win32 GUI EXE.


2. Project structure

The repository is laid out to keep responsibilities clear:

SysMonitor/
  src/
    main.cpp                   # Entry point, WinMain, window procedure
  libs/
    common/                    # Common macros, typedefs, constants
    globals/                   # Global state (window handles, metrics, layout constants)
    cpu/, mem/, gpu/, disk/, net/
                                # Metric collectors per subsystem
    external/                  # Public IP, weather, HTTP helpers
    http/, json/               # Minimal HTTP client and JSON parser
    tray/                      # System tray icon and context menu
    gdip/                      # GDI+ initialisation and helper wrappers
    layout/                    # Layout calculations (widget and section widths)
    draw/                      # Rendering primitives and main Render() function
    tooltip/                   # Hover tooltips for per-core and volume info

The src/main.cpp file is intentionally small. It wires together these libraries instead of doing “real work” itself.


3. Window, event loop, and single instance

SysMonitor uses a classic Win32 message loop with a custom window class:

int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int) {
    if (!AcquireSingleInstance()) return 0;      // Single instance via mutex

    SetProcessDPIAware();
    g_hInst = hInst;

    // Register window class
    WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc);
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInst;
    wc.lpszClassName = WND_CLASS;
    wc.hCursor       = LoadCursor(nullptr, IDC_ARROW);
    RegisterClassExW(&wc);

    // Init subsystems (GDI+, metrics, network, etc.) ...
    // Create transparent, topmost window and tray icon ...

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Shutdown background thread and clean up ...
}

The widget window is created with:

  • WS_EX_LAYERED – per-pixel alpha blending (true transparency).
  • WS_EX_TOPMOST – always on top of normal app windows.
  • WS_EX_TOOLWINDOW – hidden from taskbar and Alt+Tab.

The window procedure (WndProc) is responsible for:

  • WM_CREATE – start a periodic timer.
  • WM_TIMER – refresh metrics and call Render().
  • WM_MOUSEMOVE / WM_MOUSELEAVE – track hover state and show/hide tooltips.
  • WM_TRAYICON – respond to right-click (context menu) and double-click (show/hide).
  • WM_COMMAND – handle tray menu commands (Show/Hide, Auto-start, Exit).
  • WM_DESTROY – stop the timer, remove tray icon, and quit.

A named mutex (SysMonitor_SingleInstance) prevents multiple instances from running at once.


4. Metric collectors: CPU, memory, disk, network, GPU

Each metric lives in its own small module (e.g. libs/cpu/cpu.cpp, libs/mem/mem.cpp). The header API is intentionally tiny:

  • InitCpu(), UpdateCpu()
  • UpdateMem()
  • UpdateDisk()
  • InitNet(), UpdateNet()
  • InitGpuD3dKmt(), UpdateGpu()

These modules:

  • Use low-level Windows APIs:
    • CPU: NtQuerySystemInformation for per-processor timing.
    • Memory: GlobalMemoryStatusEx.
    • Network: GetIfTable2 (IP Helper API) for byte counters.
    • Disk: disk I/O counters.
    • GPU: DXGI/DirectX where available.
  • Write results into shared structs defined in libs/globals/.

The WM_TIMER handler in main.cpp simply calls these Update* functions and then Render(); the UI thread never blocks on slow operations.


5. External data: public IP and weather

SysMonitor also displays:

  • Public IP address and geolocation (from ip-api.com).
  • Weather information (temperature and conditions) via Open-Meteo.

This is handled by three modules:

  • libs/external – high-level functions like UpdateLanIP() and weather fetch.
  • libs/http – tiny WinHTTP wrapper for GET requests.
  • libs/json – small JSON parser tailored to the API responses.

Architecture pattern:

  • A background thread (BgThread) wakes periodically, calls HTTP helpers, parses JSON, and updates global state (e.g. g_weather, g_publicIp).
  • The UI thread just reads the latest values when drawing — there are no blocking network calls in the timer handler or window procedure.

This keeps the widget smooth even on slow or unreliable networks.


6. Rendering pipeline with GDI+ and layered windows

Rendering is done with GDI+ plus a layered window for transparency:

  1. InitGdip() is called once at startup.
  2. On every timer tick (WM_TIMER), Render():
    • Creates an off-screen bitmap the size of the widget.
    • Fills a semi-transparent dark background.
    • Draws each section in sequence:
      • Clock + date.
      • CPU bars (total + per-core).
      • RAM/swap usage.
      • Disk and network throughput.
      • IP + weather info.
    • Uses color-coded bars to show utilisation levels.
  3. Calls UpdateLayeredWindow to blit the bitmap into the layered window with an alpha channel.

Layout is handled by libs/layout:

  • CalcCpuSecW() – width for the CPU section.
  • CalcDiskSecW() – width for the disk section.
  • CalcWidth() – total widget width based on which sections are active.

Drawing helpers live in libs/draw and include:

  • Text rendering with anti-aliasing.
  • Rounded rectangles and bars.
  • Icon/glyph drawing helpers.

Because metrics only write numbers into globals, you can tweak layout and styling without touching metric code.


7. Tray icon, context menu, and auto-start

The widget is controlled entirely via a system tray icon:

  • Implemented in libs/tray using Shell_NotifyIcon.
  • Right-click opens a context menu with:
    • Show/Hide Widget.
    • Start with Windows (toggle auto-start).
    • Exit.
  • Double-click toggles visibility (show/hide) quickly.

Auto-start is implemented using the Run key under HKCU:

  • On toggle, SysMonitor adds or removes itself from
    HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run.
  • Helper functions live in libs/external / libs/util.

Because the main window uses WS_EX_TOOLWINDOW, the app never shows a taskbar button — the tray icon is the only entry point.


8. Tooltips and interaction

To make the tiny widget informative without clutter:

  • The tooltip system (libs/tooltip) computes which CPU core bar or volume segment the mouse is over using simple hit-tests (HitTestCore, HitTestVol).
  • On hover:
    • UpdateTip() shows a small tooltip near the cursor with detailed numbers (e.g. per-core usage).
    • As the mouse moves, the code re-renders only the tooltip content.
  • When the cursor leaves the widget (WM_MOUSELEAVE), HideTip() removes the tooltip.

Tooltips use the same GDI+ and layered-window technique, so visually they match the main widget.


9. Build setup and portability

SysMonitor is built as a single Win32 GUI executable using CMake:

  • CMakeLists.txt lists all libs/*/*.cpp sources and links against:
    • user32, gdi32, gdiplus, shell32, iphlpapi, winhttp, advapi32, ole32, comctl32, dxgi.
  • Compiles with C++17, -O2 or /O2 optimisations, and standard warning levels.
  • build.bat is a convenience entry-point for:
    • Visual Studio / MSVC.
    • MinGW-w64 via MSYS2.

There is also a separate mac.main.cpp plus libs/mac/* for a macOS port using native APIs, but this particular project primarily targets Windows.


10. Ideas for extending the widget

Some directions you can take this architecture:

  • Configurable sections – toggle weather, IP, or other modules on/off via a config file or tray menu.
  • Themes – light/dark modes, accent colors, or “compact” vs “detailed” layouts.
  • Clickable actions – e.g. clicking CPU opens Task Manager, clicking disk opens Resource Monitor, clicking network opens your router UI.
  • Alerts – highlight sections or show warning icons when CPU/memory/disk or network utilisation stays high for a configurable period.
  • Plugin-style metrics – additional libs/* modules (e.g. temperatures, battery, Wi-Fi strength) following the same Init/Update pattern.

Because each concern lives in its own small module (cpu, mem, net, draw, layout, tray, tooltip), the codebase stays approachable and hackable, even as you add new features.

← All posts

Comments