QT集成CEF12-JavaScript监听文件夹与文件变化
本节要实现的目标是: 监听本地磁盘文件变化:
- 文件夹中新增文件
- 文件夹中的文件被删除
- 文件内容改变
一旦发生这些变化,这些变化将作为“事件”发送给HTML网页,网页中JavaScript 注册的监听函数开始执行处理事件。其实本质上实现的就是Browser进程发送消息到Render进程, Render进程执行 JavaScript注册的回调函数,同时传递从Browser进程发送过来的消息内容,最终消息的内容在JavaScript回调函数中处理。
主要步骤如下:
-
Browser进程侧:
- 主窗体监听文件夹,使用
QFileSystemWatcher
, 取得变化内容后封装成一个自定义的 Event结构体 - 调用browser中的frame 的 SendProcessMessage 方法,将 自定义的Event结构体转换为 CefProcessMessage 发送给 Renderer进程。
- 主窗体监听文件夹,使用
-
Renderer进程侧:
-
在 OnContextCreated 函数中注册JavaScript 函数,其函数原型为:
// 添加事件监听器,后端有事件发生,会执行callback回调函数来让JavaScript来处理数据 // eventName 事件名称,字符串 // callback 当事件发生以后,执行的事件处理函数,原型为: // callback(eventData) {} evetnData为事件数据 app.addEventListener(eventName,callback); // 删除时间监听 app.removeEventListener(eventName);
-
自定义 CefV8Handler,在这个Handler中保存所有注册的 callback函数
-
OnProcessMessageReceived 中接收Browser进程中发送的消息,取出消息内容
-
遍历所有JavaScript注册的callback函数,并执行
-
JavaScript执行回调函数。
-
1. Browser进程侧
Browser进程侧主要完成文件变化监控,然后将监控到的消息发送到Renderer进程侧
1. 1 文件变化监控
定义一个枚举FileChangeEventType
,表示文件变化类型. 定义一个事件FileChangeEvent
,保存事件信息
// filesystemwatcher.h
// 文件变化类型
typedef enum _FileChangeEventType {
// 新建文件夹或文件,删除文件夹或文件,新建文件或文件夹,删除文件或文件夹,更新文件, 重命名文件或文件夹
NEW, REMOVE, UPDATE_FILE, RENAME
} FileChangeEventType;
// 文件变化事件内容
typedef struct _FileChangeEvent
{
FileChangeEventType fileChangeEventType; //事件类型
QString message; //消息
}FileChangeEvent;
可以使用 QFileSystemWatcher 监控文件/文件夹变化。一般应用场景中主要是对指定的文件夹进行监控,但是新增了哪个文件,删除了哪个文件无法监控到,需要对它做一些逻辑处理。这里自定义了 FileSystemWatcher 类对QFileSystemWatcher 做了封装:
// filesystemwatcher.h
class FileSystemWatcher : public QObject
{
Q_OBJECT
public:
explicit FileSystemWatcher(QObject* parent = 0);
void addWatchPath(QString path);
signals:
// 文件改变信号
void onFileChangeEventTrigger(FileChangeEvent event);
public slots:
void directoryUpdated(const QString& path); // 目录更新时调用,path是监控的路径
void fileUpdated(const QString& path); // 文件被修改时调用,path是监控的路径
private:
QFileSystemWatcher* m_pSystemWatcher; // QFileSystemWatcher变量
QMap<QString, QStringList> m_currentContentsMap; // 当前每个监控的内容目录列表
};
该类中定义了一个onFileChangeEventTrigger 信号,当文件发生改变时发射这个信号。这个信号会与 SimpleHandler 中的槽函数关联,SimpleHandler的槽函数会找到 浏览器中的frame,然后再发送信号。
filesystemwatcher.cpp 直接参考源代码
1.2 MainWindow中开启监控
mainWindow中定义成员变量m_FileSystemWatcher
,构造函数中实例化它,并开启监控。
mainWindow中创建浏览器窗体的时候,将FileSystemWatcher中的onFileChageEventTrigger
信号关联到SimpleHandler中的槽函数onFileChageEventTrigger
。
// mainwindow.h
// ... 省略
class MainWindow : public QMainWindow {
// ... 省略
private:
FileSystemWatcher* m_FileSystemWatcher;
}
// mainwindow.cpp
MainWindow::MainWindow(SimpleApp* cefApp, QWidget* parent)
: QMainWindow(parent), m_cefApp(cefApp),
m_cef_query_handler(new CefQueryHandler),
m_FileSystemWatcher(new FileSystemWatcher(this)) //初始化FileSystemWatcher
{
// ... 省略
// 开启监控
QString watchPath = "E:\\tmp";
m_FileSystemWatcher->addWatchPath(watchPath);
}
// ... 省略
// FileSystemWatcher 中的onFileChangeEventTrigger 信号关联到 SimpleHandler中的onFileChageEventTrigger 槽函数
void MainWindow::createBrowserWindow() {
CefRefPtr<SimpleHandler> handler(new SimpleHandler(false, m_cef_query_handler));
// 当文件发生变化时,通知到SimpleHandler
connect(m_FileSystemWatcher, &FileSystemWatcher::onFileChangeEventTrigger, handler, &SimpleHandler::onFileChageEventTrigger);
// ... 省略
}
1.3 SimpleHandler 中定义槽函数
定义槽函数onFileChageEventTrigger
在槽函数中找到浏览器对象,然后找到frame向render进程发送消息。
/// <summary>
/// 文件发生变化槽函数
/// </summary>
/// <param name="fileChangeEvent"></param>
void SimpleHandler::onFileChageEventTrigger(FileChangeEvent fileChangeEvent) {
// 要与Renderer接收端保持一致
std::string notify_message = "FILE_CHAGE_EVENT_TRIGGER_NOTIFY";
//创建消息
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(notify_message);
//枚举转换为字符串
QString str = "NEW,REMOVE,UPDATE_FILE,RENAME";
QStringList list2 = str.split(",");
std::string eventType = list2[fileChangeEvent.fileChangeEventType].toStdString();
qDebug() << "=====发送消息给Renderer进程=======event Type:" << QString::fromStdString(eventType);
// 发送消息给 Renderer进程
CefRefPtr<CefListValue> args = msg->GetArgumentList();
args->SetSize(2); //2个参数
args->SetString(0, eventType); //事件类型
args->SetString(1, fileChangeEvent.message.toStdString());//消息内容
// 发送消息给Browser进程
// 获取浏览器对象
if (browser_list_.empty()) {
return;
}
CefRefPtr<CefFrame> frame = browser_list_.front()->GetMainFrame();
// 给Renderer 进程发送消息
frame->SendProcessMessage(PID_RENDERER, msg);
}
2. Renderer进程侧
首先考虑我们需要为window对象绑定两个函数:
// 添加事件监听器,后端有事件发生,会执行callback回调函数来让JavaScript来处理数据
// eventName 事件名称,字符串
// callback 当事件发生以后,执行的事件处理函数,原型为:
// callback(eventData) {} evetnData为事件数据
app.addEventListener(eventName,callback);
// 删除时间监听
app.removeEventListener(eventName);
使用它们可以注册事件监听函数。
注册的callback要映射到c++的native方法上。
2.1 绑定addEventListener与removeEventListener
绑定有回调的JavaScript函数这里选择在 OnContextCreated
函数中进行:
// QyAppRenderer.h
class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
// ...省略
private:
// 浏览器
CefRefPtr<CefBrowser> m_mainBrowser;
AppEventListenerV8Handler* m_appEventListenerV8Handler;
}
// QyAppRenderer.cpp
void QyAppRenderer::OnBrowserCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefDictionaryValue> extra_info) {
//保存浏览器对象
m_mainBrowser = browser;
}
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
// 注册 window.cefQuery
//注册监听器
Retrieve the context's window object.
CefRefPtr<CefV8Value> window = context->GetGlobal();
//创建一个JavaScript 对象,全部为只读属性
CefRefPtr<CefV8Value> appObject = CefV8Value::CreateObject(NULL, NULL);
// JavaScript 函数就需要 CefV8Handler 来处理。这里使用了前面定义的 AppNativeV8Handler
m_appEventListenerV8Handler = new AppEventListenerV8Handler();
CefRefPtr<CefV8Value> funcAddEventListener = CefV8Value::CreateFunction("addEventListener", m_appEventListenerV8Handler);
CefRefPtr<CefV8Value> funcRemoveEventListener = CefV8Value::CreateFunction("removeEventListener", m_appEventListenerV8Handler);
appObject->SetValue("addEventListener", funcAddEventListener, V8_PROPERTY_ATTRIBUTE_NONE);
appObject->SetValue("removeEventListener", funcRemoveEventListener, V8_PROPERTY_ATTRIBUTE_NONE);
//绑定到window对象上,同样为只读属性
window->SetValue("app", appObject, V8_PROPERTY_ATTRIBUTE_READONLY);
}
这里创建了一个JavaScript中的 app对象,然后在 app中挂载了两个函数:addEventListener
用来注册监听器,removeEventListener
用来移除监听。
这两个JavaScript函数的调用由自定义的AppEventListenerV8Handler
类来处理
2.2 AppEventListenerV8Handler 处理器
在处理器中,需要对注册进来的 JavaScript回调函数保存起来,以便在收到 Browser进程的消息后能够调用到。
它除了保存回调函数,还要提供一个函数,能够共外部调用,触发回调函数的调用。
// app_event_listener_v8_handler.h
/// <summary>
/// 将回调函数和上下文包装在一起
/// </summary>
typedef struct _EventListener
{
CefRefPtr<CefV8Value> callback_; // 注册的回调函数
CefRefPtr<CefV8Context> context_;// javaScript 上下文
} EventListener;
typedef QMap < QString, QList<EventListener>> CallbackMap;
class AppEventListenerV8Handler :public CefV8Handler
{
public:
bool Execute(const CefString& name, //JavaScript 函数名
CefRefPtr<CefV8Value> object, //JavaScript函数持有者
const CefV8ValueList& arguments,//JavaScript 参数
CefRefPtr<CefV8Value>& retval, // JavaScript返回值
CefString& exception) override;
// 执行通知,调用回调函数
void executeNotify(QString eventType, QString content);
private:
// 保存回调函数映射
CallbackMap m_callbackMap;
IMPLEMENT_REFCOUNTING(AppEventListenerV8Handler);
};
app_event_listener_v8_handler.cpp 见github 源码
这里对其中的 executeNotify
做一下说明:在这个函数中,我们是需要调用JavaScript注册的回调函数的。这里一定要注意,根据事件名称取出了所有注册的回调函数以后,**一定要获取上下文后,调用上下文的Enter() 函数 ** 进入上下文。因为JavaScript的执行一定是要在上下文中的。
当消息到来的时候,此时就调用JavaScript的回调是无法完成的。因为此时的JavaScript已经离开了 Context,所以要获取到Context后进入以后才能执行JavaScript 回调。
void AppEventListenerV8Handler::executeNotify(QString eventType, QString content) {
// 文件夹改变
if (m_callbackMap.contains(eventType)) {
QList<EventListener> callbackList = m_callbackMap[eventType];
qDebug() << QString("AppEventListenerV8Handler::executeNotify====>%1===>%2").arg(eventType).arg(callbackList.size());
for (auto it = callbackList.begin(); it != callbackList.end(); ++it) {
// 回调函数参数值
CefRefPtr<CefV8Value> callbackFunctionParam = CefV8Value::CreateString(content.toStdString());
CefV8ValueList arguments;
arguments.push_back(callbackFunctionParam);
// 执行JavaScript回调,并将参数传递给它,参数是一个CefV8ValueList
CefRefPtr<CefV8Context> context = (*it).context_;
CefRefPtr<CefV8Value> callback = (*it).callback_;
context.get()->Enter();//进入上下文
callback->ExecuteFunction(NULL, arguments);
context.get()->Exit();//退出上下文
}
}
}
2.4 接收Browser 进程消息
最后就是接收Browser进程额消息了。当Browser 进程有消息发送过来的时候,会执行OnProcessMessageReceived
,所以需要在 QyAppRenderer 中实现 OnProcessMessageReceived
函数:
从 CefProcessMessage 中取出传递过来的消息,然后调用 AppEventListenerV8Handler 中的executeNotify
函数,将消息交给回调函数执行带入到HTML中。
bool QyAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
// 收到 browser进程发送的消息
CEF_REQUIRE_RENDERER_THREAD();
DCHECK_EQ(source_process, PID_BROWSER);
// 取得消息的名字
QString msgName = QString::fromStdString(message.get()->GetName());
if (msgName == "FILE_CHAGE_EVENT_TRIGGER_NOTIFY") { //与 Browser进程中的一致
//取出内容
CefString eventType = message.get()->GetArgumentList().get()->GetString(0); //第0个参数
CefString content = message.get()->GetArgumentList().get()->GetString(1); //第1个参数
qDebug() << "收到browser 进程消息:" << QString::fromStdString(content);
// 调用JS 回调
m_appEventListenerV8Handler->executeNotify(QString::fromStdString(eventType), QString::fromStdString(content));
return true;
}
return m_message_router->OnProcessMessageReceived(browser, frame, source_process, message);
}
2.4 JavaScript 注册回调
现在要接收文件夹变化,只需要在 JavaScript中注册监听器即可:
// index.js
window.onload = () => {
console.log(window.app);
if (window.app && window.app.addEventListener) {
function handleFileChangeEvent(data) {
// 将结果放入到div中
var html = $("#msgContent").html() + "<br />" + data;
$("#msgContent").html(html);
console.log("=======>" + data);
}
//NEW, REMOVE, UPDATE_FILE, RENAME
// 添加监听函数
app.addEventListener("NEW", handleFileChangeEvent);
app.addEventListener("REMOVE", handleFileChangeEvent);
app.addEventListener("UPDATE_FILE", handleFileChangeEvent);
app.addEventListener("RENAME", handleFileChangeEvent);
}
}
3. 编译运行
Browser进程中,监控的是 E:\tmp 文件夹,所以在这个文件夹中添加删除文件,文件夹测试 一下:
代码请访问 GitHub qt_cef_12分支