初识Libcef

Libcef 的完整发行包中自带示例代码,可以用cmake生成工程后编译运行。我的目的将cefclient编译为DLL,由它与libcef打交道,最终的exe与所有dll互无编译依赖,通过 GetProcAddress 动态对接。

参考:notepad++插件、miniblink-wke控件的实现。

首先,编译 cefclient.exe,记得关闭“treat warning as error”。关闭这种选项在项目设置里的"all options"中搜索即可。

编译成功后,显示出有几个按钮、一些菜单的简单浏览器界面。

2020年了,LIBCEF的演示项目一上来就三个问题:

  • 默认会搜索代理服务器,当打开 ie设置->互联网->链接->局域网设置->自动检测设置时,启动相当慢,需要6秒左右。
  • 默认是"多进程模式",对于一个控件来说,未免太过怪异与奢侈了点。
  • 会记录log文件,而且启动有弹窗提示。

解决前两个问题,需要在程序启动参数中追加参数开关–no-proxy-server --single-process。多进程模式下,新启动的进程会重复几次进入wWinMain入口,而且自带参数,可以打印查看:

若想要直接在程序中嵌入参数,也不是没有办法。因为 GetCommandLine 返回的字符串是可以修改的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  auto cmd = GetCommandLine();
auto idx=wcsstr(cmd, L"\" --");
if(idx)
{
idx=idx+1;
}
else
{
// todo boundary check
idx = cmd+lstrlen(cmd);
}
if(idx)
{
lstrcpy(idx, TEXT(" -no-proxy-server -single-process -disable-logging"));
}
// 在 CefExecuteProcess 之前调用

这一点恐怕没人想得到吧,不过可能有潜在的数组越界问题。

解决问题三就简单的多了,直接修改cefsetting结构,关闭log即可。

1
2
3
CefSettings settings;
// context->PopulateSettings(&settings); // 不要用这句,骗小孩子的玩意。
settings.log_severity=LOGSEVERITY_DISABLE;

吐槽一下CMAKE,生成的项目真是一团糟,一模一样的运行库复制了好几遍,一下子就浪费近一个G。CMAKE不是生成了就拍一拍衣袖深藏功与名的那种,而是存留了很多莫名其妙的CustomBuild与安装指令,事实上把这些CMAKE余孽全部删掉都没有问题。

CMAKE最大的问题:生成的vs项目里全部都是绝对路径,目录一搬就出错。如果是相对路径,直接把解决方案上传github都没有问题,别人打开就可以编译,哪里用得着CMAKE?

初次封装,拿到浏览器HWND

修改这些源码:cefclient_win.cc、cefclient_handler.cc、main_message_loop_multithreaded_win.cc。

第一步,封装出 bwCreateBrowser 方法,用于创建浏览器控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

HINSTANCE g_hInstance;

HWND hwnd;

client::MainContextImpl * pMainContextImpl;

client::MainMessageLoopMultithreadedWin* looper;

extern "C" __declspec(dllexport) HWND bwGetHWNDForBrowser(void* pBrowser)
{
CefBrowser* browser = (CefBrowser*)pBrowser;
return browser?browser->GetHost()->GetWindowHandle():0;
}

extern "C" __declspec(dllexport) int bwCreateBrowser(HWND hParent, CHAR* URL, BrowserCallback bwCallback) {
if(IsWindow(hParent))
{
if(!looper)
{
// Enable High-DPI support on Windows 7 or newer.
//CefEnableHighDPISupport();
CefMainArgs main_args(g_hInstance);

void* sandbox_info = nullptr;

#if defined(CEF_USE_SANDBOX)
// Manage the life span of the sandbox information object. This is necessary
// for sandbox support on Windows. See cef_sandbox_win.h for complete details.
CefScopedSandboxInfo scoped_sandbox;
sandbox_info = scoped_sandbox.sandbox_info();
#endif

// Parse command-line arguments.
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();

CefRefPtr<CefApp> app = new ClientAppBrowser();

TCHAR cmd_extra[MAX_PATH];
decorateCommandLine(cmd_extra, 0); // 修改命令行参数

// Execute the secondary process, if any.
int exit_code = CefExecuteProcess(main_args, app, sandbox_info);
if (exit_code >= 0)
return exit_code;

pMainContextImpl = new MainContextImpl(command_line, true); // Create the main context object.

CefSettings settings;
settings.Set({}, 0);
CefString(&settings.application_client_id_for_file_scanning).FromString("9A8DE24D-B822-4C6C-8259-5A848FEA1E68");
//context->PopulateSettings(&settings);
settings.command_line_args_disabled=0;
settings.no_sandbox = false;
settings.log_severity=LOGSEVERITY_DISABLE;
settings.multi_threaded_message_loop=1;

// Initialize CEF.
pMainContextImpl->Initialize(main_args, settings, app, sandbox_info);
// Initialize CEF.

looper = new MainMessageLoopMultithreadedWin();
looper->Run();
}

CefRefPtr<ClientHandler> g_handler = new ClientHandlerStd(0, URL);
CefBrowserSettings browser_settings;
CefWindowInfo window_info;
RECT rc;
GetWindowRect(hParent, &rc);

//设置父窗口信息
window_info.SetAsChild(hParent, rc);
CefRefPtr<CefDictionaryValue> extra_info;
CefRefPtr<CefRequestContext> request_context;
g_handler->bwCallback = bwCallback;

//最终创建控件
CefBrowserHost::CreateBrowser(window_info, g_handler, URL, browser_settings, extra_info, request_context);
}
}

