Settings UI Position, Size & Folder Picker Design
Design notes for compacting the settings UI window, locking its size, centring it on screen, and centring the folder-browse dialog.
1. Requirement
The virtual scanner's settings UI is a local browser page launched via ShellExecuteA. It needs the following UI behaviour constraints:
Main requirements:
- Compact layout: only controls are visible, no excess whitespace; fixed 460px content width.
- Window centred: the browser window opens centred on screen.
- Fixed window size: user cannot drag borders or click the maximise button.
- Folder picker centred: the
SHBrowseForFolderWdialog is also centred. - Identical behaviour on 32-bit and 64-bit DS across common resolutions.
- Compatible with Chrome / Edge / Firefox; does not rely on JS
resizeTo/moveTopermissions.
Non-functional:
- No new dependencies.
- Changes limited to
settings_server.cpp. - Existing functionality (i18n, show/hide controls, form submit) preserved.
2. Domain knowledge
2.1 ShellExecuteA opens the browser
ShellExecuteA(nullptr, "open", url, ..., SW_SHOWNORMAL) starts the default browser. The DS cannot control initial position/size — the window appears where the browser last closed.
2.2 SetWindowPos
SetWindowPos(hwnd, hwndInsertAfter, x, y, cx, cy, flags) moves and sizes a window. To change style bits use GetWindowLongW(GWL_STYLE) + SetWindowLongW, then call SetWindowPos with SWP_FRAMECHANGED to repaint the non-client area.
2.3 WS_THICKFRAME and WS_MAXIMIZEBOX
WS_THICKFRAME: resizable border. Removing it gives a fixed-size dialog-like border.WS_MAXIMIZEBOX: maximise button. Removing it leaves only the close button.
2.4 FindWindowW and EnumWindows
FindWindowW(nullptr, title): exact title match.EnumWindows(callback, lparam): enumerates top-level windows; usewcsstrfor prefix matching as a fallback (browsers append " - Chrome" etc.).
2.5 SHBrowseForFolderW and BFFM_INITIALIZED
SHBrowseForFolderW(&BROWSEINFOW) opens a folder picker. The lpfn callback receives BFFM_INITIALIZED after the dialog is created, at which point SetWindowPos can centre it.
2.6 BIF_NEWDIALOGSTYLE layout-delay bug
The BIF_NEWDIALOGSTYLE dialog is not fully laid out at BFFM_INITIALIZED; GetWindowRect returns the pre-expansion size. Centring from this causes a large offset — at 1600×900 the OK button may be off-screen. Fix: use classic style (no BIF_NEWDIALOGSTYLE).
2.7 COM initialisation for SHBrowseForFolderW
SHBrowseForFolderW is a shell COM API. The calling thread must CoInitializeEx, or the API silently returns NULL. The serverThreadProc thread created by CreateThread lacks COM initialisation by default.
2.8 JS resizeTo/moveTo limitations
Modern browsers block scripts from resizing/moving windows not opened by script. JS-based centring is unreliable; the fix must happen in C++ with Win32.
3. Design goals
- Browser window found, centred, and locked within 3 s of
ShellExecuteA. - Folder picker always centred and fully visible at all resolutions.
- Compact CSS as a fallback if the browser rejects the size change.
- All logic in
settings_server.cpp.
Non-goals: blocking OS shortcuts (Win+↑), embedding a browser control, custom themes.
4. Overall design
showSettingsUi()
├── 1. HTTP server thread (CoInitialize + serverThreadProc)
├── 2. ShellExecuteA("open", url)
├── 3. Poll for browser HWND (≤ 3 s, every 100 ms)
│ ├── FindWindowW (exact title)
│ ├── EnumWindows + wcsstr (prefix fallback)
│ └── Found:
│ ├── SetWindowPos centre + 500×420
│ ├── Get/SetWindowLongW remove WS_THICKFRAME | WS_MAXIMIZEBOX
│ └── SetWindowPos + SWP_FRAMECHANGED
├── 4. WaitForSingleObject (≤ 60 s)
└── 5. Cleanup
serverThreadProc()
├── CoInitializeEx
├── accept() loop → / /index /browse /submit
│ └── /browse:
│ ├── BIF_RETURNONLYFSDIRS (classic)
│ ├── lpfn → BFFM_INITIALIZED centre
│ └── SHBrowseForFolderW → UTF-8 path
└── CoUninitialize
5. Key decisions and rationale
5.1 Win32 SetWindowPos centring (not JS)
JS resizeTo/moveTo is blocked by modern browsers for non-script-opened windows. Win32 is 100% reliable.
5.2 Remove WS_THICKFRAME | WS_MAXIMIZEBOX via SetWindowLongW
Content is fixed 460px; resizing only adds whitespace. Removing these style bits gives a dialog-like fixed-size border.
5.3 Classic folder dialog (no BIF_NEWDIALOGSTYLE)
BIF_NEWDIALOGSTYLE returns pre-expansion rect at BFFM_INITIALIZED, causing centring offset. Classic style has a fixed size, so centring is accurate.
5.4 Dual-path browser window lookup
FindWindowW for exact match; EnumWindows + wcsstr for prefix fallback (browsers append suffixes). Highly specific app title avoids false matches.
5.5 Poll up to 3 s
Browser startup usually 200–800 ms; 3 s covers cold starts. Timeout leaves the window at its last position — still functional, just not centred.
5.6 CoInitializeEx in serverThreadProc
SHBrowseForFolderW is a COM API; without CoInitializeEx it returns NULL silently (Browse button no-op). Reproduced and fixed.
5.7 Compact CSS: fixed 460px body + overflow:hidden
460px fits the longest row exactly. overflow:hidden prevents scrollbars. Acts as a fallback even if the browser ignores SetWindowPos.
6. Component changes
6.1 src/settings_server.cpp
The only file changed:
| Area | Change |
|---|---|
buildHtmlPage() CSS |
body{width:460px}, html,body{overflow:hidden}, tighter margins/padding/font-sizes (see table in §6.3 of the Chinese section) |
showSettingsUi() |
~30-line poll + find + centre + lock-size block after ShellExecuteA |
showSettingsUi() |
GetWindowLongW/SetWindowLongW + SWP_FRAMECHANGED |
serverThreadProc() |
CoInitializeEx at entry, CoUninitialize at exit |
/browse handler |
BIF_RETURNONLYFSDIRS only; BFFM_INITIALIZED centre in callback |
6.2 Untouched
settings_server.h, twain_data_source.cpp, localization.*, CMakeLists.txt, all other files.
7. Typical flows
7.1 Settings UI launch (ShowUI=TRUE)
1. showSettingsUi()
2. CoInitialize + HTTP server thread
3. ShellExecuteA("open", "http://127.0.0.1:xxxxx/")
4. Poll ≤ 3 s:
FindWindowW("BN Tech Virtual Scanner") — fails (browser suffix)
EnumWindows + wcsstr("BN Tech Virtual Scanner") — found
5. SetWindowPos centre + 500×420
6. Remove WS_THICKFRAME | WS_MAXIMIZEBOX + SWP_FRAMECHANGED
7. User interacts → submit → server thread ends
8. Cleanup
7.2 Folder picker (Browse button)
1. JS: GET /browse
2. BROWSEINFOW: ulFlags = BIF_RETURNONLYFSDIRS, lpfn centres at BFFM_INITIALIZED
3. SHBrowseForFolderW → user picks folder
4. UTF-8 path returned to JS → outputdir.value updated
8. Limitations
- OS shortcuts (
Win+↑) can still resize the window. - Title-based lookup relies on
app_titleappearing in the browser title bar. EnumWindowsprefix search has a tiny false-match risk.- Classic folder dialog lacks a tree pane (full functionality otherwise).
- CSS assumes max row width ≤ 460px; new wider controls need a width bump.
- Not tested with RTL languages.
- Missed
CoUninitializeon exceptional thread exit is a theoretical COM leak.
9. Next steps
- Test more browsers: Firefox, Opera, Brave.
- Consider WebView2 if Chrome tightens
SetWindowPosrestrictions. - Test high-DPI (150%, 200% scaling).
- Make 500×420 configurable (e.g. via
config.ini). - Add
GetClassNameWcheck toEnumWindowsfor match precision. - Try
SWP_NOSENDCHANGINGto reduce browser re-layout flicker. - Add automated UI tests.
- Log when window lookup times out.