背景
需要解决以下几个问题
- 政府项目新浏览器兼容老系统ActiveX控件,Qt WebEngineView没有直接的实现方案,需要利用Qt的ActiveX兼容模块与浏览器往返多次交互
- Qt ActiveX未实现COM事件通知
- 官方Win32示例存在滥用lambda函数的嫌疑,lambda函数多层嵌套,程序逻辑层次混乱,整个逻辑被揉成一垛。
官方示例代码
官方介绍文档在这里:https://learn.microsoft.com/microsoft-edge/webview2/get-started/win32。官方代码仓库在这里:GitHub - MicrosoftEdge/WebView2Samples: Microsoft Edge WebView2 samples
摘录一段lambda多层嵌套的代码,你们体会一下:
CreateCoreWebView2EnvironmentWithOptions(nullptr, nullptr, nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[hWnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
// 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(&webview);
}
// Add a few settings for the webview
// The demo step is redundant since the values are the default settings
wil::com_ptr<ICoreWebView2Settings> settings;
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
webview->Navigate(L"https://www.bing.com/");
// <NavigationEvents>
// Step 4 - Navigation events
// register an ICoreWebView2NavigationStartingEventHandler to cancel any non-https navigation
EventRegistrationToken token;
webview->add_NavigationStarting(Callback<ICoreWebView2NavigationStartingEventHandler>(
[](ICoreWebView2* webview, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
wil::unique_cotaskmem_string uri;
args->get_Uri(&uri);
std::wstring source(uri.get());
if (source.substr(0, 5) != L"https") {
args->put_Cancel(true);
}
return S_OK;
}).Get(), &token);
// </NavigationEvents>
// <Scripting>
// Step 5 - Scripting
// Schedule an async task to add initialization script that freezes the Object object
webview->AddScriptToExecuteOnDocumentCreated(L"Object.freeze(Object);", nullptr);
// Schedule an async task to get the document URL
webview->ExecuteScript(L"window.document.URL;", Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
[](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT {
LPCWSTR URL = resultObjectAsJson;
//doSomethingWithURL(URL);
return S_OK;
}).Get());
// </Scripting>
// <CommunicationHostWeb>
// Step 6 - Communication between host and web content
// Set an event handler for the host to return received message back to the web content
webview->add_WebMessageReceived(Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[](ICoreWebView2* webview, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
wil::unique_cotaskmem_string message;
args->TryGetWebMessageAsString(&message);
// processMessage(&message);
webview->PostWebMessageAsString(message.get());
return S_OK;
}).Get(), &token);
// Schedule an async task to add initialization script that
// 1) Add an listener to print message from the host
// 2) Post document URL to the host
webview->AddScriptToExecuteOnDocumentCreated(
L"window.chrome.webview.addEventListener(\'message\', event => alert(event.data));" \
L"window.chrome.webview.postMessage(window.document.URL);",
nullptr);
// </CommunicationHostWeb>
return S_OK;
}).Get());
return S_OK;
}).Get());
解决方案
下面以实现自动登录外网网关为目标,企业微信自动上线,免开机输入账号密码。这样领导看到你上线,可以开机以后就可以慢慢吃早餐了。(开个玩笑)
本实现方案把官方示例代码做成了静态库,没有添加其它东西。
新建CMake项目
这里用到了Qt5静态库,目的是单文件可执行,不需要部署。需要静态库的读者可以自行删除。代码如下:
cmake_minimum_required(VERSION 3.21)
project(auto-online CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
add_definitions(/D_UNICODE /DUNICODE)
add_compile_definitions(WIN32 _WINDOWS)
add_compile_options(/utf-8 $<IF:$<CONFIG:Debug>,/MTd,/MT>)
link_directories($ENV{Qt5_DIR}/lib)
link_directories($ENV{Qt5_DIR}/plugins/platforms)
link_directories($ENV{Qt5_DIR}/plugins/imageformats)
link_libraries(UxTheme Winmm Version ws2_32 imm32 dwmapi)
link_libraries($<IF:$<CONFIG:Debug>,Qt5FontDatabaseSupportd,Qt5FontDatabaseSupport>)
link_libraries($<IF:$<CONFIG:Debug>,Qt5UiToolsd,Qt5UiTools>)
link_libraries($<IF:$<CONFIG:Debug>,Qt5AccessibilitySupportd,Qt5AccessibilitySupport>)
link_libraries($<IF:$<CONFIG:Debug>,Qt5EventDispatcherSupportd,Qt5EventDispatcherSupport>)
link_libraries($<IF:$<CONFIG:Debug>,Qt5ThemeSupportd,Qt5ThemeSupport>)
link_libraries($<IF:$<CONFIG:Debug>,Qt5UiToolsd,Qt5UiTools>)
link_libraries($<IF:$<CONFIG:Debug>,qtpcre2d,qtpcre2>)
link_libraries($<IF:$<CONFIG:Debug>,qtlibpngd,qtlibpng>)
link_libraries($<IF:$<CONFIG:Debug>,qtharfbuzzd,qtharfbuzz>)
link_libraries($<IF:$<CONFIG:Debug>,qtfreetyped,qtfreetype>)
link_libraries($<IF:$<CONFIG:Debug>,qwindowsd,qwindows>)
link_libraries($<IF:$<CONFIG:Debug>,qicnsd,qicns>)
link_libraries($<IF:$<CONFIG:Debug>,qtgad,qtga>)
link_libraries($<IF:$<CONFIG:Debug>,qtiffd,qtiff>)
link_libraries($<IF:$<CONFIG:Debug>,qwbmpd,qwbmp>)
link_libraries($<IF:$<CONFIG:Debug>,qtiffd,qtiff>)
link_libraries($<IF:$<CONFIG:Debug>,qwebpd,qwebp>)
link_libraries($<IF:$<CONFIG:Debug>,qgifd,qgif>)
link_libraries($<IF:$<CONFIG:Debug>,qjpegd,qjpeg>)
link_libraries($<IF:$<CONFIG:Debug>,qicod,qico>)
message("-- Qt5_DIR: " $ENV{Qt5_DIR})
find_package(Qt5 COMPONENTS Core Gui Widgets Network REQUIRED)
include_directories(${CMAKE_SOURCE_DIR}/3rdparty/webview2loader/include)
include_directories(${CMAKE_SOURCE_DIR}/3rdparty/wil/include)
link_directories(${CMAKE_SOURCE_DIR}/3rdparty/webview2loader/lib)
link_libraries(Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network WebView2LoaderStatic)
file(GLOB SRCS *.ui *.cpp *.h)
add_executable(${PROJECT_NAME} WIN32 ${SRCS})
set_directory_properties(PROPERTIES VS_STARTUP_PROJECT ${PROJECT_NAME})
# 环境变量的路径不能带双引号
# message("VCINSTALLDIR: " $ENV{VCINSTALLDIR})
# find_file(VSPATH NAMES "vcruntime140d.dll" PATHS $ENV{VCINSTALLDIR} REQUIRED NO_DEFAULT_PATH)
# file(TO_NATIVE_PATH ${VSPATH} VSPATH)
#message("VC CRT PATH: " ${VSPATH})
# set(VSPATH $<IF:$<CONFIG:Debug>,$ENV{VCINSTALLDIR}/Redist/MSVC/14.29.30133/onecore/debug_nonredist/x64/Microsoft.VC142.DebugCRT,$ENV{VCINSTALLDIR}/Redist/MSVC/14.29.30133/x64/Microsoft.VC142.CRT> CACHE STRING "VCRT" FORCE)
# string(CONCAT VSPATH ${VSPATH} ";$ENV{Qt5_DIR}\\bin")
set_target_properties(${PROJECT_NAME} PROPERTIES
VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
VS_DEBUGGER_ENVIRONMENT "Path=$ENV{Qt5_DIR}\\bin"
WIN32_EXECUTABLE TRUE)
用Qt Designer设计一个简单的窗口类型
类型名字很简单,就是MainWindow。这个窗口仅启用了布局,没有添加任何控件。UI代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="central_widget">
<layout class="QVBoxLayout" name="vl1">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="browser_widget" native="true">
<layout class="QVBoxLayout" name="vl2"/>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
声明信号槽
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include <windows.h>
#include <stdlib.h>
#include <string>
#include <tchar.h>
#include "wrl.h"
#include "wil/com.h"
#include "WebView2.h"
#define PROFILE_DATA "cw_webview2"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
void load_webview2(HWND hWnd);
void task_run();
HRESULT cb_create_environment(HWND, HRESULT, ICoreWebView2Environment *);
HRESULT cb_create_controller(HWND, HRESULT, ICoreWebView2Controller *);
HRESULT on_navigate_started(ICoreWebView2 *, ICoreWebView2NavigationStartingEventArgs *);
HRESULT cb_execute_script(HRESULT, LPCWSTR);
HRESULT on_webmessage_received(ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *);
HRESULT on_navigate_completed(ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs*);
// HRESULT cb_deliver(HWND, HRESULT, ...);
signals:
void prepare();
void load_auth_page();
void put_name();
void put_password();
void click_login();
void wait(int iminute);
protected:
void resizeEvent(QResizeEvent* evt) override;
void closeEvent(QCloseEvent*) override;
protected slots:
void on_prepare();
void on_load_auth_page();
void on_put_name();
void on_put_password();
void on_click_login();
void on_wait(int iminute);
private:
Ui::MainWindow *ui;
// Pointer to WebViewController
wil::com_ptr<ICoreWebView2Controller> m_controller;
bool m_brunning = false;
QThread* m_task;
// Pointer to WebView window
wil::com_ptr<ICoreWebView2_15> m_webview;
Microsoft::WRL::ComPtr<ICoreWebView2ExecuteScriptCompletedHandler> js_cb;
Microsoft::WRL::ComPtr<ICoreWebView2WebMessageReceivedEventHandler> msg_cb;
};
#endif // MAINWINDOW_H
在合适的时机填入账号密码
本来想做成弹框让用户输入账号密码,忽然发现这种做法对信息部门的同事不太友好,各位读者如有请自行修改。示例代码中的网关地址、用户名和密码是乱写的。
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QResizeEvent>
#include <QRect>
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
#include <QMessageBox>
#include <QTimer>
#include <QApplication>
using namespace Microsoft::WRL;
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
load_webview2((HWND)ui->browser_widget->winId());
setWindowTitle(u8"自动连接外网工具");
connect(this, SIGNAL(prepare()), this, SLOT(on_prepare()), Qt::QueuedConnection);
connect(this, SIGNAL(load_auth_page()), this, SLOT(on_load_auth_page()), Qt::QueuedConnection);
connect(this, SIGNAL(put_name()), this, SLOT(on_put_name()), Qt::QueuedConnection);
connect(this, SIGNAL(put_password()), this, SLOT(on_put_password()), Qt::QueuedConnection);
connect(this, SIGNAL(click_login()), this, SLOT(on_click_login()), Qt::QueuedConnection);
connect(this, SIGNAL(wait(int)), this, SLOT(on_wait(int)), Qt::QueuedConnection);
}
MainWindow::~MainWindow()
{
delete ui;
}
HRESULT MainWindow::on_navigate_started(ICoreWebView2 *webview,
ICoreWebView2NavigationStartingEventArgs *args)
{
wil::unique_cotaskmem_string uri;
args->get_Uri(&uri);
QString zurl = QString::fromStdWString(uri.get());
// std::wstring source(uri.get());
// if (source.substr(0, 5) != L"https")
// {
// args->put_Cancel(true);
// }
qInfo().noquote() << "navigate: " << zurl;
return S_OK;
}
HRESULT MainWindow::cb_execute_script(HRESULT errorCode, LPCWSTR resultObjectAsJson)
{
LPCWSTR URL = resultObjectAsJson;
//doSomethingWithURL(URL);
qInfo() << "executed javascript.";
return S_OK;
}
HRESULT MainWindow::on_webmessage_received(ICoreWebView2 *webview,
ICoreWebView2WebMessageReceivedEventArgs *args)
{
wil::unique_cotaskmem_string message;
args->TryGetWebMessageAsString(&message);
// processMessage(&message);
//webview->PostWebMessageAsString(message.get());
QString zmsg = QString::fromStdWString(message.get());
qInfo().noquote() << "message: " << zmsg;
return S_OK;
}
HRESULT MainWindow::on_navigate_completed(ICoreWebView2 *webview,
ICoreWebView2NavigationCompletedEventArgs *args)
{
BOOL bsuccess;
COREWEBVIEW2_WEB_ERROR_STATUS ierror;
args->get_IsSuccess(&bsuccess);
args->get_WebErrorStatus(&ierror);
LPWSTR lpuri;
webview->get_Source(&lpuri);
QString zuri = QString::fromStdWString(lpuri);
qInfo().noquote() << "complate : " << bsuccess << ", " << ierror << ", " << zuri;
return S_OK;
}
void MainWindow::resizeEvent(QResizeEvent *evt)
{
if (!m_controller)
return;
RECT rc;
QRect qrc = ui->browser_widget->rect();
rc.left = qrc.left();
rc.top = qrc.top();
rc.right = qrc.right();
rc.bottom = qrc.bottom();
m_controller->put_Bounds(rc);
}
void MainWindow::closeEvent(QCloseEvent *)
{
m_brunning = false;
m_task->terminate();
QThread::sleep(1);
m_task->deleteLater();
m_task = nullptr;
}
void MainWindow::on_prepare()
{
if (!m_webview)
return;
m_webview->NavigateToString(L"Preparing...");
}
void MainWindow::on_load_auth_page()
{
if (!m_webview)
return;
m_webview->Navigate(L"http://1.1.1.3/ac_portal/default/pc.html?tabs=pwd");
qInfo() << "begin load auth page";
}
void MainWindow::on_put_name()
{
if (!m_webview)
return;
m_webview->ExecuteScript(L"$('#password_name').val('test1');"
"window.chrome.webview.postMessage('put name ok.');",
js_cb.Get());
}
void MainWindow::on_put_password()
{
if (!m_webview)
return;
m_webview->ExecuteScript(L"$('#password_pwd').val('123456');"
"window.chrome.webview.postMessage('put password ok.');",
js_cb.Get());
}
void MainWindow::on_click_login()
{
if (!m_webview)
return;
m_webview->ExecuteScript(L"$('#password_submitBtn').click();"
"window.chrome.webview.postMessage('click login ok.');",
js_cb.Get());
}
void MainWindow::on_wait(int iminute)
{
QString zhtml = QString("Already wairted %1 minutes.").arg(iminute);
if (!m_webview)
return;
m_webview->NavigateToString(zhtml.toStdWString().c_str());
}
HRESULT MainWindow::cb_create_controller(HWND hWnd, HRESULT result,
ICoreWebView2Controller *controller)
{
if (!controller)
return E_POINTER;
m_controller = controller;
wil::com_ptr<ICoreWebView2> webview;
HRESULT ir = m_controller->get_CoreWebView2(&webview);
if (FAILED(ir))
return ir;
ir = webview->QueryInterface(IID_ICoreWebView2_15, (void **)&m_webview);
if (FAILED(ir))
return ir;
// Add a few settings for the webview
// The demo step is redundant since the values are the default settings
wil::com_ptr<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);
m_controller->put_Bounds(bounds);
m_webview->NavigateToString(L"Preparing...");
// <NavigationEvents>
// Step 4 - Navigation events
// register an ICoreWebView2NavigationStartingEventHandler to cancel any non-https navigation
EventRegistrationToken token;
auto nav_func = std::bind(&MainWindow::on_navigate_started, this, std::placeholders::_1,
std::placeholders::_2);
auto nav_cb = Callback<ICoreWebView2NavigationStartingEventHandler>(nav_func);
m_webview->add_NavigationStarting(nav_cb.Get(), &token);
auto com_func = std::bind(&MainWindow::on_navigate_completed, this, std::placeholders::_1,
std::placeholders::_2);
auto com_cb = Callback<ICoreWebView2NavigationCompletedEventHandler>(com_func);
m_webview->add_NavigationCompleted(com_cb.Get(), &token);
// </NavigationEvents>
// <Scripting>
// Step 5 - Scripting
// Schedule an async task to add initialization script that freezes the Object object
// 注入脚本
//webview->AddScriptToExecuteOnDocumentCreated(L"Object.freeze(Object);", nullptr);
// Schedule an async task to get the document URL
auto js_func = std::bind(&MainWindow::cb_execute_script, this, std::placeholders::_1,
std::placeholders::_2);
js_cb = Callback<ICoreWebView2ExecuteScriptCompletedHandler>(js_func);
//webview->ExecuteScript(L"window.document.URL;", js_cb.Get());
// </Scripting>
// <CommunicationHostWeb>
// Step 6 - Communication between host and web content
// Set an event handler for the host to return received message back to the web content
auto msg_func = std::bind(&MainWindow::on_webmessage_received, this, std::placeholders::_1,
std::placeholders::_2);
msg_cb = Callback<ICoreWebView2WebMessageReceivedEventHandler>(msg_func);
m_webview->add_WebMessageReceived(msg_cb.Get(), &token);
// Schedule an async task to add initialization script that
// 1) Add an listener to print message from the host
// 2) Post document URL to the host
// 注入脚本
//webview->AddScriptToExecuteOnDocumentCreated(
// L"window.chrome.webview.addEventListener(\'message\', event => alert(event.data));" \
// L"window.chrome.webview.postMessage(window.document.URL);",
// nullptr);
// </CommunicationHostWeb>
m_task = QThread::create(&MainWindow::task_run, this);
m_task->setParent(this);
m_task->start();
return S_OK;
}
void MainWindow::load_webview2(HWND hWnd)
{
MainWindow *obj = this;
auto func = &MainWindow::cb_create_environment;
auto fn = [hWnd, obj, func](HRESULT result, ICoreWebView2Environment * env)->HRESULT
{
return (obj->*func)(hWnd, result, env);
};
auto cb = Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(fn);
QDir d(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
auto ztemp = d.absoluteFilePath(PROFILE_DATA).toStdWString();
HRESULT ir = CreateCoreWebView2EnvironmentWithOptions(nullptr, ztemp.c_str(), nullptr, cb.Get());
if (FAILED(ir))
{
qCritical() << "create webview2 failed. code: " << ir;
QTimer::singleShot(3000, []()
{
QApplication::quit();
_Exit(1);
});
QMessageBox::warning(this,
windowTitle(),
u8"严重错误。程序即将退出。\n"
"请检查Microsoft Edge WebView2 Runtime是否存在。\n");
}
}
void MainWindow::task_run()
{
if (!m_controller || !m_webview)
return;
m_brunning = true;
while (m_brunning)
{
emit on_prepare();
QThread::sleep(3);
if (!m_brunning)
break;
emit load_auth_page();
QThread::sleep(10);
if (!m_brunning)
break;
emit put_name();
QThread::sleep(3);
if (!m_brunning)
break;
emit put_password();
QThread::sleep(3);
if (!m_brunning)
break;
emit click_login();
for (int i = 0; i < 10; i++)
{
QThread::sleep(60);
if (!m_brunning)
break;
emit wait(i + 1);
}
}
}
HRESULT MainWindow::cb_create_environment(HWND hWnd, HRESULT result, ICoreWebView2Environment *env)
{
MainWindow *obj = this;
auto func = &MainWindow::cb_create_controller;
auto fn = [hWnd, obj, func](HRESULT result, ICoreWebView2Controller * controller)->HRESULT
{
return (obj->*func)(hWnd, result, controller);
};
// Create a CoreWebView2Controller and get the associated CoreWebView2 whose parent is the main window hWnd
auto cb = Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(fn);
HRESULT hr = env->CreateCoreWebView2Controller(hWnd, cb.Get());
return hr;
}
免责声明
此代码仅供技术研究娱乐之用,禁止用于商业用途,否则一切后果自负。
作者:岬淢箫声
岬淢箫声的博客_CSDN博客-C/C++,MFC/VC,桌面H5领域博主https://caowei.blog.csdn.net/
转载请注明来源