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 开发的程序,在三大系统内的运行条件如下——
- Windows:方便静态链接,只需系统带有 Edge 浏览器即可运行。实在需要在比较低版的 Windows 下运行,可考虑到 https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/ 单独下载 WebView2 运行时。
- macOS/Cocoa:有系统自带的 Safari 浏览器即可运行。
- 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();
}