×
Namespaces

Variants
Actions
Revision as of 00:08, 13 May 2012 by jupaavola (Talk | contribs)

QmlPaint - how to make paint application with QML

From Nokia Developer Wiki
Jump to: navigation, search

This article explains how to create paint application with QML

Article Metadata
Code ExampleTested with
SDK: Qt SDK 1.2.1
Devices(s): Nokia N8
Compatibility
Platform(s): Symbian Anna/Belle
Symbian
Article
Keywords: qml image flickable canvas drawpoint drawline scaled
Created: jupaavola (16 May 2012)
Last edited: jupaavola (13 May 2012)

Note.pngNote: This is an entry in the PureView Imaging Competition 2012Q2

Contents

Introduction

While QML offers easy way to show images on screen and put those to everywhere, it still lacks of possibility to edit image itself. All imaging is only about to display, not any chance to modify or even save. But when mixing Qt, it is possible to edit image. Also from Qt we can draw on to the display.

Derive new component from QDeclarative to make it accessible from QML. Why would not do all with Qt, answer is simple; QML offer much better and nicer ways to show data to user. Let's imagine paint application, device screen itself is small and eventually drawing small pictures is not so fun. If we create item to handle drawing, put it to Flickable component and we get more space. And more, afterall image to be show'd is QPixmap. No need to be blank, that can be anything from Gallery. Saving is also possible, Qt images already provide functions for that.

Paint application also needs undo. Idea is quite simple, save drawed points so those can be popped out from image.

While this item is not derived from QDeclarativeImage or any other image class, it need to implement some of the functions by itself. Drawing and scaling are the most important.

Whole example can be founded from projects: http://projects.developer.nokia.com/QmlPaintExample

Creating canvas and loading image

Canvas item is best to create with to images, one for display and one for backup. Without scaling of displayed image, backup is not necessary. But when using scaling, like using canvas in Flickable, it is too painful to figure out where is finger pointing and what could be image and display scaling.

Creating empty image and loading from image differs slightly. As creating image in the example, is creating first the displayed image and then backup which is also image used to saving. But in loading method, image is first loaded and then put on to display image. Reason here were to keep displayed image as long as we can, if in image reading occurs error the application won't erase ongoing work.

Remember when using Image source from QML, it is QUrl to file but Qt wants filename as QString. Use QUrl::toLocalFile() to convert.

void ImageCanvas::setUnderneathImage(QUrl filename)
{
m_underneathImageFilename=filename;
QFile file(m_underneathImageFilename.toLocalFile());
if(file.open(QIODevice::ReadOnly)){
QScopedPointer<QImageReader> reader(new QImageReader(&file));
m_sourceImage = reader->read();
 
m_penPoints.clear();
m_penPointsScreen.clear();
 
if(reader->error()|| m_sourceImage.isNull()){
m_error = LoadError;
m_errorString = reader->errorString();
qDebug() << "error: " << reader->error() << " str: " << reader->errorString();
emit error();
}else{
m_editImage = QPixmap::fromImage(m_sourceImage);
m_sourceSize = m_editImage.size();
m_originalSourceSize = m_sourceImage.size();
this->setImplicitHeight(m_editImage.height());
this->setImplicitWidth(m_editImage.width());
this->update();
m_error = NoError;
emit sourceSizeChanged();
emit originalSourceSizeChanged();
}
}else{
m_error = LoadError;
m_errorString = file.errorString();
}
emit underneathImageChanged();
}
 
void ImageCanvas::createEmpty(int width,int height)
{
if(width>0&&height>0){
m_editImage = QPixmap(width,height);
if(!m_editImage.isNull()){
m_editImage.fill();
m_sourceSize = m_editImage.size();
m_originalSourceSize = m_sourceSize;
this->setImplicitHeight(m_editImage.height());
this->setImplicitWidth(m_editImage.width());
this->update();
m_error = NoError;
m_sourceImage = m_editImage.toImage();
emit sourceSizeChanged();
emit originalSourceSizeChanged();
}else{
m_error = CreateError;
emit error();
}
}
}


Scaling

Creating bigger image than screen resolution is, scaling acts important part and it also affects to drawing.

