加载中...
webview-让浏览器颤抖的力量
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
第7节:spdlog-首选的C++日志库
第8节:libUSB-脱掉USB的外套
第9节:libxlsxwriter-让数据说话
第10节:sqlite-orm-迅速上手.上
第11节:sqlite-orm-迅速上手.下
第12节:libExiv2-读写图像元数据
第13节:webview-让浏览器颤抖的力量
第14节:pystring-躺平的C++字符串工具库
第15节:cpr-自制Deepseek心理陪聊师
第16节:ExprTk-计算的力量
课文封面

使用 webview/webview 项目编写基于HTML的图形用户界面,并调用来自原生(以Windows为例)系统 API。让你的应用在拥有优秀的 B/S 架构优点的同时,拥有原生系统的强悍力量,以更好的满足地用户的需求。演示包括:

  1. JS 和 C++ 通信;
  2. 调用 C++ 函数,让应用拥有 JS 无法拥有的功能;
  3. 借 C++ 之力,获取强劲性能

1. 准备

  • 开发包下载

webview 暂未进入 msys2 的仓库,需到 github https://github.com/webview/webview 克隆项目或下载 zip 包。

下载后,将其 core/include 目录解压到磁盘上某个目录下即可,无需编译。

  • Windows下开发

在 Windows 下使用 webview 库开发,还需要下载 Windows 的一个 .NET 库(仅开发时需要,我们的程序编译后无需依赖它)。链接:https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2

下载后扩展名为 .nupkg,但可用类似 7Zip 的工具打开后,同样找个目录解压。

  • Linux 下开发

需安装 GTK3 或 GTK4(建议)的开发包,以 ubuntu 为例,命令为:

sudo apt install libWebKitGTK-devl
  • macOS 下开发

无需额外下载开发库,系统自带有官方浏览器 Safari 即可。

2. 运行条件

使用 webview 开发的程序,在三大系统内的运行条件如下——

  1. Windows:方便静态链接,只需系统带有 Edge 浏览器即可运行。实在需要在比较低版的 Windows 下运行,可考虑到 https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/ 单独下载 WebView2 运行时。
  2. macOS/Cocoa:有系统自带的 Safari 浏览器即可运行。
  3. Linux:需安装 GTK、WebKitGTK 等库,较难静态链接。

3. 课堂视频

4. 关键代码解释

4.1 初始化窗口

bool debugMode = false; #ifndef NDEBUG debugMode = true; std::system("chcp 65001 >nul"); #endif // 创建一个简单的 Webview 窗口 webview::webview w(debugMode, nullptr); if (!w.window().ok()) { return 1; } w.set_title("Hello WebView GUI - 第2学堂教学案例"); w.set_size(1200, 800, WEBVIEW_HINT_NONE);

webivew 的构造函数需要两个参数,第一个指明是否运行于调试模式,第二个用于指定个窗口指针(解释见后)作为 weview 的外层窗口,通常用于和外部窗口库配合,比如 Windows API、Qt、wxWidget、KDE 等;如传入空指针,则由 webview 库自动创建一个顶层窗口。

4.2 获取 Webview 窗口

上一小节代码中的 w.window(),可用于获得一个 webview 对原生窗口的简易包装。其 ok() 方法用于判断该窗口是否有效,在有效的情况下,我们可以将它所包装的窗口,倒过来转换为原生 GUI 库的窗口。以 Windows 为例,则为 HWND 。

HWND hWnd = static_cast<HWND>(w.window().value());

有这个 hWnd (在 Windows下通常称为 句柄)非常重要,我们可以将它传给各种作用的 Windows API,比如视频中示例的 “闪烁/Flush” 窗口在 Windows 下的实现:

::FLASHWINFO fi; fi.cbSize = sizeof(fi); fi.hwnd = hWnd; // 这里用到原生窗口句柄 fi.dwFlags = FLASHW_ALL; fi.uCount = 10; fi.dwTimeout = 0; ::FlashWindowEx(&fi);

4.3 设置页面内容

