Skip to content

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 SHBrowseForFolderW dialog 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/moveTo permissions.

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; use wcsstr for 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_title appearing in the browser title bar.
  • EnumWindows prefix 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 CoUninitialize on exceptional thread exit is a theoretical COM leak.

9. Next steps

  • Test more browsers: Firefox, Opera, Brave.
  • Consider WebView2 if Chrome tightens SetWindowPos restrictions.
  • Test high-DPI (150%, 200% scaling).
  • Make 500×420 configurable (e.g. via config.ini).
  • Add GetClassNameW check to EnumWindows for match precision.
  • Try SWP_NOSENDCHANGING to reduce browser re-layout flicker.
  • Add automated UI tests.
  • Log when window lookup times out.