perjantai 14. maaliskuuta 2014

Drawing on Sailfish


Some time ago I got myself Jolla phone (which is based on Sailfish OS), and while I had some difficulties with it in beginning - the fully gesture-based UI takes a bit for getting used to - after latest system update ("Naamankajärvi") I've been really happy.

Well, except on application front, but that was to be expected. There aren't that many apps that are on my must-have list, but those two that that aren't available are very useful to me. These of course are Karttaselain and Sports tracker (Warning; their web site appears to be flash-based).

First one is very useful when in forest to keep you from wandering to a property where you shouldn't be, and latter is handy when, well, training, although I generally used only the distance measurement in that (which was really bad anyway, at worst showing 30% more distance than I actually had walked).

So, since these aren't available, I might as well try SailfishOS/Qt programming out and write simple combined app myself. As usual, primarily to scratch my own itch.

Sailfish encourages apps to be written in QML/Qt system. User interface is done with QML (with javascript based logic) and simple applications actually can be written entirely in it. I expected to be handling a lot of custom/downloaded graphics, so the natural choice was to write the UI in QML and backend in C++.

This however was when I started to run into trouble. Sailfish uses (rather new) Qt5, and while the Qt reference pages are very exhaustive (as in lots of minute detail on functions), they are really bad in describing overall basics (remember, I had never used Qt before). Also a lot of information in web is for older versions of Qt, and I constantly ran into forum posts and mailing lists that essentially said "you shouldn't do it that way" (and of course failing to tell what is the correct way) or "you can do that, but it's not supported" (again, failing to tell what is the correct way).

A frustrating week, indeed (granted, I was able to put only hour or two a day to this). I'll just paraphrase Edison: "I didn't fail at drawing graphics in dozens of ways, I found out dozens of ways that no longer work [in context of Sailfish]". If I were lesser programmer I just might have given up before getting anywhere.

So, to help someone else that might be in similar situation, I'll just post some basic drawing code here. I tried to remove all excess cruft so this only demonstrates how to integrate C++ drawing in QML UI in Sailfish.

This is based on default app provided with Sailfish SDK, so some adaptation is needed to make things fit together, but I'll expect any reader to handle that with ease. Also note that I have not tried compiling this at all, this is stripped down version of my current implementation so there may be some errors. I also haven't even tried this on real device yet, just in emulator, so all this may need to be taken with proverbial grain of salt.

And remember also that I am by no means Qt expert, I try to explain what is going on as well as I can, but there may be errors here.

Let's start with C++ header, "graph.h". The main view in this case is "GraphView", which is derived from QQuickPaintedItem.

Method paint is where the magic happens. This is called by framework whenever contents of item should be repainted. More of it below.

Q_INVOKABLEs are functions that can be called from QML code; in this case they are used to grab mouse (sorry, finger) movement so view can be scrolled.
class GraphView : public QQuickPaintedItem
{
    Q_OBJECT

public:
    GraphView(QQuickItem *parent = 0);
    ~GraphView();

public:
    void paint(QPainter *painter);

    Q_INVOKABLE void onPress(int x, int y);
    Q_INVOKABLE void onMove(int xd, int yd);
    Q_INVOKABLE void onRelease(int x, int y);

private:
    bool moving;
    QImage *testpic;
    int xpos,ypos;
    int moveLastX,moveLastY;
};

Then the QML of this view, "graphpage.qml" (this should be referred from "main.qml" as initial view). Note CustomComponents declaration; this is needed for our C++ -defined view, which is defined as "GraphView". MouseArea is used to grab and pass touch notifications to the C++ code; the implementation simply passes the event to Q_INVOKABLEs defined in C++ code.

import QtQuick 2.0
import Sailfish.Silica 1.0
import CustomComponents 1.0

Page {
    id: graphpage

    GraphView {
        id: graphview
        objectName: "graphview"
        anchors {
            top: parent.top
            left: parent.left
            right: parent.right
            bottom: parent.bottom
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
            }

            onPressed: {
                graphview.onPress(mouse.x, mouse.y)
            }

            onReleased: {
                graphview.onRelease(mouse.x, mouse.y)
            }

            onPositionChanged: {
                graphView.onMove(mouse.x, mouse.y)
            }
        }
    }
}

Then the C++ itself. Note again that the standard headers/includes are not included in code below. More information on C++ function comments.
#include "graph.h"


/* ----------------------------------------------------
 * Default constructor.
 * We also create a new QImage that is drawn on screen; this is
 *
expected to be small 32x32 PNG that is defined in resources.
 * Note that if image does not exist in resources, this does NOT throw
 *
but silently creates an empty (invisible) image.
 */
GraphView::GraphView(QQuickItem *parent)  :
        QQuickPaintedItem(parent),
        moving(false),
        xpos(100),
        ypos(100)
{
    testpic = new QImage(":/test.png");
}


/* ------------------------------------------------
 * Remember to always clean up after yourself!
 */
GraphView::~GraphView()
{
    if (testpic)
        delete testpic;
}



/* ------------------------------------------------
 * This is the main drawing function that is called by framework
 *
whenever updating is needed. Do not call this yourself.
 *
 * As example we have just a small image that can be moved around
 * with touch.
 *
 */
void GraphView::paint(QPainter *painter)
{
    QRectF r(xpos,ypos,32,32);
    painter->drawImage(r, *testpic);
}



/* -------------------------------------------------
 * This is called when touch is detected. Mark that we are moving
 * (although it shouldn't be necessary in touch-based UI; we should
 * get move-events only when pressed anyway) and start coordinates.
 */
void GraphView::onPress(int x, int y)
{
    moving = true;
    moveLastX = x;
    moveLastY = y;
}


/* ----------------------------------------------------
 * Touch/mouse moved. If pressed, calculate new position and
 * signal framework that this should be redrawn.
 */
void GraphView::onMove(int x, int y)
{
    if (moving) {
        int dx = x-moveLastX;
        int dy = y-moveLastY;


        if (dx && dy) {
            xpos += dx;
            ypos += dy;

            moveLastX = x;
            moveLastY = y;
           
             // I am not exactly sure that this is the most correct way
             //
to do this, but it appears to work.
            setFlag(QQuickItem::ItemHasContents);
            update();
        }

    }
}


/* --------------------------------------------------------
 * Touch released; send final move event (it should have been
 * handled already anyway, but just in case) and remove "moving" flag.
 */
void GraphView::onRelease(int x, int y)
{
    onMove(x,y);
    moving = false;
}




/* ---------------------------------------------------------
 * The main.
 */
int main(int argc, char *argv[])
{
    QGuiApplication* app(SailfishApp::application(argc, argv));

     // Register our custom view in CustomComponents package, version
     //
1.0, so it is available in QML.
    qmlRegisterType<GraphView>("CustomComponents", 1, 0,"GraphView");

     // Usual initialisation.
    QQuickView* view = SailfishApp::createView();
    view->setSource(SailfishApp::pathTo("qml/main.qml"));
    view->show();

    return app->exec();

}
So there. Hope this helps :)

Oh, and of the my own map/tracker app - it's work in progress, I may or may not release it at some point. If there is interest, drop me a note, I'll keep updating this page. The main issue is the gigabytes of map data that needs to be hosted somewhere - and hosting isn't exactly free, unfortunately.

Don't expect anything too quickly however - right now I'm trying to figure out how to work with QThreads (to make downloading pages that aren't yet locally available a background operation). I expect it to take a good calendar week (again, hour or two a day) to get it working based on quick glance I took yesterday.



Ei kommentteja:

Lähetä kommentti