So, using property for this to make easy access from QML. As QML is mostly informing new scaling resolutions to image. Keep also original sizes, it comes handy when showing image in original size.

Q_PROPERTY(QSize sourceSize READ sourceSize WRITE setSourceSize NOTIFY sourceSizeChanged)
Q_PROPERTY(QSize originalSourceSize READ originalSourceSize WRITE setOriginalSourceSize NOTIFY originalSourceSizeChanged)

That was about settings scale resolutions, more important is to draw image in correct size. Handle this in paint method what is protected from QDeclarativeItem. Of course QML should not inform what kind of image it wants. Just easiest way, draw image to here. Canvas item also knows it's width and height. If those are not matching with image size, draw scaled and otherwise full size.

void ImageCanvas::paint(QPainter* painter, const QStyleOptionGraphicsItem* styleOption, QWidget* widget)
{
qDebug() << __PRETTY_FUNCTION__ << " image: " << m_editImage.isNull() << " save: " << m_saveInProgress;
if(m_editImage.isNull())
return;
 
if(m_saveInProgress)
return;
 
bool aa = painter->testRenderHint(QPainter::Antialiasing);
 
if(smooth()){
painter->setRenderHint(QPainter::Antialiasing,true);
}
 
if(width()!=m_editImage.width() || height()!=m_editImage.height()){
painter->drawPixmap(QRectF(this->x(),this->y(),this->width(),this->height()),
m_editImage,
m_editImage.rect());
}else{
painter->drawPixmap(QPoint(0,0),m_editImage);
}
 
if(smooth()){
painter->setRenderHint(QPainter::Antialiasing,aa);
}
}

Drawing

Example takes care of two different drawing styles; single points and lines. All data is stored in two QList's, one for displayed image and and with original image resolution. Original image resolution is not changed, then why to need separate point storage for it ?

It's the undo function, if keep only one storage for points it would be possible to take drawed points out from screen but not from image to be saved. Also, editable image meaning the one to be displayed could be scaled and so are the points also. Both lists are following own image resolutions and cannot be mixed together.

Single point drawing is little bit easier, take mouse event point and put coordinates to point. But when drawing line, there is one and few other issues. First is line need starting point. This could be single point on screen and then appending new point to list with this single point as starting and other screen tap to ending point. Or do not draw anything before second tap, line ending coordinates is there. Now if want line to follow last line ending, look for last point from list. Currently last ones ending is new starting.

Drawing needs helper class to keep on track of points or anykind of shapes what to draw, if points would be single points the helper class is not needed. For other shapes, lines for example, helper class is there to help.

Prepare point

class CanvasPoint
{
public:
CanvasPoint(){}
 
QPen pen;
QPointF start;
QPointF end;
int type;
};

Then we need get point from QML. The point where finger is pointing. Get only when position is changed, if following separately X/Y axis changing leads to situation where QML is informing all the time new points and points get to drawn on places where not wanted.

 
MouseArea{
anchors.fill: parent
onMousePositionChanged: {
if(canvas.penEnabled){
canvas.penPosition = Qt.point(mouse.x,mouse.y);
mouse.accepted = true;
}
}
}

Point to Qt side, set it and call update image:

Q_PROPERTY(QPoint penPosition READ penPosition WRITE setPenPosition NOTIFY penPositionChanged)
 
QPoint penPosition(){return QPoint(m_penX,m_penY);}
void setPenPosition(QPoint point){m_penX=point.x();m_penY=point.y(); emit penPositionChanged(); updateImage();}

Updating image

While talking all the time about scaling, it is mostly visible in here updateImage() function. Mouse pointer event reports resolution points where it is. Meaning, if screen width is 360 pixels, it never report over that even flickable is showing image from different point. Drawing points need take care of image position on screen and finger tap position on screen, then calculate scaling and applying it to point. And of course, scaling of shown and backup image are different.

