Przeglądarka w jednym pliku. 240 KB, nowoczesny silnik.

Do uruchamiania modułów webowych na hali, w kiosku albo obok starej aplikacji desktopowej nie potrzebujesz Electrona. Wystarczy cienki program na WebView2, który bierze parametry z linii komend. Pokazujemy, kiedy to ma sens, dlaczego ten silnik, na jakich Windowsach zadziała i cały kod w jednym pliku.

Autor: · Ostatnia aktualizacja:

243 200 bajtów jeden plik .exe, bez DLL obok (MinGW, -Os, statycznie)
Silnik z systemu WebView2 aktualizowany przez Microsoft
Windows 7–11 na 7 i 8.1 z runtime 109, bez poprawek

Najkrócej: prompt, który zrobi z tej strony plik .exe.

Wklej go do Claude Code albo innego agenta w terminalu, w pustym folderze. Agent pobierze kod z tej strony, nagłówek WebView2 z NuGet, skompiluje i sprawdzi plik. Działa na macOS, Linux i Windows; przed instalacją kompilatora zapyta o zgodę.

Zbuduj mi plik szron-view.exe: mini-przeglądarkę dla Windows według poradnika https://szron.tech/mini-przegladarka/

Ustawienia (zmień, jeśli trzeba):
- architektura: x64 (x86 tylko dla starych 32-bitowych Windowsów)
- folder roboczy: szron-view w bieżącym katalogu

Kroki:
1. Przeczytaj poradnik: opcje linii komend oraz sekcje „Kompilacja” i „Moduły webowe i pułapki”.
2. Kod pobierz dokładnie, bez streszczania (curl, nie narzędzie do czytania stron):
   curl -fsSL https://szron.tech/mini-przegladarka/szron-view.cc -o szron-view.cc
   Sprawdź, że plik zaczyna się od „// szron-view.cc” i ma około 300 linii.
3. Ustal system i kompilator:
   - macOS: MinGW-w64 z Homebrew (brew install mingw-w64)
   - Linux: pakiet g++-mingw-w64-x86-64 (x86: g++-mingw-w64-i686)
   - Windows: jeśli działa cl (Developer Command Prompt z Visual Studio), użyj wariantu Visual Studio; inaczej MinGW-w64 z MSYS2 (pakiet mingw-w64-ucrt-x86_64-gcc)
   Zanim cokolwiek zainstalujesz, pokaż mi komendę i poczekaj na zgodę.
4. Pobierz nagłówek WebView2 (paczka NuGet to zwykły zip):
   curl -fsSL -o wv2.zip https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.4314.35
   Rozpakuj do wv2/ i skopiuj wv2/build/native/include/WebView2.h do include/.
   Dla Visual Studio weź też wv2/build/native/x64/WebView2LoaderStatic.lib (x86: build/native/x86).
5. Skompiluj.
   MinGW x64 (pod MSYS2 kompilator nazywa się g++, dla x86 i686-w64-mingw32-g++):
   x86_64-w64-mingw32-g++ -std=c++17 -Os -s -static -mwindows -municode szron-view.cc -Iinclude -lole32 -luuid -lshell32 -luser32 -ladvapi32 -o szron-view.exe
   Visual Studio:
   cl /std:c++17 /O1 /MT /EHsc szron-view.cc /Iinclude WebView2LoaderStatic.lib ole32.lib shell32.lib user32.lib advapi32.lib version.lib /link /SUBSYSTEM:WINDOWS
6. Sprawdź wynik: rozmiar (MinGW x64 to około 240 KB), typ pliku (PE32+ z GUI dla x64) i importy (objdump -p albo dumpbin /imports). Dozwolone: KERNEL32, ole32, SHELL32, USER32, ADVAPI32 i api-ms-win-crt-*. Obok .exe nie może być potrzebny WebView2Loader.dll.
7. Na Windows uruchom test: szron-view.exe https://szron.tech --width=1024 i zapytaj mnie, czy okno się otworzyło. Na macOS i Linux niczego nie uruchamiaj.
8. Nie zmieniaj kodu. Jeśli kompilacja padnie, pokaż błąd i proponowaną poprawkę, zanim ją wprowadzisz.

Na koniec podaj: pełną ścieżkę do .exe, rozmiar w bajtach, wersję kompilatora i trzy przykładowe komendy uruchomienia z poradnika.

Kod leży też jako surowy plik: szron-view.cc. Agent pobiera go curl-em, bo narzędzia do czytania stron streszczają treść i potrafią zgubić fragment kodu. Wynik sprawdziliśmy kompilacją MinGW na macOS; uruchomienie zostaje po Twojej stronie, na Windows.

