Refactoring the HTML Text Editor for QWebEngine
published at 13.02.2017 16:45 by Jens Weller
Save to Instapaper Pocket
In the last post, I described my experience with using MSVC as a compiler in combination with QtCreator. The reason I set this up was, that with Qt 5.7 QWebkit isn't anymore supported, and the HTML TextEditor based on tinymce3 is a central part of my application. Instead of QWebkit there is now QWebEngine, based on chromium, a very fine solution. But as chrome uses MSVC to build on windows, there is no way for Qt to include this into the MinGW SDK flavor I'm usually using. Hence the switch. There is an example program available on github, showing you the working code for this blog post.
There are some needed changes for this to work in my own Qt code and a little change in my hacked version of TinyMCE3. You might want to take a look at the old post for QWebView. I'll start with the more interesting C++ changes, and challanges. The API for QWebEngine is similar to QtWebkit, but some details are different. The Porting guide from QWebKit to QWebEngine will give you a good overview. So, you'll have to do some renaming, and then see if any of your code isn't already build to run async. Also you just can't register JS Variables anymore directly. These are the major challenges.
The result it self hasn't changed, the editor still looks the same, as it did in QWebView:
Running JavaScript
This is still very easy, and having a member function executing all needed calls to JS made the refactoring here quite easy. Most of the JS the editor needs is fire and forget, so async is not a problem. Except, at the point where I do ask the editor for its HTML. This is usually triggered by a focus lost event, which then writes the value into the model. The way WebEngine handels this is, that you can give the runJavaScript call a lambda, which is called with the return value of your JS code: [](const QVariant[&] v){...}. So only a new class handling the async setting of the value in case of focus loss was needed.
QWebChannel: the connection between C++ and JS
The biggest change actually is not QWebEngine, its QWebChannel. This is the mechanism connecting your Qt Application with the browser instance QWebEngine is running. This will be new to you, and is a new API. And its great. QWebChannel is independent from QWebEngine, so you can also use it to connect to other webapplications, as long as they run also qwebchannel.js. This is done over WebSockets, QWebChannel also offers a connection via IPC to QWebEngine. QWebChannel will expose QObjects to javascript, you can connect to signals, and call public methods and slots from javascript.
When in use with QWebEngine, one should not use the websockets, but prefer the much faster IPC solution. There was a visible startup time with the web editor when it also needed to connect via websockets. Sadly the examples and documentation focus very much on WebSockets, so that you (and I had) might get the impression that only WebSockets are supported as transport. For using WebSockets you also need to write two classes, in order to connect the webchannel object to the QWebSocketsServer. There is an example containing these two classes.
QWebChannel exposes only QObjects to JS, it is a good Idea to have one or several classes acting as C++ Endpoints. You can use the editor class it self, as its derived via QWidget from QObject. But this will expose many signals and slots to JavaScript that otherwise wouldn't be handled, also you will see quite some warnings because of this in your JS console. So using an Endpoint class to handle all executions of C++ from the WebEngine is the better solution, also splits the needed code for the editor in nicer parts. Currently this is mainly to display a link or image dialog, or the HTML it self. The reason for this is, that the JS Dialogs of the editor are bound to the margin of the editor window it self...
QWebChannel offers a method to register your own objects, and also needs to be made known to the Editor window running QWebEngine:
auto endpoint = new EndPoint(page(),this); webchannel.registerObject("cppeditor",endpoint); page()->setWebChannel([&]webchannel);
QWebEngine and qwebchannel.js
You will need to load qwebchannel.js into the environment of QWebEngine in order to use it and expose qt.webChannelTransport, the IPC endpoint for QWebEngine in JS. This is accomplished by this code:
QWebEngineProfile* profile = new QWebEngineProfile("MyWebChannelProfile", this); QFile webChannelJsFile(":/qtwebchannel/qwebchannel.js"); if(!webChannelJsFile.open(QIODevice::ReadOnly) ) qFatal( QString("Couldn't open qwebchannel.js file: %1").arg(webChannelJsFile.errorString()).toStdString().c_str() ); else { QByteArray webChannelJs = webChannelJsFile.readAll(); webChannelJs.append("\nnew QWebChannel(window.qt.webChannelTransport, function(channel) {window.hostObject = channel.objects.cppeditor;});"); QWebEngineScript script; script.setSourceCode(webChannelJs); script.setName("qwebchannel.js"); script.setWorldId(QWebEngineScript::MainWorld); script.setInjectionPoint(QWebEngineScript::DocumentCreation); script.setRunsOnSubFrames(false); profile->scripts()->insert(script); } setPage(new QWebEnginePage(profile,this));
Qt does not ship with a file named qwebchannel.js (except in one of the examples), the file is part of the build, you can access it via qrc://. Note, that the initialisation of the QWebChannel object in JS is added to the js file via append, before it is added to the profile via QWebEngineScript. The internal QWebEnginePage of the QWebEngineView needs to be renewed. This code runs inside the constructor of the editor.
Issues
QWebChannel and QWebEngine work very well, as it is build on a recent version of chrome. When using WebSockets, you'll need an instance of QWebSocketServer, which is the only real problem for me. As the endpoint for JS is always hostObject, I can only register one endpoint with this name. Multiple Editorwindows are very common in my application, so each of them would need their own WebSocket server, listening to a different port. Unless I wanted to write an endpoint where the different editor instances register their objects, and each of them is then using a different ID to identify which object is to be called on the C++ side. This does not apply to the IPC version.
I was very happy with this solution, until I started to test the rest of the newly build CMS Program. There are little known issues, only the areas where I still was working the last time in Summer. Clicking on the wrong icons brought repeatable crashes, pointing at very strange points in the program. Code that should run without any issues. I can't say its related to QWebEngine, after all the example program I linked was not affected by this. After debugging for days the cause is unknown. I think its probably some weird combination of drivers and calls to wrong APIs. At least some crashes went away after a driver update, but others are still there. I'm not sure if its my local system, a problem with the Qt or boost libraries I link, its a mystery. When building the application with MinGW 5.3 in Qt5.8, the program runs fine. In the next post I'll explain how the HTMLTextEditor runs only using QWebChannel.
Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!