Blog

QtWebKit, One Page, Three Worlds

I needed to show a formatted report inside a Qt application. The report had headings, tables, a few images and links to related objects. My first attempt used a QTextDocument, and it was perfectly respectable until the design acquired a stylesheet and enough layout requirements to qualify as a small web page.

Qt 4.4’s QtWebKit module looked like the easy answer. Put the HTML in a QWebView, connect the link signal and go home. It did work, but the interesting part begins when the page needs to interact with the application. At that point there are three different worlds involved: Qt widgets, WebKit’s page and frame objects, and JavaScript running inside a frame.

The useful layers

QWebView is the widget. It fits into a normal layout and provides the familiar actions such as loading, back, forward and reload. It owns a QWebPage, which holds page-wide behavior: navigation policy, actions, settings and the main frame. The QWebFrame represents a frame’s document and is where HTML, JavaScript and the rendered contents meet.

For a simple report I can call setHtml() on the frame or view, optionally supplying a base URL so relative images and stylesheets resolve properly. That last argument is easy to omit and produces the pleasing result of a beautifully laid-out report with no images. I know this because I conducted the experiment several times.

Loading is asynchronous. A call to load() starts work and returns. Signals report progress and completion. Code that immediately asks for the final document after load() is racing the network and parser, even when the URL points to a local file. This is the same event-loop discipline as the rest of Qt, merely attached to a much more complicated component.

Crossing into JavaScript

QtWebKit can expose a QObject to JavaScript with addToJavaScriptWindowObject(). Slots and properties then become available in the script environment. This is convenient for a report that needs to request an application action. A link can call a narrow bridge object rather than encoding commands into invented URLs and parsing them later.

The narrow part is important. Exposing the main window would give page script an enormous accidental API. I prefer a small object with slots such as showItem(int) or requestPrint(). The bridge validates its arguments and emits signals; application code performs the actual work. The web page gets capabilities, not the keys to the building.

The JavaScript window object can be cleared when the frame’s context changes, so the bridge may need to be added again when notified. Treating injection as a one-time constructor chore works for the first page and mysteriously fails after navigation. Web pages have lifecycles too, because apparently one kind was not enough.

Communication can go in the other direction with evaluateJavaScript(). It is useful for updating a small piece of page state, but assembling large scripts from C++ strings becomes unpleasant immediately. For anything substantial I keep the logic in a JavaScript resource and invoke a defined function with carefully encoded data.

An embedded page should not automatically handle every clicked URL. The application may want internal links to select objects, ordinary HTTP links to open in the user’s browser, and unknown schemes to do nothing. QWebPage provides navigation hooks where that decision belongs.

I initially connected linkClicked() and assumed the job was finished. It was not: navigation behavior depends on the page’s link delegation policy. Once configured, the signal allows the application to decide rather than letting every click replace the report. This is less magical and therefore better.

Content is another policy boundary. For reports generated entirely by the application, exposing a bridge is manageable because the application controls the HTML and script. Doing the same for arbitrary remote pages is a different security proposition. A page that can invoke native slots has left the ordinary browser sandbox through the door I opened for it.

A large tool for a specific job

QtWebKit brings a real browser engine, not merely a richer label. That means capable HTML and CSS rendering, JavaScript, network loading, history and all the state accompanying them. It also means more memory and startup cost than QTextDocument. If the requirement is a few paragraphs with bold text, a web engine is unnecessary furniture.

Printing and exporting deserve testing as well. What looks right in the viewport may paginate badly. External resources may still be loading when the user presses Print. Font choices differ from the surrounding widgets unless the page stylesheet makes them deliberate. Embedding the web does not make it cease being the web.

For complex, interactive documents that already fit the HTML model, I strongly prefer QtWebKit to inventing a private layout engine from labels and painters. It gives designers familiar tools and keeps document structure out of widget code. I qualify that preference heavily: the page should be controlled, the native bridge should be tiny, and navigation should be explicit.

My report ended up simpler than the QTextDocument version, not because QtWebKit itself is simple, but because its complexity matches the problem. The trick is to use the browser as a document component and resist the urge to turn every dialog into a website. We have enough browsers already.

QtConcurrent for the Small Jobs

I had a directory full of images to scale and a progress dialog that froze while doing it. The obvious fix was a worker thread. The less obvious consequence was a new class, a queue, two mutexes and several opportunities to close the dialog at exactly the wrong moment.

Qt 4.4 includes QtConcurrent, which handles this sort of bounded parallel work at a higher level. Instead of managing threads, I can express the operation as a function over a sequence and let mapped() apply it. The result is represented by a QFuture, and QFutureWatcher turns progress and completion into signals suitable for the event-driven parts of the application.

The thread pool is the useful mechanism here. Creating one thread per thumbnail would merely exchange a frozen interface for a small scheduling experiment. QtConcurrent uses a pool and distributes work across it. The application says what can run independently; the library decides which worker executes each item.