Kiedy potrzebujesz własnej mini-przeglądarki.

Panel na hali, kiosk, ekran informacyjny --kiosk
Jedna strona na pełnym ekranie, bez paska adresu, menu i skrótów. Start z Autostartu albo z harmonogramu zadań z gotowymi parametrami.
Moduł webowy w starej aplikacji Xbase++, C++Builder, Delphi
Nowy ekran piszesz w HTML i JavaScripcie, a stary program otwiera go jednym ShellExecute z parametrami. To dobra pierwsza faza modernizacji: nowy moduł działa, zanim przepiszecie resztę (zobacz plugin code-modernization).
Moduł z lokalnego folderu --dir
Raport, konfigurator, instrukcja z animacjami. Moduły ES, fetch i Web Workery nie działają z file://, a tu folder dostaje adres https://app.local/ bez serwera.
Kiedy nie Wystarczy Edge
Jednorazowy pokaz albo test: msedge.exe --app=https://adres --window-size=1280,800 otwiera okno bez paska adresu, a --kiosk https://adres --edge-kiosk-type=fullscreen robi kiosk. Zero kilobajtów do wdrożenia.

Dlaczego nie po prostu Edge

  • Edge nie jest gwarantowany. Microsoft sam pisze, że w firmach i szkołach przeglądarki bywa brak albo admin blokuje jej aktualizacje. Runtime WebView2 aktualizuje się osobno.
  • Własny profil i własne zasady. Mini-przeglądarka nie dzieli ciasteczek, rozszerzeń i polityk z przeglądarką użytkownika.
  • Most do aplikacji. Strona wysyła window.chrome.webview.postMessage("close") i program się zamyka. Tak samo dodasz drukowanie, port szeregowy czy zapis pliku.
  • Jeden plik z Twoimi parametrami. Kopiujesz .exe na stanowisko i gotowe, bez instalatora.

Który silnik: rachunek wielkości i zgodności.

Mały plik i pełna zgodność z nowym webem to sprzeczne wymagania, o ile silnik jedzie w paczce. Na Windows da się je pogodzić tylko w jeden sposób: silnik bierzemy z systemu. Silniki spoza Chromium sprawdziliśmy według stanu na październik 2026.

PodejścieCo wozisz ze sobąZgodność z nowym webemWerdykt
Cienki host na WebView2 (ten poradnik)ok. 240 KBChromium w wersji Edge Stable, aktualizowany przez MicrosoftTak
msedge --app / --kiosk0 KBPełnaDo pokazów i testów
WebView2 Fixed Version, CEF, ElectronFixed Version to ponad 250 MB, reszta podobniePełna, ale wersja zamrożonaTylko gdy musisz zamrozić wersję
SciterDLL 5+ MBWłasny silnik: CSS 2.1 i część CSS3, własny skryptNie dla nowych modułów
ServoBiblioteka Rust od wersji 0.1.0 (kwiecień 2026)Rośnie co miesiąc, API jeszcze się zmieniaZa wcześnie
Ladybird—Alfa w 2026 tylko na Linux i macOS, Windows nieoficjalnieZa wcześnie
Kontrolka WebBrowser (IE)0 KBSilnik IE11: bez modułów ES i nowego CSSNie

WebView2 to Chromium, ale nie powiększa Twojego pliku: współdzielony runtime jest już w Windows 11 i na zdecydowanej większości Windows 10, a przy tej samej wersji jego pliki są twardo linkowane z Edge. Tego samego nie da żaden silnik spoza Chromium, który dziś mieści się w kilku megabajtach.

Na jakich Windowsach to ruszy.

Windows 11 Runtime w systemie
Działa od ręki. Runtime aktualizuje się sam, razem z poprawkami bezpieczeństwa.
Windows 10 Wsparcie co najmniej do października 2028
Runtime jest prawie wszędzie. Microsoft obiecuje aktualizacje Edge i WebView2 na 22H2 co najmniej do października 2028, bez wykupywania ESU.
Windows 7, 8.1, Server 2012 R2 Ostatnia wersja 109
Edge i WebView2 109 to ostatnie wersje dla tych systemów (styczeń 2023). Działają, ale bez poprawek bezpieczeństwa, a Fixed Version nowsza niż 109 się nie uruchomi. Nadaje się do zamkniętej sieci i własnych modułów, nie do otwartego internetu.
Brak runtime 2 MB i jedna komenda
Bootstrapper Microsoftu (ok. 2 MB) instaluje runtime po cichu: MicrosoftEdgeWebview2Setup.exe /silent /install. Bez internetu: Standalone Installer. Przeglądarka sama podpowiada to w komunikacie, gdy nie znajdzie runtime.

