迁移通知
本站内容正在逐步向 [[https://www.weiran.ink]] 迁移,更新内容请到新站查找。
---
# 在Qt Webengine中使用Alpaca获取本地文件路径
上一篇文章“[[:Coding:Web:Custom_input_file_contorl_under_specific_class_via_CSS|使用CSS定制某类元素下的input文件选择控件]]”提到,折腾半天实现了对于Qt web engine的定制和修复,然后才发现,由于安全机制的作用,html中的``是不能获取本地文件路径的。
还好,环境不是公众浏览器而是Qt web engine客户端,本身就提供了`JS`与`C++`通讯的功能,相当于可以魔改浏览器。文章“[[:Coding:Cpp:Qt_webengine_runJavaScript_cannot_assign_to_local_variable|Qt Webengine回调函数内赋值引发Segment Fault的灵异事件]]”已经提供了一个这样的案例,不过这次要做的通讯更复杂得多,包括:
+ 通过对`Alpaca`表单生成器的定制,使得在用户点击某个需要填写本地文件路径的表单输入框时,主动发起`C++`侧选择本地文件的操作;
+ 在`C++`侧完成文件选择之后,将路径字符串传递回到`JS`侧;
+ 将回传的文件/文件夹路径写入到`Alpaca`表单中;
## 对Alpaca表单生成器的定制
`Alpaca`原生提供了定义自定义`field`的功能,基本上就是声明一个类,重载一些处理函数,然后`注册`一下该类型的名字,最后在表单生成时标明某个`field`属于这个自定义的类就行了:
自定义类,这里我定义了两个,分别用来选择文件和文件夹:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
$.alpaca.Fields.LocalFileChooser = $.alpaca.Fields.TextField.extend({
getFieldType: function () {
return "localfile";
},
onClick: async function (e) {
userIsChoosing = true;
localFileHelper.chooseFile();
while (userIsChoosing) { await sleep(100); }
this.setValue(recentChosenFilePath);
this.refreshValidationState();
},
});
Alpaca.registerFieldClass("localfile", Alpaca.Fields.LocalFileChooser);
$.alpaca.Fields.LocalDirChooser = $.alpaca.Fields.TextField.extend({
getFieldType: function () {
return "localdir";
},
onClick: async function (e) {
userIsChoosing = true;
localFileHelper.chooseDir();
while (userIsChoosing) { await sleep(100); }
this.setValue(recentChosenDirPath);
this.refreshValidationState();
},
});
Alpaca.registerFieldClass("localdir", Alpaca.Fields.LocalDirChooser);
这一段不涉及什么别的对象,在加载`alpaca`之后就可以运行。其中出现了两个奇怪的东西,分别是`localFileHelper`和`while (userIsChoosing) { await sleep(100); }`这么一个循环。
`localFileHelper`是通过`Qt`跟`C++`侧通讯用的对象,可以简单的理解为它实际上指向那个`C++`对象,可以调用其中的一些函数。`chooseFile()`和`chooseDir()`就是处理好了可以调用的函数。
由于这些调用全都是通过`Qt`的`信号/槽`机制进行的,所以全都是没有返回值的异步调用。为此,需要设定等待机制:先把全局变量`userIsChoosing`设定为`true`,再发起调用,随后一直等待(注意`onClick`函数的`async`标记)。直到`C++`侧用户操作完毕,由`C++`侧主动把选择后的数据传递到全局变量`recentChosenFilePath/recentChosenDirPath`中,再主动将`userIsChoosing`置为`false`。这头发现`userIsChoosing`被置为`false`之后,跳出等待,把`C++`送来的值写入到表单(`SetValue`函数),随后需要主动触发一次`refreshValidationState`函数,把表单框可能有的“不能为空”或者别的验证提示刷掉。
截至到此,都是些民用版的`JS`相关,随便什么非战斗人员都能看懂的。麻烦的在下面。
## C++与JS的通信
这里需要两头操作。首先,通讯是通过`Qt`的`signal/slot`进行的,涉及这玩意儿的代码很容易就会变得比较细碎。为了干净,先搞一个`C++`类用来做通信中转:
#ifndef LOCALFILESYSTEMJSHELPER_H
#define LOCALFILESYSTEMJSHELPER_H
#include
class LocalFileSystemJSHelper : public QObject {
Q_OBJECT
Q_PROPERTY(QString m_chosenFilePath MEMBER m_chosenFilePath NOTIFY chosenFilePathChanged)
Q_PROPERTY(QString m_chosenDirPath MEMBER m_chosenDirPath NOTIFY chosenDirPathChanged)
public:
LocalFileSystemJSHelper(QWidget *targetWebview, QWidget *parent = nullptr);
public slots:
void chooseFile();
void chooseDir();
signals:
void chosenFilePathChanged(const QString &newChosenFilePath);
void chosenDirPathChanged(const QString &newChosenDirPath);
private:
QWidget *_targetWebview, *_parentWidget;
QString m_chosenFilePath, m_chosenDirPath;
void setWebViewLock(bool status);
};
#endif // LOCALFILESYSTEMJSHELPER_H
要搞信号和槽,首先必须是`QObject`,这个不解释。紧随其后的`Q_PROPERTY`相关语句,有些教程里提到了,但经我实验没有也行,估计是跟`QML`通信用的,`Qt webengine`中与JS的通信并不依赖这个。
要通信的关键是下面的几个信号和槽。若要让一个函数能够被`JS`主动调用,其必须是一个`slot`,如`ChooseFile()`函数和`ChooseDir()`函数。这俩函数分别调用`Qt`的对话框提示用户选择文件/文件夹。
信号和槽都是没有返回值的——因为这套机制本身就是建立在异步和多线程的基础上,文章“[[:Coding:CAEDevelop:Call_external_executable_and_fetch_output_by_qt|通过Qt优雅地调用命令行计算程序并抓取输出显示在窗口上]]”提供了姿势更丰富的案例。因此,为了把选择结果传回去,还需要从`C++`侧发送两个信号,分别是`chosenFilePathChanged`和`chosenDirPathChanged`,这两个信号应该在`ChooseFile()`函数和`ChooseDir()`函数的末尾被发送:`emit chosenDirPathChanged(m_chosenDirPath);`,信号发送出去之后,`JS`侧绑定好了的函数就会被调用并收到`m_chosenDirPath`中的数据。
其他成员、参数什么的都是为了配合GUI而存在的,不说了。
这个`LocalFileSystemJSHelper`类声明晚了之后,使用下面的姿势跟`webengine`实例发生关系:
// 这里是窗体构造函数
localFileSystemJSHelper = new LocalFileSystemJSHelper(ui->webEngineView, this);
// QWebChannel qWebChannel(this) 实例化过了
qWebChannel.registerObject("localFileHelper", localFileSystemJSHelper);
ui->webEngineView->page()->setWebChannel(&qWebChannel);
ui->webEngineView->setHtml(generateFormHtml(), QUrl("qrc:/html/"));
显然,`registerObject("localFileHelper", localFileSystemJSHelper);`这一行中,`localFileHelper`就是上文`JS`里面调用的那个神奇的东西,这个名字在这里与`LocalFileSystemJSHelper`这个`C++`类的一个实例发生绑定,后面就可以在`JS`侧用了,直接调用其某个`槽`的同名`JS`函数即可触发对应的`C++`函数,不需要特殊处理。
注意,`setWebChannel`函数的调用必须在`setHtml`函数或者其他什么打开网页的调用之前发生,否则页面加载后就开始在另一个线程中执行`JS`,这时候`qWebChannel`还没绑定给页面的话,`JS`侧的的初始化操作会失败,具体就是下面这些:
window.recentChosenFilePath = "";
window.recentChosenDirPath = "";
window.userIsChoosing = false;
new QWebChannel(qt.webChannelTransport,
function (channel) {
var localFileHelper = channel.objects.localFileHelper;
window.localFileHelper = localFileHelper;
localFileHelper.chosenFilePathChanged.connect(function (newChosenFilePath) {
recentChosenFilePath = newChosenFilePath;
userIsChoosing = false;
});
localFileHelper.chosenDirPathChanged.connect(function (newChosenDirPath) {
recentChosenDirPath = newChosenDirPath;
userIsChoosing = false;
});
});
`new QWebChannel`这里,使用了一个叫`qt.webChannelTransport`的对象,如果上文说的还没执行`setWebChannel`就开始载入页面,则这一步就会因为不存在`qt.webChannelTransport`而失败。
下面的两个`xxxxx.xxxx.connect(xxx)`语句,是用来响应`C++`类`LocalFileSystemJSHelper`中的那两个`信号`的,具体干了啥在第一部分都说过了。
这样的异步消息传递模式,还依赖全局变量,其实挺没有安全感的。但尝试了一下,又好像没什么问题,同时自动实现了多线程异步调用的感觉也还挺不错的。