Saturday, May 23, 2009

Useing Qt to write Equinox-OSGi-UI-Applications

I saw I didn't blogged since about 2 months. So I thought I'll start a series of blog entries showing off new things and paths I'm exploring.

I'll start with a Technical Topic because it's a really exciting thing I guess not only for me but also for the whole Equinox-OSGi/Java-Community.

Since some time Qt is released under LGPL and since some weeks now their Java-Binding named Qt-Jambi is released too under LGPL. I've been playing with Qt-Jambi (because my UFaceKit project has a Qt-Port) before but now that the code is under LGPL it's getting more interesting to the wider Java-audience and naturally also people who use Equinox-OSGi for their applications.

A simple QtJambi-Application


Before digging into the details what I've done let's look at a simply QtJambi-Application if we are not using Equinox-OSGi.


package at.bestsolution.qt;

import com.trolltech.qt.gui.QApplication;
import com.trolltech.qt.gui.QGridLayout;
import com.trolltech.qt.gui.QLabel;
import com.trolltech.qt.gui.QLineEdit;
import com.trolltech.qt.gui.QMainWindow;
import com.trolltech.qt.gui.QWidget;

public class HelloWorld extends QMainWindow {

public HelloWorld() {
setWindowTitle("Hello World!");

QWidget composite = new QWidget();

QGridLayout layout = new QGridLayout();
composite.setLayout(layout);

QLabel label = new QLabel();
label.setText("Label");
layout.addWidget(label,0,0);

QLineEdit text = new QLineEdit();
layout.addWidget(text,0,1);

setCentralWidget(composite);
}

public static void main(String[] args) {
QApplication.initialize(new String[0]);

HelloWorld world = new HelloWorld();
world.show();

QApplication.exec();
}
}

This looks not much different to a SWT-Application besides the fact that one doesn't has to pass a parent when creating a widget and instead of running the event loop one simply calls QApplication.exec().

QtJambi and Equinox-OSGi


Couldn't be hard you think when you've used other UI-Toolkits (SWT,Swing) in your Equinox-OSGi-Applications already but the problem is that Swing is not problematic because it is part of the JRE and SWT is shipped as an (in fact multiple) Equinox-OSGi-Bundle/Fragment.

What we need to do is to Equinox-OSGify the bundles coming from Qt but this task is more complex then it looks on the first sight because using the simple converter provided by PDE is not providing us a solution because QtJambi-Code expects to load the libraries in very special way which means we need to patch their Java-Code to make it aware of Equinox-OSGi.

The really cool thing is that patching and maintaining the patch is easier than one might think because they provide their sources through a git-repo one could simply clone and maintain the patched sources. So maintaining the patch is easier than it is for example to maintain a patch for the eclipse-platform because of git.

The tough thing is to get the environment setup in a way than one can produce .jars from the sources because one

  • Has to compile the Qt-Sources

  • To generate the Java-Binding-Classes to the Qt-Sources (extracted from the C++-Header-Files)


which is a bit time consuming and not documented very well at the moment. Though this is doable for a medium skilled Java-Dev I think one should be able to checkout the complete project with native and generated Java-Code and doesn't have to compile all the stuff.

After having managed to setup a build environment I patched the libary loading classes and recreated the .jar-packages. QtJambi is split in 2 .jars:

  • qtjambi.jar: Hold platform independent Java-Classes

  • qtjambi-${os}.jar: Holding native libraries for the platform and the JNI-Glue


So the setup is similar to SWT but in SWT also the Java-Code is part of the native fragment because it differs from platform to platform and the host bundle is simply an empty bundle. In contrast to that in Qt the Host-Bundle is holding all Java-Classes and in the native fragments one has the native-libs and JNI-Glue.

So what this all means for you? Not too much because I did 2 things as part of UFaceKit-Target-Setup:

  • Packaged my changes to the Java-Code and provide it for download

  • Added ant-tasks who fetch the native libs from Qt-Software and repackage them