Sam plik .exe korzysta tylko z funkcji systemu dostępnych od Windows Vista i 7 (sprawdziliśmy tabelę importów). Przy kompilacji MinGW z UCRT stary Windows 7 potrzebuje jeszcze aktualizacji Universal C Runtime (KB2999226). Toolchain z msvcrt albo Visual Studio z /MT tej zależności nie mają.

Jak się tego używa: linia komend.

Program nazywa się szron-view.exe. Bez parametrów pokazuje okno z pomocą.

OpcjaCo robiPrzykład
adres | plik | folderCo otworzyć. Plik albo folder trafia na wirtualny host https://app.local/.https://panel.local
--dir=FOLDERFolder z modułem serwowany pod https://app.local/--dir=D:\hmi
--entry=PLIKPlik startowy w --dir (domyślnie index.html)--entry=linia3.html
--title=TEKSTTytuł okna (domyślnie tytuł strony)--title="Raport"
--width=N --height=NRozmiar okna (domyślnie 1280×800)--width=1024
--fullscreenPełny ekran bez ramki
--kioskPełny ekran bez menu kontekstowego, skrótów przeglądarki i nowych okien
--zoom=XPowiększenie treści--zoom=1.25
--devtoolsNarzędzia deweloperskie pod F12 (domyślnie wyłączone)
--profile=FOLDERCiasteczka, localStorage, IndexedDB (domyślnie %LOCALAPPDATA%\szron-view)--profile=D:\dane\hmi
--runtime=FOLDERWebView2 Fixed Version zamiast zainstalowanego--runtime=C:\wv2\109
--args="FLAGI"Dodatkowe flagi silnika Chromium--args="--disable-gpu"

Przykłady

:: kiosk z panelem z serwera w sieci firmowej
szron-view.exe https://panel.firma.local --kiosk

:: moduł z dysku, moduły ES i fetch działają
szron-view.exe C:\moduly\raport\index.html --title="Raport zmiany" --width=1024 --height=700

:: panel linii 3 na pełnym ekranie, większa czcionka
szron-view.exe --dir=D:\hmi --entry=linia3.html --fullscreen --zoom=1.25

:: praca nad modułem: F12 i autoodtwarzanie wideo
szron-view.exe --dir=. --devtools --args="--autoplay-policy=no-user-gesture-required"

:: Windows 7 z rozpakowanym runtime 109
szron-view.exe --runtime=C:\wv2\109 --dir=D:\hmi --kiosk

Ze starej aplikacji

// C++Builder, Delphi i każdy język, który woła WinAPI
ShellExecuteW(nullptr, L"open", L"szron-view.exe",
              L"--dir=raporty --entry=faktura.html --title=\"Faktura\"",
              nullptr, SW_SHOWNORMAL);

Ze strony do programu

// zamknij okno (np. przycisk „Gotowe” w module)
window.chrome.webview.postMessage("close");
// przełącz pełny ekran (poza trybem kiosku)
window.chrome.webview.postMessage("fullscreen");

Cały kod: jeden plik, 306 linii.

C++17, bez frameworka. Pięć części: opcje z linii komend, obiekty COM dla zdarzeń WebView2, loader runtime, okno Win32 oraz ustawienia z mostem do JavaScriptu.

Opcje parseArgs()
CommandLineToArgvW rozbija linię komend z cudzysłowami. Ścieżka do pliku albo folderu zamienia się na --dir i --entry. Profil domyślnie trafia do %LOCALAPPDATA%, bo folder obok .exe w Program Files jest tylko do odczytu.
Obiekty COM Handler<I, A…>
WebView2 jest asynchroniczny i woła callbacki COM. Jeden szablon zamienia lambdę w obiekt COM, więc nie potrzeba WRL, którego MinGW nie ma.
Loader createEnvironment()
Visual Studio linkuje oficjalny WebView2LoaderStatic.lib. Pod MinGW program sam czyta z rejestru, gdzie jest runtime, i ładuje EmbeddedBrowserWebView.dll. Tak samo robi biblioteka webview/webview. Dzięki temu obok .exe nie leży WebView2Loader.dll.
Okno wndProc(), setFullscreen()
Zwykłe okno Win32. Pełny ekran zdejmuje ramkę i rozciąga okno na monitor. DPI Per-Monitor v2 na Windows 10 1703+, starsze systemy dostają zwykły tryb DPI-aware.
Ustawienia i most onWebViewReady()
Wyłączone DevTools i pasek stanu, w kiosku też menu i skróty (ICoreWebView2Settings3). Folder trafia na https://app.local/ przez SetVirtualHostNameToFolderMapping. WebMessageReceived to most ze strony do programu.
Pokaż kod szron-view.cc
// szron-view.cc: jednoplikowa mini-przeglądarka na WebView2 (Windows 10/11, Windows 7/8.1 z runtime 109).
// MinGW: x86_64-w64-mingw32-g++ -std=c++17 -Os -s -static -mwindows -municode szron-view.cc -Iinclude -lole32 -luuid -lshell32 -luser32 -ladvapi32 -o szron-view.exe
// MSVC:  cl /std:c++17 /O1 /MT /EHsc szron-view.cc /Iinclude WebView2LoaderStatic.lib ole32.lib shell32.lib user32.lib advapi32.lib version.lib /link /SUBSYSTEM:WINDOWS
#ifndef UNICODE
#define UNICODE
#endif
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <functional>
#include <string>
#include "WebView2.h"

