Building a Lightweight Windows System Monitor Widget in C++
#windows#c++#systems#architecture#desktop#monitoring
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 callRender().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:
NtQuerySystemInformationfor per-processor timing. - Memory:
GlobalMemoryStatusEx. - Network:
GetIfTable2(IP Helper API) for byte counters. - Disk: disk I/O counters.
- GPU: DXGI/DirectX where available.
- CPU:
- 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 likeUpdateLanIP()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:
InitGdip()is called once at startup.- 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.
- Calls
UpdateLayeredWindowto 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/trayusingShell_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.txtlists alllibs/*/*.cppsources and links against:user32,gdi32,gdiplus,shell32,iphlpapi,winhttp,advapi32,ole32,comctl32,dxgi.
- Compiles with C++17,
-O2or/O2optimisations, and standard warning levels. build.batis 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.
Comments