void ImageCanvas::updateImage()
{
qDebug() << __PRETTY_FUNCTION__;
if(m_penEnabled){
QPen pen(m_color,m_penWidth,Qt::SolidLine,m_penCapStyle,m_penJoinStyle);
 
QScopedPointer<QPainter> painter(new QPainter());
 
painter->begin(&m_editImage);
if(smooth()){
painter->setRenderHint(QPainter::Antialiasing,true);
}
 
qreal scaleX = m_editImage.width()/this->width();
qreal scaleY = m_editImage.height()/this->height();
 
// store point also with original size,
qreal storeScaleX = m_originalSourceSize.width()/this->width();
qreal storeScaleY = m_originalSourceSize.height()/this->height();
 
QPointF point;
if(m_editImage.size()!=m_sourceSize){
point = QPointF(m_penX*scaleX,m_penY*scaleY);
}else{
point = QPointF(m_penX,m_penY);
}
 
CanvasPoint cpoint;
cpoint.type = m_drawType;
painter->setPen(pen);
 
 
switch(m_drawType){
case DrawPen:{
cpoint.pen = pen;
cpoint.start = point;
m_penPointsScreen.append(cpoint);
painter->drawPoint(point);
 
CanvasPoint cstore;
cstore.pen = pen;
cstore.start = QPointF(m_penX*storeScaleX,m_penY*storeScaleY);
cstore.type = m_drawType;
m_penPoints.append(cstore);
}break;
case DrawLine:{
if(m_penPointsScreen.isEmpty()){
cpoint.pen = pen;
cpoint.start = point;
m_penPointsScreen.append(cpoint);
 
CanvasPoint cstore;
cstore.pen = pen;
cstore.start = QPointF(m_penX*storeScaleX,m_penY*storeScaleY);
cstore.type = m_drawType;
m_penPoints.append(cstore);
}else{
if(m_penPointsScreen.last().type==DrawLine){
if(m_penPointsScreen.last().end.isNull()){
m_penPointsScreen.last().end = point;
painter->drawLine(m_penPointsScreen.last().start,m_penPointsScreen.last().end);
m_penPoints.last().end = QPointF(m_penX*storeScaleX,m_penY*storeScaleY);
}else{
cpoint.start = m_penPointsScreen.last().end;
cpoint.end = point;
cpoint.pen = pen;
m_penPointsScreen.append(cpoint);
painter->drawLine(m_penPointsScreen.last().start,m_penPointsScreen.last().end);
 
CanvasPoint cstore;
cstore.pen = pen;
cstore.start = m_penPoints.last().end;
cstore.end = QPointF(m_penX*storeScaleX,m_penY*storeScaleY);
cstore.type = m_drawType;
m_penPoints.append(cstore);
}
}
if(m_penPointsScreen.last().type==DrawPen){
cpoint.start = m_penPointsScreen.last().start;
cpoint.end = point;
cpoint.pen = pen;
m_penPointsScreen.append(cpoint);
painter->drawLine(m_penPointsScreen.last().start,m_penPointsScreen.last().end);
 
CanvasPoint cstore;
cstore.pen = pen;
cstore.start = m_penPoints.last().start;
cstore.end = QPointF(m_penX*storeScaleX,m_penY*storeScaleY);
cstore.type = m_drawType;
m_penPoints.append(cstore);
}
}
}break;
default:break;
}
 
painter->end();
}
update();
}

Undo

Here is also implemented undo function. Idea is simple, as points are stored then pop out last one. This is why we have two images and both have own lists which contains points. Loop through images with lists, draw all points and update.

void ImageCanvas::undo()
{
if(!m_sourceImage.isNull() && !m_penPoints.isEmpty()){
m_saveInProgress = true;
 
QSize size = m_editImage.size();
m_editImage = QPixmap::fromImage(m_sourceImage);
m_penPoints.takeLast();
QScopedPointer<QPainter> p(new QPainter());
p->begin(&m_editImage);
for(int i=0;i<m_penPoints.count();i++){
CanvasPoint cpoint = m_penPoints.at(i);
p->setPen(cpoint.pen);
switch(cpoint.type){
case DrawPen:{
p->drawPoint(cpoint.start);
}break;
case DrawLine:{
p->drawLine(cpoint.start,cpoint.end);
}break;
default:break;
}
}
p->end();
if(size!=m_editImage.size())
m_editImage = m_editImage.scaled(size);
 
m_saveInProgress = false;
update();
}
}

Demo video

223 page views in the last 30 days.
×