// --- opcje z linii komend ---------------------------------------------------
struct Opts {
  std::wstring url, dir, entry = L"index.html", title, profile, runtime, args;
  int width = 1280, height = 800;
  double zoom = 1.0;
  bool fullscreen = false, kiosk = false, devtools = false;
} o;

static const wchar_t *USAGE =
    L"szron-view [adres | plik.html | folder] [opcje]\n\n"
    L"--dir=FOLDER        serwuj folder pod https://app.local/\n"
    L"--entry=PLIK        plik startowy w --dir (index.html)\n"
    L"--title=TEKST       tytuł okna (domyślnie tytuł strony)\n"
    L"--width=N --height=N  rozmiar okna (1280x800)\n"
    L"--fullscreen        pełny ekran bez ramki\n"
    L"--kiosk             pełny ekran, bez menu, skrótów i nowych okien\n"
    L"--zoom=1.25         powiększenie\n"
    L"--devtools          włącz narzędzia deweloperskie (F12)\n"
    L"--profile=FOLDER    dane przeglądarki (domyślnie %LOCALAPPDATA%\\szron-view)\n"
    L"--runtime=FOLDER    WebView2 Fixed Version zamiast zainstalowanego\n"
    L"--args=\"FLAGI\"      dodatkowe flagi silnika, np. --autoplay-policy=no-user-gesture-required";

static bool parseArgs() {
  int argc = 0;
  wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
  for (int i = 1; i < argc; ++i) {
    std::wstring a = argv[i], k = a, v;
    if (auto eq = a.find(L'='); a.rfind(L"--", 0) == 0 && eq != std::wstring::npos) { k = a.substr(0, eq); v = a.substr(eq + 1); }
    if (k == L"--dir") o.dir = v;
    else if (k == L"--entry") o.entry = v;
    else if (k == L"--title") o.title = v;
    else if (k == L"--width") o.width = _wtoi(v.c_str());
    else if (k == L"--height") o.height = _wtoi(v.c_str());
    else if (k == L"--zoom") o.zoom = _wtof(v.c_str());
    else if (k == L"--profile") o.profile = v;
    else if (k == L"--runtime") o.runtime = v;
    else if (k == L"--args") o.args = v;
    else if (k == L"--fullscreen") o.fullscreen = true;
    else if (k == L"--kiosk") o.kiosk = o.fullscreen = true;
    else if (k == L"--devtools") o.devtools = true;
    else if (a.rfind(L"--", 0) != 0) o.url = a;
    else { LocalFree(argv); return false; }
  }
  LocalFree(argv);
  // ścieżka zamiast adresu: folder albo plik serwujemy przez wirtualny host (moduły ES, fetch, CORS działają)
  if (!o.url.empty() && o.url.find(L"://") == std::wstring::npos) {
    wchar_t full[MAX_PATH];
    GetFullPathNameW(o.url.c_str(), MAX_PATH, full, nullptr);
    DWORD attr = GetFileAttributesW(full);
    if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) o.dir = full;
    else { std::wstring f = full; auto s = f.find_last_of(L'\\'); o.dir = f.substr(0, s); o.entry = f.substr(s + 1); }
    o.url.clear();
  }
  if (o.url.empty() && o.dir.empty()) return false;  // bez adresu: pokaż pomoc
  if (o.profile.empty()) {
    wchar_t base[MAX_PATH];
    SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, base);
    o.profile = std::wstring(base) + L"\\szron-view";
  }
  return true;
}