使用 CefBrowserHost::CreateBrowser 也就是 cef_browser_host_create_browser 创建浏览器时,无法直接得到浏览器指针,需要在 OnAfterCreated 回调中处理:

1
2
3
4
5
6
7
8
9
10
11
typedef const void * (__cdecl * BrowserCallback)(CefBrowser*);


void ClientHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
……
if(bwCallback)
{
bwCallback(browser);
}
NotifyBrowserCreated(browser);
}

关于 ClientHandler,每一个浏览器控件都会新建一个。兼容原生win32的while(GetMessage)消息循环,是通过MainMessageLoopMultithreadedWin实现的,多个浏览器控件可以共用一个MainMessageLoopMultithreadedWin,所以在封装bwCreateBrowser之时我用looper变量判断cef是否已经加载并初始化。

MainMessageLoopMultithreadedWin实现的原理是看不懂,不过它会调用PostQuitMessage终止消息循环,需要修改掉这一点。PostQuitMessage怎么可能由一个控件调用?一般都是在主窗口WNDPROC的case WM_DESTORY中调用的。

主窗口WNDPROC不能瞎写,比如始终返回true,或者在case WM_PAINT返回true,会导致CPU占用飙升。

封装就是这个样子,如下在.exe中调用,很简单吧!

在wWinMain入口中,先新建主窗口,得主窗口hwnd。

1
2
3
4
regWndClass(L"TESTWND", CS_HREDRAW | CS_VREDRAW);
hwnd = ::CreateWindowEx(WS_EX_APPWINDOW , L"TESTWND" , NULL
, WS_OVERLAPPEDWINDOW | WS_VISIBLE , 0 , 0 , 840 , 680 , NULL , NULL , ::GetModuleHandle(NULL), NULL);
ShowWindow(hwnd, true);

再加载 cefclient.dll,新建浏览器控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef const int (__cdecl * BWCREATEBROWSER)(HWND, const CHAR*, LONG_PTR);

typedef const HWND (__cdecl * BWGETHWNDFORBROWSER)(void*);

BWCREATEBROWSER bwCreateBrowser;

BWGETHWNDFORBROWSER bwGetHWNDForBrowser;

void onBrowserPrepared(void* browserPtr)
{
hBrowser = bwGetHWNDForBrowser(browserPtr);
// 拿到了浏览器控件的HWND!
}

// 加载cefclient.dll
// 首先拼接出库目录,现只支持与exe同路径,
// 未来考虑通过配置文件/环境变量来查找库文件
TCHAR buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
PathRemoveFileSpec(buffer);
PathAppend(buffer, L"cefclient.dll");
TCHAR LibBwgtPath[MAX_PATH];
PathCanonicalize(LibBwgtPath, buffer);
if(PathFileExists(LibBwgtPath))
{
const DWORD dwFlags = GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "AddDllDirectory") != NULL ? LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0;
auto hLibBwgt = ::LoadLibraryEx(LibBwgtPath, NULL, dwFlags);
if(hLibBwgt) // 库文件加载成功
{
bwCreateBrowser = (BWCREATEBROWSER)GetProcAddress(hLibBwgt, "bwCreateBrowser");
bwGetHWNDForBrowser = (BWGETHWNDFORBROWSER)GetProcAddress(hLibBwgt, "bwGetHWNDForBrowser");

// 新建控件
bwCreateBrowser(hwnd, "www.bing.com", (LONG_PTR)onBrowserPrepared);

// 甚至可以在同一窗口再开一个浏览器控件 :
bwCreateBrowser(hwnd, "www.baidu.com", (LONG_PTR)onBrowserPrepared2);
}
}