和普通浏览器一样,我们使用 webivew 访问资源,可以访问三种域的资源:

  • 可以直接 set_html()来设置 webview 要展现的内容(此时域为 “none”);
  • 也可以使用文件协议(file:///)让 webview 展现本地磁盘 html 等文件的内容(此时域为 “file”);
  • 当然也可以直接 navigate(url) 一个网址,比如 w.navigate(www.d2school.com)(此时域为对应的网站,即:d2school.com 。

注意,出于安全原因,浏览器(webview)默认禁止跨域访问。

  • 直接设置
w.set_html("<html><body><h1>Hello!</h1></body></html>"); // 域:none
  • 本地文件
auto fileURL = "file:///c:/tmp/a.html"; w.navigate(fileURL);
  • 真实网址:
w.navigate("https://www.d2school.com");

4.4 绑定C++函数

// 绑定斐波那契计算函数到JavaScript w.bind("cppFibonacci", [](std::string const& req) -> std::string { std::cout << "收到请求: " << req << std::endl; try { // 使用 nlohmann/json 解析 JSON 参数 // webview 传递的参数是 JSON 数组格式,例如: ["40"] auto jsonData = nlohmann::json::parse(req); if (!jsonData.is_array() || jsonData.empty()) { return ::error("参数格式错误:需要非空 JSON 数组"); } // 直接获取第一个参数并转换为整数 int n = jsonData[0].get<int>(); if (n < 0) { return ::error("参数必须为非负整数"); } if (n > 50) { return ::error("参数过大,可能导致长时间计算"); } // 计算斐波那契数 return ::result(fibonacciCPP(n)); } catch (nlohmann::json::parse_error const& e) { return ::exception("入参 JSON 解析错误", e); } catch (nlohmann::json::type_error const& e) { return ::exception("入参数据类型错误", e); } catch (std::exception const& e) { return ::exception("计算错误", e); } });

所绑定的函数(包括 lambda)需为 std::string (std::string) ,即入参和返回值均为 std::string;并且二者在实际运行时,也都必须是标准的 JSON 数据(允许是空串)。其中,入参固定为一 JSON 数组数组,元素对应 JS 传过来的参数。

提醒:JSON 数组支持异构元素,比如第1个元素为整数,第2个元素为字符串,第3个为对象等等。

上例中,入参字符串为 “[40]”, 表示 JS 传入的参数仅一个,且为整数 40 。

5. 完整代码

5.1 CMakeLists.txt

cmake_minimum_required(VERSION 3.16) project(helloWebView LANGUAGES CXX C) # 设置 C++ 17 标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 配置静态链接 msys2-ucrt64 的 DLL set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") # 配置调试版本下,程序运行带控制台 if (CMAKE_BUILD_TYPE STREQUAL "Debug") add_executable(${PROJECT_NAME}) else() add_executable(${PROJECT_NAME} WIN32) endif() target_sources(${PROJECT_NAME} PRIVATE main.cpp) # 添加额外的头文件包含路径 target_include_directories(${PROJECT_NAME} PRIVATE include) target_include_directories(${PROJECT_NAME} PRIVATE webview2-sdk/build/native/include) # 设置指定项目生成的可执行文件路径: set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin ) # 查找 nlohmann_json 库,请替换路径 set(nlohmann_json_DIR "C:/msys64/ucrt64/share/cmake/nlohmann_json") find_package(nlohmann_json REQUIRED) # 链接 Windows 基本运行库 target_link_libraries(${PROJECT_NAME} PRIVATE advapi32 ole32 shell32 shlwapi user32 version nlohmann_json::nlohmann_json)

5.2 main.cpp

#include <cstdlib> // system #include <algorithm> #include <chrono> #include <filesystem> #include <functional> #include <iostream> #include <optional> // 注意,须出现在 <nlohmann/json.hpp> 前面 #include <sstream> #include <string_view> #include <thread> #include <nlohmann/json.hpp> #include "webview/webview.h" #ifdef _WIN32 #include <windows.h> std::atomic_bool playing; namespace Win32Pro { HWND hWnd = NULL; // 窗口居中函数 void center_window() { if (!hWnd) { return; } // 获取窗口尺寸 RECT window_rect; GetWindowRect(hWnd, &window_rect); int window_width = window_rect.right - window_rect.left; int window_height = window_rect.bottom - window_rect.top; // 获取屏幕尺寸 int screen_width = GetSystemMetrics(SM_CXSCREEN); int screen_height = GetSystemMetrics(SM_CYSCREEN); // 计算居中位置 int x = (screen_width - window_width) / 2; int y = (screen_height - window_height) / 2; // 设置窗口位置 SetWindowPos(hWnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE); } // 闪烁窗口 void flush_window() { if (!hWnd) { return; } ::FLASHWINFO fi; fi.cbSize = sizeof(fi); fi.hwnd = hWnd; fi.dwFlags = FLASHW_ALL; fi.uCount = 10; fi.dwTimeout = 0; ::FlashWindowEx(&fi); } // Beep void beep_win(DWORD f, DWORD l) { ::Beep(f, l); } // 使用后台线程,播放两只老虎前四句 std::string play_two_tigers(std::string const&) { bool noPlaying = false; if (!playing.compare_exchange_strong(noPlaying, true)) { return ""; } // 两只老虎,两只老虎,跑得快,跑得快 // 使用简谱频率:C4(261.63Hz), D4(293.66Hz), E4(329.63Hz), C4(261.63Hz) std::thread trd([]() { // 两只老虎 beep_win(261, 500); // C4 beep_win(293, 500); // D4 beep_win(329, 500); // E4 beep_win(261, 500); // C4 // 两只老虎 beep_win(261, 500); // C4 beep_win(293, 500); // D4 beep_win(329, 500); // E4 beep_win(261, 500); // C4 // 跑得快 beep_win(329, 500); // E4 beep_win(349, 500); // F4 beep_win(392, 1000); // G4 (长音) // 跑得快 beep_win(329, 500); // E4 beep_win(349, 500); // F4 beep_win(392, 1000); // G4 (长音) playing.store(false); }); trd.detach(); return ""; } // 窗口狂抖 void shake_window(HWND hWnd, int durationSeconds, int shakeIntensity) { if (!hWnd || !IsWindow(hWnd)) { return; } // 获取窗口原始位置 RECT rect; GetWindowRect(hWnd, &rect); int originalX = rect.left; int originalY = rect.top; // 计算抖动结束时间 DWORD endTime = GetTickCount() + durationSeconds * 1000; DWORD startTime = GetTickCount(); while (GetTickCount() < endTime) { DWORD elapsed = GetTickCount() - startTime; float progress = (float)elapsed / (durationSeconds * 1000.0f); // 使用衰减的正弦波来创建逐渐减弱的抖动效果 float decay = 1.0f - progress; float timeFactor = elapsed * 0.02f; // 计算当前帧的偏移量 int offsetX = static_cast<int>(sin(timeFactor * 7.3f) * shakeIntensity * decay); int offsetY = static_cast<int>(cos(timeFactor * 5.7f) * shakeIntensity * decay); // 移动窗口 ::SetWindowPos(hWnd, NULL, originalX + offsetX, originalY + offsetY, 0, 0, SWP_NOZORDER | SWP_NOSIZE); ::Sleep(16); // 约60FPS } // 恢复原始位置 ::SetWindowPos(hWnd, NULL, originalX, originalY, 0, 0, SWP_NOZORDER | SWP_NOSIZE); } } // namespace Win32Pro #endif // 错误响应 std::string error(std::string_view error) { return nlohmann::json { { "error", error } }.dump(); } // 异常响应 std::string exception(std::string title, std::exception const& e) { return nlohmann::json {{"error", title + ":" + e.what()}}.dump(); } // 结果响应 std::string result(long long result) { return nlohmann::json { { "result", result } }.dump(); } // C++ 斐波那契计算函数(递归实现,与JS版本保持一致) long long fibonacciCPP(int n) { if (n <= 1) { return n; } return fibonacciCPP(n - 1) + fibonacciCPP(n - 2); } // 包装函数,用于WebView绑定 void bindCPPFunctions(webview::webview& w) { // 绑定第三方库版本查询函数 w.bind("cppGet3rdLibrariesVersion", [](std::string const&) -> std::string { // 使用 WEBVIEW_VERSION_NUMBER 宏快速获取 WebView 版本 std::string webview_version = WEBVIEW_VERSION_NUMBER; // 使用 nlohmann/json 的 meta() 方法获取完整库信息 nlohmann::json nlohmann_meta = nlohmann::json::meta(); nlohmann::json response = { { "webview", webview_version }, { "nlohmann-json", nlohmann_meta } }; return response.dump(); }); // 绑定斐波那契计算函数到JavaScript w.bind("cppFibonacci", [](std::string const& req) -> std::string { std::cout << "收到请求: " << req << std::endl; try { // 使用 nlohmann/json 解析 JSON 参数 // webview 传递的参数是 JSON 数组格式,例如: ["40"] auto jsonData = nlohmann::json::parse(req); if (!jsonData.is_array() || jsonData.empty()) { return ::error("参数格式错误:需要非空 JSON 数组"); } // 直接获取第一个参数并转换为整数 int n = jsonData[0].get<int>(); if (n < 0) { return ::error("参数必须为非负整数"); } if (n > 50) { return ::error("参数过大,可能导致长时间计算"); } // 计算斐波那契数 return ::result(fibonacciCPP(n)); } catch (nlohmann::json::parse_error const& e) { return ::exception("入参 JSON 解析错误", e); } catch (nlohmann::json::type_error const& e) { return ::exception("入参数据类型错误", e); } catch (std::exception const& e) { return ::exception("计算错误", e); } }); // 绑定后台线程播放 两只老虎 w.bind("cppPlayTwoTigers", &Win32Pro::play_two_tigers); // 绑定窗口抖动 w.bind("cppShakeWindow", [](std::string const& req) -> std::string { int durationSeconds = 5, shakeIntensity = 15; try { auto jsonData = nlohmann::json::parse(req); if (jsonData.is_array() && !jsonData.empty()) { durationSeconds = jsonData[0].get<int>(); if (jsonData.size() > 1) { shakeIntensity = jsonData[1].get<int>(); } } } catch (...) { } Win32Pro::shake_window(Win32Pro::hWnd, durationSeconds, shakeIntensity); return ""; }); } int main() { bool debugMode = false; #ifndef NDEBUG debugMode = true; std::system("chcp 65001 >nul"); #endif // 创建一个简单的 Webview 窗口 webview::webview w(debugMode, nullptr); if (!w.window().ok()) { return 1; } Win32Pro::hWnd = static_cast<HWND>(w.window().value()); w.set_title("Hello WebView GUI - 第2学堂教学案例"); w.set_size(1200, 800, WEBVIEW_HINT_NONE); Win32Pro::center_window(); Win32Pro::flush_window(); bindCPPFunctions(w); // 先获取当前程序的运行路径: auto current_path = std::filesystem::current_path(); // 在 vs-code 内跑,需接上 bin/ #if 1 auto index_html_path = (current_path / "bin/index.html").string(); #else // 否则,在 外部环境直接运行,请在编译前将上面 1 改成 0 auto index_html_path = (current_path / "index.html").string(); #endif std::cout << "准备加载首页文件:" << index_html_path << std::endl; if (!std::filesystem::exists(index_html_path)) { std::cerr << "首页文件不存在!" << std::endl; w.set_html("<html><body><h1>file no found!</h1></body></html>"); // 域:none } else { std::string file_url = "file:///" + index_html_path; // 在 Windows 下的话,将反斜杠转换为正斜杠 std::replace(file_url.begin(), file_url.end(), '\\', '/'); w.navigate(file_url); // 域:file } w.run(); }

5.3 网页与图片资源

【附件】:helloWebview-项目前端资源.zip