// --- minimalne obiekty COM dla callbacków WebView2 ---------------------------
template <class I, class... A>
struct Handler final : I {
  IID iid;
  std::function<HRESULT(A...)> fn;
  ULONG refs = 1;
  Handler(const IID &id, std::function<HRESULT(A...)> f) : iid(id), fn(std::move(f)) {}
  HRESULT STDMETHODCALLTYPE QueryInterface(REFIID r, void **p) override {
    if (r == IID_IUnknown || r == iid) { *p = this; AddRef(); return S_OK; }
    *p = nullptr; return E_NOINTERFACE;
  }
  ULONG STDMETHODCALLTYPE AddRef() override { return ++refs; }
  ULONG STDMETHODCALLTYPE Release() override { ULONG r = --refs; if (!r) delete this; return r; }
  HRESULT STDMETHODCALLTYPE Invoke(A... a) override { return fn(a...); }
};

static const IID IID_EnvCompleted = {0x4e8a3389, 0xc9d8, 0x4bd2, {0xb6, 0xb5, 0x12, 0x4f, 0xee, 0x6c, 0xc1, 0x4d}};
static const IID IID_CtlCompleted = {0x6c4819f3, 0xc9b7, 0x4260, {0x81, 0x27, 0xc9, 0xf5, 0xbd, 0xe7, 0xf6, 0x8c}};
static const IID IID_WebMessage = {0x57213f19, 0x00e6, 0x49fa, {0x8e, 0x07, 0x89, 0x8e, 0xa0, 0x1e, 0xcb, 0xd2}};
static const IID IID_TitleChanged = {0xf5f2b923, 0x953e, 0x4042, {0x9f, 0x95, 0xf3, 0xa1, 0x18, 0xe1, 0xaf, 0xd4}};
static const IID IID_NewWindow = {0xd4c185fe, 0xc81c, 0x4989, {0x97, 0xaf, 0x2d, 0x3f, 0xa7, 0xab, 0x56, 0x51}};
static const IID IID_EnvOptions = {0x2fde08a8, 0x1e9a, 0x4766, {0x8c, 0x05, 0x95, 0xa9, 0xce, 0xb9, 0xd1, 0xc5}};
static const IID IID_WebView2_3 = {0xa0d6df20, 0x3b92, 0x416d, {0xaa, 0x0c, 0x43, 0x7a, 0x9c, 0x72, 0x78, 0x57}};
static const IID IID_Settings3 = {0xfdb5ab74, 0xaf33, 0x4854, {0x84, 0xf0, 0x0a, 0x63, 0x1d, 0xeb, 0x5e, 0xba}};

static HRESULT dupStr(const std::wstring &s, LPWSTR *out) {
  *out = static_cast<LPWSTR>(CoTaskMemAlloc((s.size() + 1) * sizeof(wchar_t)));
  if (!*out) return E_OUTOFMEMORY;
  memcpy(*out, s.c_str(), (s.size() + 1) * sizeof(wchar_t));
  return S_OK;
}

// opcje środowiska: tylko dodatkowe flagi silnika z --args
struct EnvOptions final : ICoreWebView2EnvironmentOptions {
  ULONG refs = 1;
  HRESULT STDMETHODCALLTYPE QueryInterface(REFIID r, void **p) override {
    if (r == IID_IUnknown || r == IID_EnvOptions) { *p = this; AddRef(); return S_OK; }
    *p = nullptr; return E_NOINTERFACE;
  }
  ULONG STDMETHODCALLTYPE AddRef() override { return ++refs; }
  ULONG STDMETHODCALLTYPE Release() override { ULONG r = --refs; if (!r) delete this; return r; }
  HRESULT STDMETHODCALLTYPE get_AdditionalBrowserArguments(LPWSTR *v) override { return dupStr(o.args, v); }
  HRESULT STDMETHODCALLTYPE put_AdditionalBrowserArguments(LPCWSTR) override { return S_OK; }
  HRESULT STDMETHODCALLTYPE get_Language(LPWSTR *v) override { return dupStr(L"", v); }
  HRESULT STDMETHODCALLTYPE put_Language(LPCWSTR) override { return S_OK; }
  HRESULT STDMETHODCALLTYPE get_TargetCompatibleBrowserVersion(LPWSTR *v) override { return dupStr(L"86.0.616.0", v); }
  HRESULT STDMETHODCALLTYPE put_TargetCompatibleBrowserVersion(LPCWSTR) override { return S_OK; }
  HRESULT STDMETHODCALLTYPE get_AllowSingleSignOnUsingOSPrimaryAccount(BOOL *v) override { *v = FALSE; return S_OK; }
  HRESULT STDMETHODCALLTYPE put_AllowSingleSignOnUsingOSPrimaryAccount(BOOL) override { return S_OK; }
};

