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: Tomek Wojciechowski · Ostatnia aktualizacja:
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
ShellExecutez 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,
fetchi Web Workery nie działają zfile://, a tu folder dostaje adreshttps://app.local/bez serwera. - Kiedy nie Wystarczy Edge
- Jednorazowy pokaz albo test:
msedge.exe --app=https://adres --window-size=1280,800otwiera okno bez paska adresu, a--kiosk https://adres --edge-kiosk-type=fullscreenrobi 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
.exena 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ście | Co wozisz ze sobą | Zgodność z nowym webem | Werdykt |
|---|---|---|---|
| Cienki host na WebView2 (ten poradnik) | ok. 240 KB | Chromium w wersji Edge Stable, aktualizowany przez Microsoft | Tak |
msedge --app / --kiosk | 0 KB | Pełna | Do pokazów i testów |
| WebView2 Fixed Version, CEF, Electron | Fixed Version to ponad 250 MB, reszta podobnie | Pełna, ale wersja zamrożona | Tylko gdy musisz zamrozić wersję |
| Sciter | DLL 5+ MB | Własny silnik: CSS 2.1 i część CSS3, własny skrypt | Nie dla nowych modułów |
| Servo | Biblioteka Rust od wersji 0.1.0 (kwiecień 2026) | Rośnie co miesiąc, API jeszcze się zmienia | Za wcześnie |
| Ladybird | — | Alfa w 2026 tylko na Linux i macOS, Windows nieoficjalnie | Za wcześnie |
| Kontrolka WebBrowser (IE) | 0 KB | Silnik IE11: bez modułów ES i nowego CSS | Nie |
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ą.
| Opcja | Co robi | Przykład |
|---|---|---|
adres | plik | folder | Co otworzyć. Plik albo folder trafia na wirtualny host https://app.local/. | https://panel.local |
--dir=FOLDER | Folder z modułem serwowany pod https://app.local/ | --dir=D:\hmi |
--entry=PLIK | Plik startowy w --dir (domyślnie index.html) | --entry=linia3.html |
--title=TEKST | Tytuł okna (domyślnie tytuł strony) | --title="Raport" |
--width=N --height=N | Rozmiar okna (domyślnie 1280×800) | --width=1024 |
--fullscreen | Pełny ekran bez ramki | |
--kiosk | Pełny ekran bez menu kontekstowego, skrótów przeglądarki i nowych okien | |
--zoom=X | Powiększenie treści | --zoom=1.25 |
--devtools | Narzędzia deweloperskie pod F12 (domyślnie wyłączone) | |
--profile=FOLDER | Ciasteczka, localStorage, IndexedDB (domyślnie %LOCALAPPDATA%\szron-view) | --profile=D:\dane\hmi |
--runtime=FOLDER | WebView2 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()
CommandLineToArgvWrozbija linię komend z cudzysłowami. Ścieżka do pliku albo folderu zamienia się na--diri--entry. Profil domyślnie trafia do%LOCALAPPDATA%, bo folder obok.exewProgram Filesjest 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 ładujeEmbeddedBrowserWebView.dll. Tak samo robi biblioteka webview/webview. Dzięki temu obok.exenie leżyWebView2Loader.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 nahttps://app.local/przezSetVirtualHostNameToFolderMapping.WebMessageReceivedto 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
.exepobrany z sieci zatrzyma SmartScreen. Podpisz go certyfikatem firmy (signtool sign). - Plik
.rcz 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ą przezhttps://app.local/: to bezpieczny kontekst, więc działają moduły ES,fetchplików JSON, Web Workery i WebAssembly. - Dane modułu Profil = folder
localStoragei 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-gpuna 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
--diralbo 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.