There are several forms. run() is convenient for one function call. mapped() transforms each input into an output, while filtered() selects items. The blocking variants are tempting in a command-line program but defeat the point in a GUI thread. I use a watcher and return to the event loop.

This does not make shared state safe. The mapping function must not casually update widgets, append to an unprotected container or rely on execution order. GUI objects remain in their own thread. I pass immutable input into the workers and collect results through the future. If work needs elaborate communication or a long-lived thread with its own event loop, QThread is still the clearer tool.

Cancellation is another qualified promise. A future can request cancellation, but code already running does not vanish. The operation must reach places where cancellation can take effect. For tiny image conversions this is acceptable; for one enormous function it may not be.

I prefer QtConcurrent when the problem really is a collection of independent jobs. It removes thread bookkeeping without hiding that concurrency exists. That is a narrower claim than “threads made easy,” and therefore more believable. My thumbnail dialog now moves smoothly, and the implementation lost enough plumbing that I can almost understand code I wrote last week.

Solid Hardware Without Device File Archaeology

I left an import dialog open, pulled the card reader, and clicked Import. The dialog still had a pointer to a Solid::StorageAccess interface from the device it had displayed. The hardware was gone; my object graph had not heard the news.

Hotplug makes lifetime the main problem, not discovery. A device can disappear between filling a menu and handling its action. A mount can vanish while a job is queued. Keeping /dev/sdb1 does not fix that, and keeping a frontend interface pointer around is worse because it looks usable right up to the point that it is not.

The dialog now stores the device’s unique device identifier, not a borrowed interface object. When the user starts an import, it asks Solid for the device again, checks that it is still valid, obtains the storage interface for that operation, and then checks the current accessibility state. No lookup, no import.

I also listen for deviceRemoved(QString). If the identifier matches the open dialog, the UI disables Import and says the device was removed. Any queued work receives the same cancellation instead of continuing with a path that happened to exist a moment ago.

There is a small race even with the fresh lookup: unplugging is allowed to win after the check. The operation still needs to handle mount or I/O failure. The lookup keeps stale application objects out of the way; it cannot promise that a human will leave the cable alone.

deviceAdded gets less special treatment now. It gives me an identifier to inspect, and I query capabilities when I need them. The removal signal is the one that tears down assumptions.

This fixed a second oddity too: unplugging and reinserting the same reader could produce a new frontend object while the dialog held the old one. Keying the UI by identifier and reacquiring the device made that boring, which is exactly what hotplug code should be.

Phonon and the Boring Audio Button

My sound preview worked with the Xine Phonon backend and failed as soon as I switched the machine to GStreamer. Same file, same button, no sound. So much for my five-line multimedia triumph.

The application called play() and immediately checked state(). Xine reached PlayingState quickly on my machine. GStreamer was still in LoadingState, which my code treated as failure and answered with stop(). I had accidentally made backend speed part of the program’s contract.

Phonon state changes are asynchronous. The fix was to let the MediaObject report stateChanged() and handle ErrorState there instead of demanding synchronous playback:

connect(media, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
        this, SLOT(mediaStateChanged(Phonon::State, Phonon::State)));
media->setCurrentSource(fileName);
media->play();

The slot updates the button when playback really starts and displays errorString() if the object enters ErrorState. LoadingState is just progress, not proof that the backend rejected the file.

Switching backends was useful precisely because it broke this. The common API kept Xine and GStreamer details out of the application, but it did not turn asynchronous operations into synchronous ones. One backend had merely been fast enough to hide my bad assumption.

The preview button is boring again. I left both backends in the test pass; otherwise the next timing assumption will wait for a user to find it.

KWin Compositing Without the Circus

I used to respond to a sluggish desktop by disabling compositing. That made the lag disappear, but also threw away the only useful fact: something in the composed path was expensive. It did not say whether the problem was KWin, one effect, the backend, or the driver.

This week I used a repeatable little test instead. I dragged the same terminal across the same busy screen for thirty seconds, watched KWin’s Show FPS display for missed frames, and recorded KWin and X CPU use. It is not a laboratory benchmark, but it is consistent enough to compare one setting at a time.

With all effects enabled, resizing caused regular stalls. Suspending compositing was the baseline, not the fix. I turned composition back on and disabled effects one by one. Blur was the expensive one on this hardware; the compositor with shadows and ordinary transitions stayed smooth. Disabling everything had hidden that distinction.

The FPS overlay needs some skepticism. A high frame count does not prove input feels responsive, and displaying the counter adds work of its own. I care more about visible pauses during the same action and whether CPU time changes in the same direction. Numbers are useful here as comparisons, not medals.

I repeated the run with OpenGL and XRender. XRender used a little more CPU but had steadier frame delivery with this driver. That is a local result, not a declaration that one backend is universally faster. Different cards and drivers can reverse it.

So compositing stays on, blur stays off, and I have notes I can repeat after the next driver update. “Feels slow” gave me one giant switch. A crude measurement found the smaller one.