// --- loader: MSVC linkuje oficjalny WebView2LoaderStatic.lib, MinGW szuka runtime sam ---------
static HRESULT createEnvironment(ICoreWebView2EnvironmentOptions *opts, ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *done) {
#ifdef _MSC_VER
  return CreateCoreWebView2EnvironmentWithOptions(o.runtime.empty() ? nullptr : o.runtime.c_str(), o.profile.c_str(), opts, done);
#else
  std::wstring base = o.runtime;
  int runtimeType = 1;  // 1 = runtime z folderu (Fixed Version), 0 = zainstalowany (Evergreen)
  if (base.empty()) {
    runtimeType = 0;
    const wchar_t *key = L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
    for (HKEY root : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) {
      wchar_t path[MAX_PATH];
      DWORD size = sizeof(path);
      if (RegGetValueW(root, key, L"EBWebView", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6432KEY, nullptr, path, &size) == ERROR_SUCCESS) { base = path; break; }
    }
    if (base.empty()) return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
  }
#if defined(__x86_64__)
  std::wstring dll = base + L"\\EBWebView\\x64\\EmbeddedBrowserWebView.dll";
#elif defined(__aarch64__)
  std::wstring dll = base + L"\\EBWebView\\arm64\\EmbeddedBrowserWebView.dll";
#else
  std::wstring dll = base + L"\\EBWebView\\x86\\EmbeddedBrowserWebView.dll";
#endif
  HMODULE lib = LoadLibraryW(dll.c_str());
  if (!lib) return HRESULT_FROM_WIN32(GetLastError());
  using CreateFn = HRESULT(STDMETHODCALLTYPE *)(bool, int, PCWSTR, IUnknown *, ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *);
  auto create = reinterpret_cast<CreateFn>(reinterpret_cast<void *>(GetProcAddress(lib, "CreateWebViewEnvironmentWithOptionsInternal")));
  return create ? create(true, runtimeType, o.profile.c_str(), opts, done) : E_NOINTERFACE;
#endif
}

// --- okno ---------------------------------------------------------------------
static HWND hwnd;
static ICoreWebView2Controller *controller;
static ICoreWebView2 *webview;
static WINDOWPLACEMENT windowed = {sizeof(WINDOWPLACEMENT)};

static void setFullscreen(bool on) {
  DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
  if (on) {
    MONITORINFO mi = {sizeof(mi)};
    GetWindowPlacement(hwnd, &windowed);
    GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &mi);
    SetWindowLongW(hwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);
    SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
                 mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
  } else {
    SetWindowLongW(hwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);
    SetWindowPlacement(hwnd, &windowed);
    SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
  }
  o.fullscreen = on;
}

static LRESULT CALLBACK wndProc(HWND h, UINT msg, WPARAM wp, LPARAM lp) {
  switch (msg) {
    case WM_SIZE:
      if (controller) { RECT r; GetClientRect(h, &r); controller->put_Bounds(r); }
      return 0;
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }
  return DefWindowProcW(h, msg, wp, lp);
}

static void fail(const wchar_t *what, HRESULT hr) {
  wchar_t text[512];
  wsprintfW(text, L"%s (0x%08X).\n\nBrak WebView2 Runtime? Zainstaluj go z developer.microsoft.com/microsoft-edge/webview2 "
                  L"albo wskaż folder Fixed Version opcją --runtime=FOLDER.", what, static_cast<unsigned>(hr));
  MessageBoxW(hwnd, text, L"szron-view", MB_ICONERROR);
  PostQuitMessage(1);
}

