ZionSoft     About     Archive     Feed     Privacy

Play with Google Play Services 6 - Push Notification

If you haven’t set up Google Play services SDK yet, please follow this tutorial. Today, we demonstrate how to use the cloud messaging / push notification.

Enable Cloud Messaging API

First, create a project in Google Developers Console for your app, and enable the Google Cloud Messaging for Android API. The project number will be used as the GCM sender ID.

Then, create a server key in Public API access with your server’s IP address (for testing purposes, you can use e.g. 0.0.0.0/0 to allow anybody to send messages with the generated API key). The generated API key will be used to authenticate your server.

Note: For devices running Android older than 4.0.4, it requires users to set up a Google account before using GCM.

Update AndroidManifest.xml

Now, update your AndroidManifest.xml file, specifying the required permissions:

<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
 
<permission
    android:name="com.example.app.permission.C2D_MESSAGE"
    android:protectionLevel="signature" />
<uses-permission android:name="com.example.app.permission.C2D_MESSAGE" />

Register The Client

The following snippet shows how to register a client for cloud messaging:

// this might invoke network calls, so call me in a worker thread
private void registerCloudMessaging() {
    // repeated calls to this method will return the same token
    // a new registration is needed if the app is updated or backup & restore happens
    final String token = InstanceID.getInstance(this).getToken("my-GCM-sender-ID",
        GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
 
    // then uploads the token to your server
 
    // or, if the client wants to subscribe to certain topics, use this method
    GcmPubSub.getInstance(this).subscribe(token, "/topics/your_topic_name", null);
}

Note that Google’s GCM server doesn’t handle localization nor message scheduling, so the client might also need to upload e.g. the preferred language and timezone to the server to improve user experiences.

Receive Messages

In your AndroidManifest.xml, specify a listener:

<application ...>
    <receiver
        android:name=".MyPushNotificationReceiver"
        android:permission="com.google.android.c2dm.permission.SEND" >
        <intent-filter>
            <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            <category android:name="com.example.app" />
        </intent-filter>
    </receiver>
</application>

Then you can handle the message yourself:

public class MyPushNotificationReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // a null or "gcm" message type indicates a regular message,
        // and you can get the message details as described in section 5
        // "deleted_messages" indicates some pending messages are deleted by server
        String messageType = intent.getStringExtra("message_type");
 
        // indicates the sender of the message, or the topic name
        String from = intent.getStringExtra("from");
    }
}

Send a Message

To send a push message, you can simply send a POST request to Google’s GCM server. The URL of the server is: https://gcm-http.googleapis.com/gcm/send.

The HTTP header must contain the following two:

  • Authorization:key=
  • Content-Type: application/json

The HTTP body is a JSON object, something like this:

{
  "to": "client_registration_id_or_topic_name",
  "data": {
    "key_1": "value_1",
    "key_2": "value_2"
  },
  "priority": "normal"
}

The fields of the data object represent the key-value pairs of the message’s payload data. With the above example, the client will receive an intent with an extra of key key_1 and value value_1, and another extra of key key_2 and value value_2.

The priority can be either normal (default) or high. The messages marked as high priority will be sent immediately, even when the device is in Doze mode. For normal priority messages, they will be batched for devices in Doze mode, and will be discarded if the message expires while the device is in Doze mode.

For a complete list of allowed fields in the posted JSON, check here.

The response can contain the following status code:

  • 200: The message is successfully processed.
  • 400: The request JSON object is malformed.
  • 401: The sender fails to authenticate itself.
  • 5xx: Server error, the sender should respect the Retry-After and retry later.

More details of how to parse the response messages can be found here.

That’s it for today. Happy hacking and keep reading!

Play with Google Play Services 3 - Map

If you haven’t set up Google Play services yet, please follow this tutorial.

Get Maps API Key

First, create a project for your app in Google Developers Console, and enable Google Maps Android API v2.

Then get you the signing certificate’s fingerprint:

# displays fingerprint of your debug certificate
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
 
# displays fingerprint of your release certificate
keytool -list -keystore <your keystore>

Then use the SHA-1 fingerprint and your app’s package name to generate a new key for API access from Google Developers Console.

Update AndroidManifest.xml

Now update your app’s AndroidManifest.xml file, specifying the permissions, feature required, and the map key:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
 
<uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>
 
