// 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 #include #include #include #include #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 struct Handler final : I { IID iid; std::function fn; ULONG refs = 1; Handler(const IID &id, std::function 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(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(reinterpret_cast(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(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(&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( 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( 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( 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(&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(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext"))); if (!setDpi || !setDpi(reinterpret_cast(-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(IDC_ARROW)); wc.hIcon = LoadIconW(nullptr, reinterpret_cast(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( 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( 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(msg.wParam); }