One could also use these ant-tasks when not using UFaceKit (I'm using it for my RCP-Development-Setup).

The Equinox-OSGi-Support is not fully finished and I'll maybe rework it a bit in future when understanding the code better but for now it sufficient to go on and file a CQ to make use of Qt in UFaceKit. Let's see what's coming out from this now that Qt is LGPL.

Simple Qt-Jambi and Equinox-OSGi-Application


Let's create an Equinox-Application which uses Qt as UI-Toolkit now. The easiest thing is to use the PDE-Wizard to create a "Headless Hello RCP" and add a MainWindow.java.
package at.bestsolution.qt;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import com.trolltech.qt.gui.QApplication;
import com.trolltech.qt.gui.QGridLayout;
import com.trolltech.qt.gui.QLabel;
import com.trolltech.qt.gui.QLineEdit;
import com.trolltech.qt.gui.QMainWindow;
import com.trolltech.qt.gui.QPixmap;
import com.trolltech.qt.gui.QPushButton;
import com.trolltech.qt.gui.QWidget;

public class MainWindow extends QMainWindow {

public MainWindow() {
QWidget widget = new QWidget();
widget.setObjectName("main_window");

QGridLayout layout = new QGridLayout();
layout.setMargin(0);
widget.setLayout(layout);

addHeader(layout,"Tom Schindl","at/bestsolution/qt/bookmarks.png");

QWidget content = new QWidget();
QGridLayout contentLayout = new QGridLayout();
content.setLayout(contentLayout);

addLine(0, contentLayout, "Firstname");
addLine(1, contentLayout, "Lastname");
addLine(2, contentLayout, "Age");

QPushButton button = new QPushButton();
button.setObjectName("submit");
button.setText("Submit");
contentLayout.addWidget(button,3,1);

layout.addWidget(content);

setCentralWidget(widget);
}

private void addHeader(QGridLayout layout, String labelText, String icon) {
QLabel header = new QLabel();
layout.addWidget(header);
header.setObjectName("header");
QGridLayout headerLayout = new QGridLayout();
headerLayout.setMargin(0);
header.setLayout(headerLayout);

QLabel headerIcon = new QLabel();
headerIcon.setObjectName("header_icon");
headerIcon.setPixmap(loadImage(icon));

headerLayout.addWidget(headerIcon);

QLabel headerText = new QLabel();
headerLayout.addWidget(headerText,0,1);
headerLayout.setColumnStretch(1, 100);
headerText.setObjectName("header_text");
headerText.setText(labelText);
}

private void addLine(int line, QGridLayout contentLayout, String labelText) {
QLabel label = new QLabel();
label.setText(labelText);
label.setObjectName("label");
contentLayout.addWidget(label);

QLineEdit text = new QLineEdit();
text.setObjectName("text");
contentLayout.addWidget(text,line,1);
}


private QPixmap loadImage(String path) {
try {
InputStream in = getClass().getClassLoader().getResourceAsStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream();

int l;
byte[] buffer = new byte[1024];

while ((l = in.read(buffer)) != -1) {
out.write(buffer, 0, l);
}

QPixmap pic = new QPixmap();
pic.loadFromData(out.toByteArray());
return pic;
} catch (Exception e) {
e.printStackTrace();
}

return null;
}
}

and modify the generated application class like this:
package at.bestsolution.qt;

import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;

import com.trolltech.qt.gui.QApplication;

public class Application implements IApplication {
public Object start(IApplicationContext context) throws Exception {
QApplication.initialize(new String[0]);

MainWindow window = new MainWindow();
window.show();

QApplication.exec();

return IApplication.EXIT_OK;
}

public void stop() {}
}

Well as you see I'm not a good designer and the application looks well not really nice though it looks native on my OS-X though this is only faked by Qt because they are drawing everything on the screen as far as I understood it.

One could think that this fact is a draw back of Qt but IMHO it's the other way round because with this strategy they can support things SWT can't support easily - Completely restyle your application using a declarative language and well they use CSS like e.g. E4 does too.

The first thing to do is to add a method to load a stylesheet to Application.java:
private String loadStyles(String cssPath) {
InputStream in = getClass().getClassLoader().getResourceAsStream(cssPath);
BufferedReader r = new BufferedReader(new InputStreamReader(in));
StringBuilder s = new StringBuilder();
String line;

try {
while( (line = r.readLine()) != null ) {
s.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}

return s.toString();
}

and set the style sheet on the main window:
public Object start(IApplicationContext context) throws Exception {
QApplication.initialize(new String[0]);

MainWindow window = new MainWindow();
window.setStyleSheet(loadStyles("at/bestsolution/qt/style.css"));
window.show();

QApplication.exec();

return IApplication.EXIT_OK;
}
and we need to define some styles:

resulting in this application:


which we all agree looks better than:



As you see this is also not my design but then one you get when using the Eclipse-Forms-API with the difference that in Eclipse one has to learn a new API to deal with besides SWT whereas in Qt the UI-Code is still Qt and styled by a declarative syntax and if you ask me the Forms-API is going to replace in space of Eclipse in E4 through SWT + CSS but this is only my personal opinion.

So should we all now move to Qt-Jambi to write UI-Applications in Java like we did years ago when we abandoned Swing and started using SWT?

Let's look at some potentially problematic areas:

  • Qt and QtJambi misses an application framework like eclipse RCP provides one for SWT-Application developers

  • Qt and QtJambi misses Databinding support like Eclipse-Databinding provides one for SWT, JavaBeans and EMF

  • Nokia removed all resources from QtJambi development and wants to build a community to work on it


For at least 2 of the above there are solutions already today:

  • E4's core application platform is UI-Toolkit agonstic so though E4 is not released until 1.5 years it would give people the possibility to use Qt as their UI-Toolkit of choice which supports many many things starting from animations, multimedia integration, ...

  • UFaceKit provides JFace-Viewer like and Eclipse-Databinding support for QtJambi if the CQ I'm going to file is approved


Still the killer problem is the lacking support from Nokia on QtJambi and it's unclear if a community could be build around it who not even maintains but also adds new features.

I think this is a bitty because with getting a real application framework with E4, it's themeing, multimedia and animation support I think QtJambi could get a real possibility to write cross-platform RCP-Applications in Java without the sometimes really hurting lowest common denominator problem we have in SWT.

So what should one do? Though QtJambi looks like a real solution for writing nice looking RCP-Applications the uncertainness caused by Nokia by cutting resources makes it unusable for most companies.

For form developers I could point you once more to UFaceKit which supports both SWT and Qt and your form application code is not affected by changing the underlying technology but one can still rely on native stuff where needed (e.g. using Qt animation/multimedia support).

For me as one of E4 committers and UFaceKit-Project lead it means:

  • I'd try to keep the application runtime widget agonstic if possible (well we are on a good track here)

  • I'll file a CQ to let UFaceKit make use of QtJambi and provide first class JFace-Viewer and Eclipse-Databinding support for QtJambi


  • 1. updated my wrong capitalization of Qt
    2. please note that I'm using Equinox specific stuff to make this work so it is maybe not runnable on other OSGi implementations but I'm happy to incooperate feedback and suggestions into my git-clone to support other OSGi implementations