<application>
    <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="your-map-key"/>
</application>

Add a Map

The easiest way to show a map is to add a MapFragment (or a SupportMapFragment for older devices) to your activity. Another approach is to add a MapView to your activity or fragment.

To add a map view showing satellite maps with an XML file:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.gms.maps.MapView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:map="http://schemas.android.com/apk/res-auto"
        android:id="@+id/map_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        map:mapType="satellite" />

Then in your activity:

public class MainActivity extends Activity {
    private MapView mMapView;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // if you use a map fragment, it will take care of its life-cycle management
        // if you use a map view, you need to forward all the following events to it
        mMapView = (MapView) findViewById(R.id.map_view);
        mMapView.onCreate(savedInstanceState);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
 
        mMapView.onResume();
    }
 
    @Override
    protected void onPause() {
        mMapView.onPause();
 
        super.onPause();
    }
 
    @Override
    protected void onDestroy() {
        mMapView.onDestroy();
 
        super.onDestroy();
    }
 
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        mMapView.onSaveInstanceState(outState);
 
        super.onSaveInstanceState(outState);
    }
 
    @Override
    public void onLowMemory() {
        mMapView.onLowMemory();
 
        super.onLowMemory();
    }
}

GoogleMap Object

Once you have the map ready, you can use the getMap() method (or the getMapAsync() to get reference to the map asynchronously) of the map view or fragment to get a GoogleMap object. If this method returns null, it means the map is not ready yet (e.g. because the Google Play services is not available). It serves as the entry point for interactions with the map:

  • To configure the UI, e.g. show / hide my location button, enable / disable gestures, use the UiSettings object fetched using GoogleMap.getUiSettings() method.
  • To convert between coordinates on the map and the location on the screen, use the Projection object fetched using GoogleMap.getProjection() method.
  • You can uses GoogleMap.setMyLocationEnabled() method to show or hide the blue dot representing device’s current location, but to get the current location you should follow my previous tutorial on locations.
  • To listen to map click events, you can simply set the corresponding listener using GoogleMap.setOnMapClickListener() or GoogleMap.setOnMapLongClickListener() method.
  • You can set the camera (i.e. which part of the map should be shown) using the GoogleMap.animateCamera() (with animation) or GoogleMap.moveCamera() (no animation) method. To listener to camera changes (e.g. triggered by user), you can specify the corresponding listener using GoogleMap.setOnCameraChangeListener().

Also, you can set map types (normal, satellite, etc.), enable / disable indoor maps, 3D buildings, etc. using the GoogleMap object.

Markers and Info Windows

Markers can be used to indicate locations or other important information on the map, with more detailed information shown in the info window.

The following snippet adds a marker with a custom icon to the map, and displays a default info window showing “Hello, Helsinki!”:

@Override
protected void onResume() {
    ...
    mGoogleMap = mMapView.getMap();
    BitmapDescriptor bitmap = ...
    Marker addedMarker = mGoogleMap.addMarker(new MarkerOptions()
        .position(new LatLng(60.1708, 24.9375))
        .title("Hello, Helsinki")
        .icon(BitmapDescriptorFactory.fromBitmap(bitmap)));
}

One thing to note when using e.g. BitmapDescriptorFactory is, you might need to use MapsInitializer to explicitly initialize the map.

To remove a marker, you can use the Marker.remove() method, or the GoogleMap.clear() method to remove all added markers, shapes, etc.

If you want to animate a marker on the map (e.g. use a custom marker to indicate current location instead of the boring blue dot), all you need is simply update its position periodically using the Marker.setPosition() method.

When the user clicks on a marker, by default it shows an info window with some title and snippet defined when the marker was added. But you can also listen to the click event:

mGoogleMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
    @Override
    public boolean onMarkerClick(Marker marker) {
        ...
    }
});

If the onMarkerClick() method returns true, it means the click event has been consumed. Otherwise, if false is returned, it will execute the default behavior, i.e. moving the camera to the marker, and showing the info window.

To provide a customized info window:

mGoogleMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
    @Override
    public View getInfoWindow(Marker marker) {
        // this will be first called
        // the returned view will be used as the entire info window
    }
 
    @Override
    public View getInfoContents(Marker marker) {
        // this will only be called if getInfoWindow() returns null
        // the returned view will be used inside the default info window frame
    }
});

