上一篇文章“使用CSS定制某类元素下的input文件选择控件”提到,折腾半天实现了对于Qt web engine的定制和修复,然后才发现,由于安全机制的作用,html中的<input type="file">
是不能获取本地文件路径的。
还好,环境不是公众浏览器而是Qt web engine客户端,本身就提供了JS
与C++
通讯的功能,相当于可以魔改浏览器。文章“Qt Webengine回调函数内赋值引发Segment Fault的灵异事件”已经提供了一个这样的案例,不过这次要做的通讯更复杂得多,包括:
Alpaca
表单生成器的定制,使得在用户点击某个需要填写本地文件路径的表单输入框时,主动发起C++
侧选择本地文件的操作;C++
侧完成文件选择之后,将路径字符串传递回到JS
侧;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
相关,随便什么非战斗人员都能看懂的。麻烦的在下面。
这里需要两头操作。首先,通讯是通过Qt
的signal/slot
进行的,涉及这玩意儿的代码很容易就会变得比较细碎。为了干净,先搞一个C++
类用来做通信中转:
#ifndef LOCALFILESYSTEMJSHELPER_H #define LOCALFILESYSTEMJSHELPER_H #include <QObject> 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
的对话框提示用户选择文件/文件夹。
信号和槽都是没有返回值的——因为这套机制本身就是建立在异步和多线程的基础上,文章“通过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
中的那两个信号
的,具体干了啥在第一部分都说过了。
这样的异步消息传递模式,还依赖全局变量,其实挺没有安全感的。但尝试了一下,又好像没什么问题,同时自动实现了多线程异步调用的感觉也还挺不错的。