// --- WebView2: ustawienia, most JS, start ------------------------------------
static void onWebViewReady() {
  ICoreWebView2Settings *s = nullptr;
  webview->get_Settings(&s);
  s->put_AreDevToolsEnabled(o.devtools);
  s->put_IsStatusBarEnabled(FALSE);
  if (o.kiosk) {
    s->put_AreDefaultContextMenusEnabled(FALSE);
    s->put_IsZoomControlEnabled(FALSE);
    ICoreWebView2Settings3 *s3 = nullptr;  // runtime 87+: blokada Ctrl+P, F5, Ctrl+F itd.
    if (SUCCEEDED(s->QueryInterface(IID_Settings3, reinterpret_cast<void **>(&s3)))) { s3->put_AreBrowserAcceleratorKeysEnabled(FALSE); s3->Release(); }
  }
  s->Release();
  controller->put_ZoomFactor(o.zoom);

  // most JS → okno: window.chrome.webview.postMessage("close" | "fullscreen")
  EventRegistrationToken token;
  webview->add_WebMessageReceived(new Handler<ICoreWebView2WebMessageReceivedEventHandler, ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *>(
      IID_WebMessage, [](ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *args) {
        LPWSTR m = nullptr;
        if (SUCCEEDED(args->TryGetWebMessageAsString(&m))) {
          if (!wcscmp(m, L"close")) PostMessageW(hwnd, WM_CLOSE, 0, 0);
          else if (!wcscmp(m, L"fullscreen") && !o.kiosk) setFullscreen(!o.fullscreen);
          CoTaskMemFree(m);
        }
        return S_OK;
      }), &token);

  if (o.title.empty())
    webview->add_DocumentTitleChanged(new Handler<ICoreWebView2DocumentTitleChangedEventHandler, ICoreWebView2 *, IUnknown *>(
        IID_TitleChanged, [](ICoreWebView2 *w, IUnknown *) {
          LPWSTR t = nullptr;
          if (SUCCEEDED(w->get_DocumentTitle(&t))) { SetWindowTextW(hwnd, t); CoTaskMemFree(t); }
          return S_OK;
        }), &token);

  // kiosk: window.open i target=_blank otwierają się w tym samym oknie
  if (o.kiosk)
    webview->add_NewWindowRequested(new Handler<ICoreWebView2NewWindowRequestedEventHandler, ICoreWebView2 *, ICoreWebView2NewWindowRequestedEventArgs *>(
        IID_NewWindow, [](ICoreWebView2 *w, ICoreWebView2NewWindowRequestedEventArgs *args) {
          args->put_NewWindow(w);
          args->put_Handled(TRUE);
          return S_OK;
        }), &token);

  if (!o.dir.empty()) {
    ICoreWebView2_3 *w3 = nullptr;  // runtime 92+: folder pod https://app.local/
    if (FAILED(webview->QueryInterface(IID_WebView2_3, reinterpret_cast<void **>(&w3)))) return fail(L"Ten runtime nie obsługuje --dir", E_NOINTERFACE);
    wchar_t full[MAX_PATH];
    GetFullPathNameW(o.dir.c_str(), MAX_PATH, full, nullptr);
    w3->SetVirtualHostNameToFolderMapping(L"app.local", full, COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW);
    w3->Release();
    o.url = L"https://app.local/" + o.entry;
  }
  webview->Navigate(o.url.c_str());
}