The info window is rendered as an image. It means any subsequent changes won’t be visible unless you re-open the info window, and the info window can only listen to one click event for the whole info window.

Circles, Polygons, and Polylines

To add those shapes to the map, you basically follow the same pattern:

Circle addedCircle = mGoogleMap.addCircle(new CircleOptions()
    .center(new LatLng(60.1708, 24.9375))
    .radius(1000));
 
// when adding a polygon, the system will automatically connect the
// last coordinate to the first one
Polygon addedPolygon = mGoogleMap.addPolygon(new PolygonOptions()
    .add(new LatLng(60, 24), new LatLng(61, 24), new LatLng(61, 25), new LatLng(60, 25));
 
// the system won't automatically connect the coordinates
// thus you can see the difference compared to the above polygon
Polyline addedPolyline = mGoogleMap.addPolyline(new PolylineOptions()
    .add(new LatLng(60, 24), new LatLng(61, 24), new LatLng(61, 25), new LatLng(60, 25));

Ground and Tile Overlays

The main difference between these two overlays is: the ground overlay provides a single image for a fixed location on the map, while the tile overlay provides a set of images for different locations at different zoom level on the map.

To add a ground overlay:

GroundOverlay addedGroundOverlay = mGoogleMap.addGroundOverlay(new GroundOverlayOptions()
    .image(bitmap)
    .anchor(0.5, 0.5)
    .position(new LatLng(60.1708, 24.9375), 100, 100));

To add a tile overlay:

TileProvider tileProvider = new TileProvider() {
    @Override
    public Tile getTile (int x, int y, int zoom) {
        // a Tile object consists of raw image data, as well its width and height
        // the map is divided into a 2^zoom x 2^zoom grid
        // for each grid, the system targets 256 x 256 dp
        // if TileProvider.NO_TILE is returned,
        // it means you don't want to provide tiles yourself
        // if null is returned, it means the tile could not be found now,
        // and will be tried again later
    }
};
TileOverlay addedTileOverlay = mGoogleMap.addTileOverlay(new TileOverlayOptions()
    .tileProvider(tileProvider));

That’s it for today. Happy hacking and keep reading :)

Play with Google Play Services 1 - Set Up

Google has offered a bunch of services that developers can use in their Android apps. In this and following few blog posts, we demonstrate how easy it is to use them. Now, let’s get everything setup.

Download the SDK

You can find Google Play services in the Extras section from your Android SDK Manager. Simply install it. If you also want to support Froyo (Android 2.2), also install Google Play services for Froyo.

After installation, you can find it at */extras/google/google_play_services/*.

Configure Your Project

Update the build.gradle file of the corresponding module, adding the following section:

dependencies {
    compile "com.google.android.gms:play-services:4.1.32"
}

Then, update the AndroidManifest.xml file of the module, adding the following section:

<application>
    ...
    <meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version"/>
</application>

Check If the Device Has Google Play Services

Considering the variety of Android devices out in the market, it’s possible that the device doesn’t have the latest version of Google Play services you required, the user disables it, or it can be simply missing from from the device. Therefore, it’s best practice to check its availability:

@Override
protected void onResume() {
    super.onResume();
 
    // code that doesn't require Google Play services
 
    if (!checkGooglePlayServices())
        return;
 
    // code that requires Google Play services
}
 
private boolean checkGooglePlayServices() {
    int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (result == ConnectionResult.SUCCESS) {
        // the device has the latest Google Play services installed
        return true;
    } else {
        Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(result, this, 8964,
            new DialogInterface.OnCancelListener() {
                @Override
                public void onCancel(DialogInterface dialog) {
                    finish();
                }
            });
        if (errorDialog != null) {
            // the problem can be fixed
            // e.g. the user needs to enable or download the latest version
            errorDialog.show();
        } else {
            // for some reason, the problem can't be fixed
            // you should provide some notice to the user
        }
        return false;
    }
}
 
@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data) {
    // you don't have to check it here,
    // since the onResume() callback will be invoked anyway
    if (requestCode == 8964) {
        // this is invoked when user finishes operation
        if (resultCode == Activity.RESULT_OK) {
            // problem fixed, now you can use Google Play services
        } else {
            // problem not fixed, e.g. user decides not to install the latest version
        }
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

Tip: You should make your app usable and requires it only when needed, if Google Play services is not a must-have feature.

Yep, that’s it. Now you have Google Play services ready, have fun with it and keep reading ;)

Go’s defer statement in C++

The Go language provides a useful defer statement to guarantee certain code is always executed when returning from the current scope. Though we can use constructor in C++, things get tricky e.g. when a pointer needs to be deleted. Here we present some simple draft code to solve this issue.

#include <functional>
 
// the user should fill copy and move constructors
class DeferredAction
{
public:
    DeferredAction(const std::function<void(void)>& deferred) : m_deferred(deferred) {}
 
    ~DeferredAction() {
        if (m_deferred)
            m_deferred();
    }
 
private:
    std::function<void(void)> m_deferred;
};

Then if the client code looks like the following:

#include "deferredaction.h"
 
#include <iostream>
 
int main()
{
    DeferredAction deferred([] () {
        std::cout << "executed when the 'deferred' object is destructed" << std::endl;
    });
    std::cout << "print some random stuff here" << std::endl;
    return 0;
}

It will first print the line: print some random stuff here. Then when the deferred object is destructed when main() returns, it prints the second line: executed when the ‘deferred’ object is destructed.

Displaying Images Efficiently on Android

In this blog post, we demonstrate some simple ways to efficiently display images.

The Straightforward Approach

The simplest way to load an image (e.g. from resource), is to load it in an AsyncTask, and updates the ImageView in onPostExecute():

class ImageLoadingTask extends AsyncTask {
    ImageLoadingTask(Resources resources, ImageView imageView) {
        mResources = resources;
        mImageView = new WeakReference(imageView);
    }
 
    @Override
    protected Bitmap doInBackground(Integer[] params) {
        return BitmapFactory.decodeResource(mResources, params[0]);
    }
 
    @Override
    protected void onPostExecute(Bitmap result) {
        final ImageView imageView = mImageView.get();
        if (imageView != null)
            imageView.setImageBitmap(result);
    }
 
    private final Resources mResources;
    private final WeakReference mImageView;
}

This works fine until you try to load some large image, when the image refuses to be loaded with this error from logcat: W/OpenGLRenderer﹕ Bitmap too large to be uploaded into a texture (4000×3726, max=2048×2048)

And of course, it might even throw an OutOfMemory error on low-end devices.

Load a Scaled Image

To solve the problem, we should request the decoder to subsample the original image, as shown below with the updated doInBackground():

protected Bitmap doInBackground(Integer... params) {
    // extracts the size of the original image first
    final int resourceId = params[0];
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(mResources, resourceId, options);
 
    // calculates the inSampleSize
    if (options.outWidth > mTargetWidth || options.outHeight > mTargetHeight) {
        options.inSampleSize
            = Math.min(Math.round((float) options.outWidth / (float) mTargetWidth),
                Math.round((float) options.outHeight / (float) mTargetHeight));
    }
 
    // decodes the image
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(mResources, resourceId, options);
}

This works pretty fine in most cases, until the ImageView is used e.g. inside a ListView, which recycles child views for performance reasons. In this scenario, each time the ImageView is displayed, it will trigger an image load task. However, the sequence when the tasks are finished is undefined. So there is a chance that the image displayed actually comes from the previous item, which for some reason takes longer to load.

ListView

To solve this issue, the ImageView should remember the last image it’s supposed to load, so we extends the class and introduces a simple loadImageResource() method as below:

public void loadImageResource(int resId) {
    // if loading or already loaded the same resource, do nothing
    // otherwise cancel the current loading task
    if (mResId == resId)
        return;
    if (mImageLoadingTask != null) {
        final ImageLoadingTask loadingTask = mImageLoadingTask.get();
        if (loadingTask != null)
            loadingTask.cancel(true);
    }
 
    mResId = resId;
    setImageBitmap(null); // or some placeholder image
 
    final ImageLoadingTask loadingTask = new ImageLoadingTask(getResources(), this);
    mImageLoadingTask = new WeakReference(loadingTask);
    loadingTask.execute(resId);
}

Cache Images

Now everything works fine, except that whenever our ImageView get recycled, it needs to load the image again, so we introduce a simple image cache:

public class ImageCache extends LruCache<Integer, Bitmap> {
    public ImageCache(int maxSize) {
        super(maxSize);
    }
 
    @Override
    protected int sizeOf(Integer key, Bitmap bitmap) {
        // uses byte sizes of the bitmaps
        return bitmap.getRowBytes() * bitmap.getHeight();
    }
}
 
// sample code to create an ImageCache using 1 / 8th of the memory
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 8L);
ImageCache imageCache = new ImageCache(cacheSize);

With the above simple ImageCache, one can easily caches images in memory, and improves the performance significantly.

Forward Secrecy

Traditionally, when someone sends a message, the message is encrypted using the receiver’s public key (or by a session key which is encrypted by the receiver’s public key). It looks secure at first glance, since only the receiver can decrypt the message with the private key. However, if an attack is able to record all the exchanged messages, and somehow obtains the private key at some point in the future, he can decrypt all the recorded messages. To solve this kind of issue, here comes the so called forward secrecy.

With forward secrecy, the attacker won’t be able to decrypt any messages recorded, even if peers’ private keys are compromised at some point in the future. To achieve this, instead of encrypting all the messages using the same key, peers would negotiate a session key through an ephemeral key exchange (e.g. Diffie–Hellman key exchange).

Forward secrecy in action

The following uses OTR as an example to illustrate how forward secrecy is achieved.

  1. When Alice wants to start a conversation with Bob, she picks a random value x<sub>1</sub>, computes A=g^x<sub>1</sub>, and sends her half of key exchange message A, Sign<sub>k<sub>A</sub></sub>(A) to Bob.
  2. Upon reception, Bob picks a random value y<sub>1</sub>, computes B=g^y<sub>1</sub>, and sends his half of key exchange message B, Sign<sub>k<sub>B</sub></sub>(B) back to Alice.

At this point, both Alice and Bob can compute the session key s=Hash(B^x<sub>1</sub>)=Hash(A^y<sub>1</sub>), and they both can also verify the messages are from the correct peer with the attached signature.

Moreover, since the session keys are not associated with the peers’ long term public / private key pairs, the attackers won’t be able to decrypt the messages even if he somehow obtains the private keys in the future. OTR takes one step further by starting a new key exchange on each message sent in a conversation and discarding the old session key once a new one is negotiated, to reduce the possible time window for attacks.

Forward secrecy, asynchronously

As one can easily notice, the above method works perfect in any synchronous environment, where several handshake messages need to be exchanged before any real messages can be sent. This is not an ideal situation e.g. for emails where users exchange mails asynchronously, or mobile platforms where users are not always online.

To tackle with this issue, Moxie proposed a simple trick.

  1. Upon registration, Bob generates 100 signed key exchange messages (called “prekeys”), and sends his half of key exchange messages for each prekey to the server.
  2. When Alice wants to start a conversation with Bob, she connects to the server and fetches Bob’s next prekey.
  3. Alice generates her half of key exchange message, computes the session key using the fetched prekey, and encrypts the text.
  4. Alice sends the message to Bob, which includes the ID of the fetched prekey, her half of key exchange message, and the encrypted text.
  5. When Bob reads the message later, he can computes the session key and decrypt the message.

With this setup, the peers don’t need to be online at the same time, and forward secrecy is guaranteed by the fact that the server never sends out the same prekey twice, and client never accepts the same prekey twice.

Hybrid Application Using QML and Qt C++

Though QML provides a nice way to design user interfaces, and JavaScript is employed there to implement the application logic and works pretty nice in many cases, we might still need Qt C++ in some situations (well, at least JavaScript has limited access outside its sandbox).

integrate QML into Qt C++

Suppose we have a QML file, named myqml.qml, like this:

// this line should be "import QtQuick 1.0" since Qt 4.7.1
import Qt 4.7
 
Rectangle {
  id: myRectangle
  width: 800
  height: 480
  color: "lightgray"
 
  Text {
    id: myText
    text: "I love hybrid application!"
    anchors.centerIn: parent
    font.pointSize: 28
    font.bold: true
  }
}

One easy way to integrate is to use the QDeclarativeView class, which provides a widget to display QML files. You just need the following three lines:

QDeclarativeView view;
view.setSource(QUrl::fromLocalFile("myqml.qml"));
view.show();

However, QDeclarativeView consumes more resources than normal widgets. Fortunately, we can integrate QML into a graphics scene. The following lines shows the basic usage:

// provides an environment for instantiating QML components
QDeclarativeEngine engine;
 
// encapsulates a QML component definition
QDeclarativeComponent component(&engine, QUrl::fromLocalFile("myqml.qml"));
 
// creates the graphics item for QML at the engine's root context
QDeclarativeItem *item = qobject_cast<QDeclarativeItem *>(component.create());
 
scene.addItem(item);

Then with the help of the QDeclarativeItem class, you can easily access the properties of the QML element, e.g.:

qDebug() << item->property("color").typeName();
item->setProperty("color", QColor(255, 255, 255));

exposing Qt C++ objects to QML

You can also expose native Qt C++ objects to QML through QDeclarativeContext:

QDeclarativeContext *context = engine->rootContext();
context->setContextProperty("textFromQt", QString("I love hybrid application!"));

Then in QML, you can have e.g. the following line to access them:

text: textFromQt

You can also use QDeclarativePropertyMap to manage the exposed properties:

QDeclarativePropertyMap map;
map.insert("key1", "value1");
map.insert("key2", "value2");
context->setContextProperty("map", &map);

In a QML engine, there could be a couple of contexts, forming a tree structure. The child contexts inherit properties in the parent context. By default, there is only one root context, but you can always add more to give finer control of the exposed data, i.e. different QDeclarativeComponent inside the same context have the same exposed data set.

To expose a self-defined object, we can use the following code:

// define a class with properties
class MyObject: public QObject
{
  Q_OBJECT
 
  // the NOTIFY signal is needed to inform about changes
  // all properties will be exposed to QML
  Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
  MyObject(QObject *parent = 0) : QObject(parent), m_text("I love hybrid application!") {}
 
  QString text() const { return m_text; }
  void setText(QString &text)
  {
    m_text = text;
    emit textChanged();
  }
 
signals:
  void textChanged();
 
private:
  QString m_text;
};
 
// then just expose it so QML can access it through name "myObject"
engine->rootContext()->setContextProperty("myObject", new MyObject());

Moreover, we can create new QML types:

// define the new type
class MyType : public QDeclarativeItem
{
  Q_OBJECT
 
public:
  MyType(QDeclarativeItem *parent = 0) : QDeclarativeItem(parent)
  {
    setFlag(QGraphicsItem::ItemHasNoContents, false);
  }
 
  void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0)
  {
    QPen pen(QColor(100, 100, 100), 2);
    painter->setPen(pen);
    painter->drawLine(0, 100, 100, 100);
  }
};
 
// then register to expose it
qmlRegisterType<mychart>("net.zionsoft.mytype", 1, 0, "MyType");

In QML, you can use it like this:

import net.zionsoft.mytype 1.0
 
MyChart {
 id: myChart
 width: 100
 height: 200
}

Now let’s jump to invoke a Qt C++ function from QML. Basically, QML can invoke slots and functions declared with Q_INVOKABLE. Suppose we have the following function in MyObject:

Q_INVOKABLE void showMessage()
{
  QMessageBox::information(NULL, "My Test", "Invoking a native function ;)");
}

Then you can invoke it in QML:

myObject.showMessage();

write plugins as QML extension

The benefits for using plugins as QML extensions are similar to using shared libraries, and it can be easily achieved with the help of QDeclarativeExtensionPlugin. Let’s reuse the MyType class defined in the previous section.

First, we need to create a plugin:

class MyPlugin : public QDeclarativeExtensionPlugin
{
  Q_OBJECT
public:
  void registerTypes(const char *uri)
  {
    qmlRegisterType<MyType>(uri, 1, 0, "MyType");
  }
};
 
Q_EXPORT_PLUGIN2(myPlugin, MyPlugin);

Then create a file named qmldir to define which plugin to load from where (suppose the plugin is called myplugin):

plugin myplugin /path/to/plugin

Now we can use qmlviewer to launch the QML file:

// no need to import now
MyChart {
 id: myChart
 width: 100
 height: 200
}

summary

  • Use QDeclarativeView or QDeclarativeComponent to integrate a QML file into native Qt C++.
  • Qt C++ can access the properties of QML elements through QDeclarativeItem.
  • Expose native objects to QML through QDeclarativeContext.
  • New QML types can be exported through qmlRegisterType.
  • The properties of native objects are exported as properties, and the slots or functions declared with Q_INVOKABLE can be invoked in QML.
  • Create plugins for extension using QDeclarativeExtensionPlugin.