最后是常规的消息循环。

1
2
3
4
5
6
7
8
9
MSG msg;
{
while (GetMessage(&msg, NULL, 0, 0)) {
//if ((looper->dialog_hwnd_ && IsDialogMessage(looper->dialog_hwnd_ , &msg)))
// continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

BrowserUI.cpp

cefclient_win.cc

main_message_loop_multithreaded_win.cc

资源拦截替换、JS调用C++ Native、首次运用

实现资源拦截器
cefclient 示例代码自带一些资源拦截替换的实现,名为某某Provider。比如“查看网页源码的”的菜单功能,就是通过 test_runner.cc 下的类 StringResourceProvider 实现。各种 Provider 通过 test_runner::SetupResourceManager 组装。我呢后继实现不在去看它,而是直接在已有的 StringResourceProvider 之上加入回调函数:

1
typedef url_intercept_result* (__cdecl * BC_URLInterceptor)(std::string);

回调结果放在结构体内,方便以后拓展:

1
2
3
4
5
6
7
8
struct url_intercept_result{
CHAR* data;
size_t length;
int status_code;
CHAR* status_text;
CHAR mime[92]={0};
bool delete_internal=false;
};

值得一提的是,结构体在exe定义的回调中构建,然后传给cefclient.dll。若直接在后者中用完了销毁,会在debug模式报错:__acrt_first_block == header。当然release模式下没有问题,不过为以后更新埋下了隐患。作为拖妥协,只好把回调函数再用一次,将结构体传回exe,exe端delete、释放内存。

示例,访问百度首页返回HAPPY

1
2
3
4
5
6
url_intercept_result* Test_InterceptBaidu(std::string url){ // 资源拦截的回调
if(url=="https://www.baidu.com/") {
return new url_intercept_result{"HAPPY", 5, 200, "OK"};
}
return nullptr;
}

使用:

1
2
3
4
5
6
7
8
struct BWCreateOptions{
HWND hParent=0; //嵌入至的父窗口句柄。若空,则使用默认的窗口模式
const CHAR* URL=nullptr; //初始化网址
BC_BrowserCallback bcCallback=0; // 创建完成的回调
BC_URLInterceptor bcInterceptor=0; //资源拦截器回调
};
BWCreateOptions args = {hwnd, "www.baidu.com", Test_browsercallback, Test_InterceptBaidu};
bwCreateBrowser(args);

实现简而言之就是想方设法使资源拦截器回调可以在 StringResourceProvider 中访问。一开始我是想将资源拦截器赋给CefBrowser对象,然后像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StringResourceProvider : public CefResourceManager::Provider {
public:
StringResourceProvider(StringResourceMap* string_resource_map, StrResourceMap* str_resource_map)
……

bool OnRequest(scoped_refptr<CefResourceManager::Request> request) OVERRIDE {
CEF_REQUIRE_IO_THREAD();

const std::string& url = request->url();
if (url.compare(0, 13, kTestOrigin)) { //try_intercept

理想中的代码:
if(request->browser()->_resource_interceptor)
{
const url_intercept_result = request->browser()->_resource_interceptor(url);

奈何每次 OnRequest 回调中获得的 request->browser() 对象都是不同的,尽管实际上只有一个浏览器控件。只能退而求其次将资源拦截器赋给ClientHandler了:
实际的代码:

1
2
3
if(((ClientHandler*)request->browser()->GetHost()->GetClient().get())->_resource_interceptor)
{
const url_intercept_result = ((ClientHandler*)request->browser()->GetHost()->GetClient().get())->_resource_interceptor(url);

test_runner.cc#L524

这就完了。可以在实际使用中进行测试了。不过那之前,需要修理一下乱糟糟的代码。几年前传承下来的c++代码都是两个空格作为缩进,还是用工具改写成一个Tab吧。

选择使用Textrument的TextFx插件进行“reindent c++ code”。前置又要修复一下TextFx这个同样古董级别的c++项目。里面有许多奇奇怪怪的函数名,比如标准里有strcspn、wcscspn、strchr,他就来许多memcspn_chr、testmemcspn、memstr、strmstrinit、strmstr、strmstrclose、memchrX、memmovearmtest、memcqspn等等等等,再加上wchar_t和char宽度的不同,这里漏一个cspn的c,那里少一个sizeof(TCHAR),BUG一大堆,简直是噩梦一般的编程体验。

幸好不负期望,完成了一部分的修复工作。现在,可以回到浏览器组件的主题,实现JS调用C++!

JS调用C++ Native,处理JS传来的参数及C++返回值

这一点实现较为复杂,尤其是涉及多进程模式时。

多进程模式下大概只能像chrome插件那样全局注入Native代码,然后根据当前网址的不同决定是否执行。CEF的多进程太复杂,难以实现“将这段Native代码注入到此浏览控件以及由该控件弹出的一系列新窗口”这样基于控件的设计理念,干脆放弃。单进程不香么,又不是单线程。

( 看client_handler.cc,可能CefMessageRouterBrowserSide是处理多进程的。嫌麻烦没看。反正之后会兼容Webview2,Libcef的集成怎么简单就怎么来好了。 )

处理参数及返回值:用一个结构体概括各种数据类型

参考:
CEF中JavaScript与C++交互

C++ 调用 JS

1
2
3
4
5
6
7
extern "C" __declspec(dllexport) void* bwExecuteJavaScript(CefRefPtr<CefBrowser>* pBrowser, CHAR* JS)
{
if(pBrowser)
{
(*pBrowser)->GetMainFrame()->ExecuteJavaScript(JS, "", 0);
}
}

完成JS调用C++与C++调用JS后,就可以具体运用了!来看第一个实例的详细介绍。

为Textrument文本编辑器开发Markdown实时预览插件

构成规划:集成多个可以互相替换的浏览器内核,采用md.html将markdown文本渲染为html。

md.html集成了以下js项目:

remark and unified families: Markdown 转 HTML 管线
github-markdown-css: 从 GitHub 提取的 Markdown CSS
highlight.js: 语法高亮库
KaTeX: latex 数学排版库

可选用的浏览器内核以及各自的优缺点:

  • miniblink : 体积小(~30MB),开源免费。但是单线程,JS会卡UI。
  • miniblink vip : 在miniblink的基础上追加两MB的付费拓展,支持多线程渲染、基于插件的视频播放。但是价格昂贵,而且虽然JS不卡UI了,但加载新的页面无法中断旧的正在运行的JS。
  • Libcef : 开源免费,用有chromium的诸多特性,保持更新。但是体积臃肿,配置复杂,剔除了播放MP4视频的功能。
  • Webview2 : 微软自家维护,基于edge chromium,功能齐全。但仍处于测试阶段,需要下载Edge的开发者预览版才能使用。

同类插件:

  • Markdown Panel (C#, IE)
  • Markdown Viewer ++ (C#, HTML Renderer)
    两者均不支持数学公式与实时预览。

如何开发 MarkdownText Plugin:

  1. 任选一带有dock panel的Notepad++插件作为模板代码。
  2. 集成览器控件,显示最基本的 hello world
  3. 利用资源拦截技术集成md.html,加载位于插件dll文件夹的js代码。

md.html略有修改,“导出” 了window.APMD方法。改动、以及调用如下:

1
2
3
4
5
6
7
8
9
//修改
window.APMD=async function(md_text){
//方法体照抄源库正下方代码。
}
//调用
// main.js 是JS库编译后的文件。
<script src="http://mdbr/main.js">

window.APMD("# hello world "); // Append and render Markdown text.

md.html如何编译:同saladict,安装vscode、yarn、node,运行yarn install然后yarn build,编译时间16s。

利用JS调用C++ Native技术,将Notepad加载的文本传给JS层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// char* GetDocTex : 获取 Notepad 当前显示的文本

BJSCV* GetDocText1(LONG_PTR funcName, int argc, LONG_PTR argv, int sizeofBJSCV)
{
int len;
return new BJSCV{typeString, 0, GetDocTex(len)};
}

void onBrowserPrepared(bwWebView browserPtr)
{
bwInstallJsNativeToWidget(browserPtr, "GetDocText1", GetDocText1);
}

bwLoadStrData(mWebView_2, "123.html", "<!doctype html><meta charset=\"utf-8\"> <script src=\"http://mdbr/main.js\"></script><body><script>window.APMD(GetDocText1(''));</script></body>", 0);

小结

已实现将Libcef打造为win32控件,供 Notepad 的 Markdown 预览插件——MarkdownText使用。

此次实现专注于要用到的功能,故而尚有欠缺,比如并未为“C++调用JS”设计“处理JS返回值”的回调。

插件代码:MarkdownText

项目描述:Notepad 插件,要与 Textrument 的头文件一起编译。不仅仅是个预览性质的插件,还加入了Markdown语法的加粗、斜体、下划线这些快捷入口,以后再添加Latex语法的加粗、斜体、下划线等等。然后去兼容html的实时预览。甚至还可以发展出小程序,比如内置一个Saladict。

BrowserWidget - win32控件代码:BrowserWidget

项目描述:将Libcef的demo程序 cefclient.exe 改写为 cefclient.dll,导出以下函数,全部在BrowserUI.h中对接处理:

bwCreateBrowser : 创建浏览器。exe端在创建完成的回调中保存 CefRefPtr<CefBrowser>* pBrowser 指针,作为以下浏览器操作的句柄。

bwGetHWNDForBrowser : 获得浏览器窗口句柄
bwLoadStrData : 加载字符串
bwInstallJsNativeToWidget : Js调用C++ Native
bwParseCefV8Args : 分析“Js调用C++ Native”时JS传来的参数(将cefclient中的数据结构)

bwGetUrl : 获得url
bwExecuteJavaScript : Native调用Js
bwCanGoBack :可以后退
bwGoBack : 后退网页
bwCanGoForward : 可以前进
bwGoForward : 前进网页
bwDestroyWebview : 销毁浏览器实例
bwGetZoomLevel : 获取页面缩放
bwSetZoomLevel : 设置页面缩放
bwZoomLevelDelta : 为页面缩放应用一个Δ增量/减量

兼容微软Webview2

Webview2 之于 windows,等同于 webview 之于安卓系统。Chromium 真是遍地开花啊。目前,Webview2处于公测阶段,用户需安装 edge 的内部版本(insider)才能使用;外界开发者则面临着暂时的文档不够详细、demo无法运行的问题。

官方 demo 仓库:

或者(比demo高级点):

其中 Webview2Gettingstarted 是最简单的win32示例项目。成功编译运行后,打开一个空窗口,里面一个Webview2控件,导航至www.bing.com。Webview2 功能齐全,右键菜单、页内搜索、下载、常用快捷键(刷新/放大/打印)、dev控制台等等样样具备,关键是产品体积小,运行内存也不大,实在是太振奋人心了!

过代码不全,需配合官方教程,该复制的复制,头文件该补的补。

源文件仅一个HelloWebview.cpp:

Demo的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//这个库需要自己创建窗口 hWnd 
// <-- WebView2 sample code starts here -->

// Step 3 - Create a single WebView within the parent window
// Locate the browser and set up the environment for WebView
auto options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>();

//CreateCoreWebView2EnvironmentWithOptions(TEXT("C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\85.0.564.63")
// , TEXT("C:\\Users\\TEST\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default")
// , options.Get(),
static wil::com_ptr<ICoreWebView2> m_webView;
static wil::com_ptr<ICoreWebView2Environment> m_webViewEnvironment;

CreateCoreWebView2EnvironmentWithOptions(0,0,0,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[hWnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {

m_webViewEnvironment=env;

// Create a CoreWebView2Controller and get the associated CoreWebView2 whose parent is the main window hWnd
env->CreateCoreWebView2Controller(hWnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[hWnd](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
if (controller != nullptr) {
webviewController = controller;
webviewController->get_CoreWebView2(&m_webView);
}

// Add a few settings for the webview
// The demo step is redundant since the values are the default settings
ICoreWebView2Settings* Settings;
m_webView->get_Settings(&Settings);
Settings->put_IsScriptEnabled(TRUE);
Settings->put_AreDefaultScriptDialogsEnabled(TRUE);
Settings->put_IsWebMessageEnabled(TRUE);

// Resize WebView to fit the bounds of the parent window
RECT bounds;
GetClientRect(hWnd, &bounds);
webviewController->put_Bounds(bounds);

// Schedule an async task to navigate to Bing
m_webView->Navigate(L"https://www.bing.com/");


// 最简单的demo建立完毕。

// 以下自行测试各种功能……

// Step 4 - Navigation events

// Step 5 - Scripting

// Step 6 - Communication between host and web content

return S_OK;
}).Get());
return S_OK;
}).Get());

// <-- WebView2 sample code ends here -->

另有一个示例项目Webview2ApiSample,代码更全,但是无法编译,打开了当参考也好,比看文档强。

成功运行最简Demo后,就可以测试各种功能了:

  • 加载字符串: m_webView->NavigateToString((LPCWSTR)L"哈哈哈哈哈");

    管中窥豹,Webview2 的API命名是与众不同的,别人load、load,他偏偏要 Navigate……

  • 添加资源拦截器,拦截所有图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    static EventRegistrationToken m_webResourceRequestedTokenForImageBlocking = {};

    //add Filter
    m_webView->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE);

    //add Handler
    CHECK_FAILURE(m_webView->add_WebResourceRequested(
    Callback<ICoreWebView2WebResourceRequestedEventHandler>(
    [hWnd](
    ICoreWebView2* sender,
    ICoreWebView2WebResourceRequestedEventArgs* args) {
    COREWEBVIEW2_WEB_RESOURCE_CONTEXT resourceContext;
    CHECK_FAILURE(
    args->get_ResourceContext(&resourceContext));
    // Ensure that the type is image
    if (resourceContext != COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE)
    {
    return E_INVALIDARG;
    }
    // Override the response with an empty one to block the image.
    // If put_Response is not called, the request will continue as normal.
    wil::com_ptr<ICoreWebView2WebResourceResponse> response;
    //wil::com_ptr<wil::com_ptr<ICoreWebView2WebResourceResponse>> response;

    m_webViewEnvironment->CreateWebResourceResponse(nullptr, 403 /*NoContent*/, L"Blocked", L"", &response);
    CHECK_FAILURE(args->put_Response(response.get()));
    return S_OK;
    }).Get(), &m_webResourceRequestedTokenForImageBlocking));

    CHECK_FAILURE 来自于 Webview2ApiSample,没什么用,嫌麻烦就拿掉或者#define CHECK_FAILURE。这个lambda表达式是个啥结构暂时看不懂,照样复制粘贴呗!

  • 拦截所有图片,然后替换为本地文件:

    1
    2
    3
    4
    5
    6
    7
    wil::com_ptr<IStream> stream;

    CHECK_FAILURE(SHCreateStreamOnFileEx(
    L"D:\\1.jpg", STGM_READ, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr,
    &stream));

    m_webViewEnvironment->CreateWebResourceResponse(stream.get(), 200, L"OK", L"", &response);
  • 拦截网址访问,返回中文字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    if (resourceContext == COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT)
    {
    wil::com_ptr<ICoreWebView2WebResourceRequest> req;
    args->get_Request(&req);
    wil::unique_cotaskmem_string navigationTargetUri;
    req->get_Uri(&navigationTargetUri);
    std::wstring uriTarget(navigationTargetUri.get());
    if(uriTarget==L"http://test/MDT/1.html");
    {
    CHAR test_buffer1[1024];
    sprintf(test_buffer1, "%s", " 1234哈哈哈");

    wil::com_ptr<ICoreWebView2WebResourceResponse> response;
    wil::com_ptr<IStream> stream = SHCreateMemStream((const BYTE*)test_buffer1, strlen(test_buffer1));

    CHECK_FAILURE(m_webViewEnvironment->CreateWebResourceResponse(stream.get(), 200, L"OK", L"charset=utf-8", &response));

    wil::com_ptr<ICoreWebView2HttpResponseHeaders> headers;
    CHECK_FAILURE(response->get_Headers(&headers));
    headers->AppendHeader(L"Content-Type", L"text/html; charset=utf-8");

    CHECK_FAILURE(args->put_Response(response.get()));
    return S_OK;
    }
    }

    注意在响应头Content-Type中定义正确的MIME类型和编码字符集,否则中文乱码,不能正常显示。

  • JS调用C++
    这个教程第六步有介绍,有点不同,是通过 postMessage 实现的。

  • C++调用JS

    1
    2
    3
    4
    5
    6
    m_webView->ExecuteScript(L"window.document.URL;", Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
    [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT {
    LPCWSTR URL = resultObjectAsJson;
    //doSomethingWithURL(URL);
    return S_OK;
    }).Get());
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //stdfx.h
    #include <windows.h>
    #include <stdlib.h>
    #include <string>
    #include <tchar.h>
    #include <wrl.h>
    #include <wil/com.h>

    // include WebView2 header
    #include "WebView2.h"
    #include "WebView2EnvironmentOptions.h"
    // include WebView2 header

    using namespace Microsoft::WRL;

编译后的产品有一个 100kb 的依赖: WebView2Loader.dll