int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR, int show) {
  if (!parseArgs()) { MessageBoxW(nullptr, USAGE, L"szron-view", MB_ICONINFORMATION); return 2; }

  // ostre okno na ekranach HiDPI: Per-Monitor v2 (Windows 10 1703+), wcześniej zwykłe DPI-aware
  using DpiFn = BOOL(WINAPI *)(HANDLE);
  auto setDpi = reinterpret_cast<DpiFn>(reinterpret_cast<void *>(GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext")));
  if (!setDpi || !setDpi(reinterpret_cast<HANDLE>(-4))) SetProcessDPIAware();

  CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  WNDCLASSW wc = {};
  wc.lpfnWndProc = wndProc;
  wc.hInstance = inst;
  wc.lpszClassName = L"szron-view";
  wc.hCursor = LoadCursorW(nullptr, reinterpret_cast<LPCWSTR>(IDC_ARROW));
  wc.hIcon = LoadIconW(nullptr, reinterpret_cast<LPCWSTR>(IDI_APPLICATION));
  RegisterClassW(&wc);
  hwnd = CreateWindowW(L"szron-view", o.title.empty() ? L"szron-view" : o.title.c_str(), WS_OVERLAPPEDWINDOW,
                       CW_USEDEFAULT, CW_USEDEFAULT, o.width, o.height, nullptr, nullptr, inst, nullptr);
  if (o.fullscreen) setFullscreen(true);
  ShowWindow(hwnd, show);

  auto *opts = new EnvOptions();
  HRESULT hr = createEnvironment(opts, new Handler<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, HRESULT, ICoreWebView2Environment *>(
      IID_EnvCompleted, [](HRESULT res, ICoreWebView2Environment *env) {
        if (FAILED(res)) { fail(L"Nie udało się uruchomić WebView2", res); return S_OK; }
        return env->CreateCoreWebView2Controller(hwnd, new Handler<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, HRESULT, ICoreWebView2Controller *>(
            IID_CtlCompleted, [](HRESULT res, ICoreWebView2Controller *c) {
              if (FAILED(res)) { fail(L"Nie udało się utworzyć widoku", res); return S_OK; }
              controller = c;
              controller->AddRef();
              controller->get_CoreWebView2(&webview);
              RECT r;
              GetClientRect(hwnd, &r);
              controller->put_Bounds(r);
              onWebViewReady();
              return S_OK;
            }));
      }));
  opts->Release();
  if (FAILED(hr)) fail(L"Nie znaleziono WebView2 Runtime", hr);

  MSG msg;
  while (GetMessageW(&msg, nullptr, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageW(&msg); }
  if (controller) controller->Close();
  CoUninitialize();
  return static_cast<int>(msg.wParam);
}

Kompilacja: MinGW albo Visual Studio.

Potrzebny jest jeden nagłówek z SDK: WebView2.h z paczki NuGet Microsoft.Web.WebView2. Nasz build: wersja SDK 1.0.4314.35, MinGW-w64 14 z GCC 16, kompilacja na macOS pod Windows x64.

1. Nagłówek WebView2

curl -L -o wv2.zip https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2
mkdir wv2 && tar -xf wv2.zip -C wv2
mkdir include && cp wv2/build/native/include/WebView2.h include/

Pod Visual Studio weź też wv2/build/native/x64/WebView2LoaderStatic.lib.

2a. MinGW (Windows z MSYS2, Linux, macOS)

x86_64-w64-mingw32-g++ -std=c++17 -Os -s -static -mwindows -municode szron-view.cc \
  -Iinclude -lole32 -luuid -lshell32 -luser32 -ladvapi32 -o szron-view.exe

Wynik: 243 200 bajtów. -static wciąga bibliotekę C++ do pliku, -s usuwa symbole, -Os optymalizuje pod rozmiar. Na macOS toolchain instaluje brew install mingw-w64.

2b. Visual Studio (Developer Command Prompt)

cl /std:c++17 /O1 /MT /EHsc szron-view.cc /Iinclude WebView2LoaderStatic.lib ^
   ole32.lib shell32.lib user32.lib advapi32.lib version.lib /link /SUBSYSTEM:WINDOWS

/MT wkompilowuje runtime C do pliku, a WebView2LoaderStatic.lib oficjalny loader. Microsoft opisuje to jako dystrybucję WebView2 jako jednego pliku.

3. Przed wdrożeniem

  • Niepodpisany .exe pobrany z sieci zatrzyma SmartScreen. Podpisz go certyfikatem firmy (signtool sign).
  • Plik .rc z ikoną i manifestem DPI dodaje kilka kilobajtów.
  • Dla starych 32-bitowych systemów kompiluj i686-w64-mingw32-g++ z tymi samymi opcjami (u nas 228 352 bajty).

Moduły webowe i pułapki.

file:// zamiast --dir Moduły ES i fetch zablokowane
Przeglądarka traktuje file:// jak osobne źródło każdego pliku. Dlatego ścieżki zawsze idą przez https://app.local/: to bezpieczny kontekst, więc działają moduły ES, fetch plików JSON, Web Workery i WebAssembly.
Dane modułu Profil = folder
localStorage i IndexedDB żyją w folderze --profile. Dwa stanowiska na jednym koncie potrzebują dwóch profili. Dane, które muszą przetrwać reinstalację stanowiska, trzymaj na serwerze, nie w profilu.
Flagi silnika --args
Microsoft zaznacza, że flagi mogą zniknąć w kolejnych wersjach. Bezpieczne w praktyce: --disable-gpu na maszynach wirtualnych, --proxy-server=…, --autoplay-policy=no-user-gesture-required. Program przekazuje je przez API, więc działają też, gdy uruchamiasz go jako administrator.
Długo działający kiosk Aktualizacje runtime
Nowa wersja runtime wchodzi dopiero po restarcie programu. Kiosk włączony miesiącami zostaje na starej wersji, więc restartuj go np. raz na dobę z harmonogramu zadań.
Windows 7 Tylko zamknięta sieć
Runtime 109 nie dostaje poprawek od 2023 roku. Otwieraj w nim wyłącznie własne moduły z --dir albo z serwera w sieci firmowej, nigdy otwarty internet.
Rozbudowa z Claude Code Jeden plik, jasny kontrakt
Plik ma ok. 300 linii, więc agent ogarnia go w całości. Przykładowe polecenie: „Dodaj do szron-view.cc opcję --allow=domena1,domena2: blokuj nawigację poza listą w NavigationStarting. Zbuduj MinGW i podaj rozmiar pliku”.

Potrzebujecie panelu albo modułu webowego obok starej aplikacji?

Budujemy cienkie przeglądarki, moduły HTML dla aplikacji desktopowych i kioski na hali. Krótka rozmowa wystarczy, żeby sprawdzić, czy ma to u Was sens. Odpowiadamy w 24 h.