What happened

Simple Account Balancer remembers where you left its window and puts it back next launch. The first version read a position, stored it, and passed it back in on the next start. It looked right on one monitor at 100% scaling and went wrong everywhere else: the window came back on the primary monitor instead of the one it was closed on, dropped lower by about a title-bar’s height each launch, and slid sideways on displays scaled above 100%. Open a second copy and the two stacked on top of each other instead of each keeping its spot.

The same bug was fixed the same way in five tools that share this window code: Simple Account Balancer, Simple Firearm Logbook, Simple RCON Tool, Simple SFTP Client, and Simple SFTP Audit Tool. That is why it is written up as a shared lesson rather than one tool’s quirk.

The artifact

This bug throws no error; it shows only as the window in the wrong place. The clearest statement of the mechanism is the code comment left where it was fixed:

Do NOT pass x/y into create_window and do NOT use window.move: pywebview’s Qt backend applies those pre-show and relative to the primary screen, so the window lands on the wrong monitor, drifts down by the title-bar height each launch, and slides sideways at non-100% scaling.

Root cause

A window has two rectangles that do not match. The frame rectangle is the whole window including its title bar and borders; the client rectangle is only the drawable area inside them. They differ by the title-bar height and the border width, and that difference changes with the display’s scaling.

The tools use pywebview with its Qt backend. Passing a saved position back through the backend, by handing coordinates to the window on creation or calling its move, applies them before the window is shown and treats them as relative to the primary screen. So the spot is measured against one rectangle and one monitor on the way out, and reapplied against a different rectangle and the wrong monitor on the way in. The rectangle gap makes the window creep down, the primary-screen assumption sends it to the wrong monitor, and above 100% scaling the pixel math no longer lines up, so it drifts sideways as well.

The fix

Stop going through the backend and talk to Windows directly. On close, read the absolute frame rectangle with the Win32 GetWindowRect, in physical pixels. On the next launch, after the OS window actually exists, put it back with SetWindowPos. Both calls speak the same frame-based, physical-pixel coordinate space, so the rectangle round-trips exactly at any scaling or monitor layout:

r = wintypes.RECT()
if not u.GetWindowRect(hwnd, ctypes.byref(r)):
    return
x, y, w, h = r.left, r.top, r.right - r.left, r.bottom - r.top

The multi-instance stacking came from finding the window by its title. FindWindowW matches a title across the whole desktop, so with a second copy open it could return the other copy’s window. The fix enumerates top-level windows and keeps only one owned by this process:

own_pid = os.getpid()
def _callback(hwnd, lparam):
    u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
    if pid.value != own_pid:
        return True   # not ours, keep looking

Two guards keep a stored position from doing harm. A minimized window reports a position near -32000, which is not saved as a real spot. And before reapplying a saved position, a point inside its title bar is checked against the connected monitors with MonitorFromPoint; if that monitor was unplugged since last launch, the position is skipped and the window opens at the default place instead of off-screen.

Evidence boundary

This is Windows-only and depends on pywebview’s Qt backend behaving as the comment describes; it is not a claim about other backends or operating systems. Saving and restoring the frame rectangle fixes the position round-trip. It does not resize a window to fit a monitor that shrank, beyond skipping a position whose monitor is gone. Every geometry call is wrapped so a failure can never block the window from opening or the app from closing: the cost of a failed save or restore is a forgotten position, not a broken app.

Source ledger

Source owner Primary URL Review class Last reviewed Exact claim or evidence mapped
JDE-Projects Simple Account Balancer https://github.com/JDE-Projects/Simple-Account-Balancer/commit/e94b3090bb8858b2d4f4804d6eae56051bb29e15 Production evidence August 19, 2026 The commit that replaced backend placement with the Win32 frame-rect save and restore.
JDE-Projects Simple Account Balancer https://github.com/JDE-Projects/Simple-Account-Balancer/blob/1100f2daa7e16533cdf881b3f92cffe911a889ac/simple_account_balancer.py Production evidence August 19, 2026 The frame-rect handler, the process-owned window lookup, the minimized and unplugged-monitor guards, and the quoted comment.
JDE-Projects Simple Firearm Logbook https://github.com/JDE-Projects/Simple-Firearm-Logbook/blob/b044eb004f86174c592d60875c18fe1d657b8a72/simple_firearm_logbook.py Production evidence August 19, 2026 The same frame-rect save and restore shipped in a second tool.
JDE-Projects Simple RCON Tool https://github.com/JDE-Projects/Simple-RCON-Tool/blob/63b5996325edbb43fbf4fdd41369a24611416284/simple_rcon_tool.py Production evidence August 19, 2026 The same frame-rect save and restore shipped in a third tool.
JDE-Projects Simple SFTP Client https://github.com/JDE-Projects/Simple-SFTP-Client/blob/521ba02aafaf4286187a37e112f546b39ef62996/simple_sftp_client.py Production evidence August 19, 2026 The same frame-rect save and restore shipped in a fourth tool.
JDE-Projects Simple SFTP Audit Tool https://github.com/JDE-Projects/Simple-SFTP-Audit-Tool/blob/aec9f0b62ed999c74dac6e096e7cd9247a3cdf61/simple_sftp_audit_tool.py Production evidence August 19, 2026 The same frame-rect save and restore shipped in a fifth tool.