Welcome!

Welcome to the official BlackBerry Support Community Forums.

This is your resource to discuss support topics with your peers, and learn from each other.

inside custom component

Native Development Knowledge Base

Using QImage and QPainter to Prepare a cascades::Image

by BlackBerry Development Advisor (Retired) on ‎07-23-2012 12:30 PM - edited on ‎04-16-2013 10:50 AM by Retired (12,945 Views)

Introduction

Qt UI objects cannot be mixed with Cascades™ UI objects in a Cascades application. However, Qt UI objects can be used as scratchpads to create the underlying images we want to render in Cascades.

 

This article explores using QImage and QPainter to load and manipulate some images prior to display.

 

The QImageScratchPad application

 The attached application has a display field, which is an ImageView.  The displayed image is constructed using QImage and QPainter.

 

There are five buttons, three with images loaded in the normal cascades fashion, two (circle and square) with images constructed using QImage and QPainter.

 

The application is simple.  Click on an image button to display it in the larger display view.  Change the number of rows or number of columns to shrink the image and replicate it in a grid.

 

By default, there is one row and one column.  Click on the button with the circle, and you see:

1x1 circle

 

 

Click on the left stamp button and enter 3 rows 2 columns to see:

3x2 bluenose

 

Code organization

The code is split into several files.

  • Routines that manipulate QImage are in draw.cpp.
    These routines are:
    - loadImage
    - drawCircle
    - drawSquare
    - replicate
  • The class that converts data from QImage pixel format to cascades::Image format is PixelBuffer, in PixelBuffer.cpp
  • app.cpp as usual manipulates the UI
  • There is only one QML file: assets/main.qml defines the one page of the app
  • assets/images contains three images from other samples used as button images but also loaded into a QImage

Loading an asset image into a QImage

A png file can be loaded into a QImage.  The QImage requires the pathname.  For images in the assets, we use the corresponding path name.   We take advantage of the application not changing directories.  In draw.cpp:

 

/*
 * Get the directory containing the images
 */
QString getAssetDir()
{
    // images are in assets/images.
    // This is under app of our initial working directory.
    // Since we don't change directories, it's safe to return a relative path.
    char cwd[PATH_MAX];
    getcwd(cwd, PATH_MAX);
    return QString(cwd) + "/app/native/assets";
}

/*
 * Convert an image url e.g. "asset:///images/Blue_20Nose_20Thumb.png"
 * into a full path that can be opened with routines that don't know asset://
 */
QString getImagePath(QUrl imageUrl)
{
    QString resourceText = imageUrl.toString();
    int index = resourceText.indexOf("/images/");
    return (index > 0)? getAssetDir() + resourceText.mid(index) : resourceText;
}

/*
 * Load an image from a QUrl.
 * We need to load the image from the corresponding path
 */
QImage loadImage(QUrl url)
{
    QString where = getImagePath(url);
    return QImage(where);
}

 

 

Manipulating QImage using QPainter

This sample draws circles and squares, scales an image, and copies an image onto a larger image.  This is done exactly as in a Qt application, in file draw.cpp:

 

To draw a circle:

        // Draw the circle!
        QPainter painter(&image);
        painter.setPen(Qt::yellow);
        painter.drawEllipse(centerX-radius, centerY-radius, diameter, diameter);

 

To draw a square:

        QPainter painter(&image);
        painter.setPen(Qt::yellow);
        painter.drawRect(centerX - w/2, centerY - h/2, w, h);

 

To scale an image:

     if (replicant.size() != desiredReplicantSize)
     {
         replicant = replicant.scaled(desiredReplicantSize);
     }

The stamp images loaded from assets are scaled to fit the grid cells in the display view, but we don't want to scale circles, as this distorts the image.  For pristine edges, we draw circles and squares into QImages of the correct size and do not scale them afterwards.

 

To create the grid of images, we copy the image into each grid cell:

     // Replicate
     QPainter painter(&destination);
     for (int rows = 0; rows < numRows; ++rows)
     {
         int y = offsetY + rows * numPixelsY;
         for (int cols = 0; cols < numCols; ++cols)
         {
             painter.drawImage(offsetX + cols * numPixelsX, y, replicant);
         }
     }

 

Creating a cascades::Image from a QImage

To convert from a QImage to an cascades::Image, we get the bits from the QImage and construct a PixelBufferData object, which can be used to create the cascades::Image. Unfortunately, QImage's pixel formats aren't really compatible with PixelBufferData.  If you use QImage bits as is, you may see a weird color shift in the output.  Fortunately we can easily convert from one format to the other by calling image.rgbSwapped().

 

Code in PixelBuffer.cpp demonstrates how to do this.

 

We create the QImage with format QImage::Format_RGB32:

    image = QImage(size, QImage::Format_RGB32);

 

For convenience and clarity, the pixel buffer conversion is done using a helper class, PixelBuffer, declared in PixelBuffer.h and implemented in PixelBuffer.cpp.  This maintains a buffer and a size.  This could have been done using std::vector, but instead uses:

    QSize m_sizeInPixels;
    unsigned char* m_buffer; // pixel data in PixelBufferData format

 

When we assign from a QImage, we first swap RGB, then copy the bits to our buffer.  The following code does this with excessive care to ensure no possibility of buffer over-read or over-write:

PixelBuffer& PixelBuffer::operator=(const QImage& image)
{
    QImage swapped     = image.rgbSwapped();
    QSize  swappedSize = swapped.size();

    int w = swappedSize.width();
    int h = swappedSize.height();
    int numPixels = w * h;
    int numBytes  = w * h * 4;
    if (swappedSize != m_sizeInPixels)
    {
        deleteBuffer();
        m_sizeInPixels = QSize(w, h);
        m_buffer = new uchar[numBytes];
    }

    // Copy the memory over.
    // We'll add defensive code in case rgbSwapped has a different size
    const uchar* from = swapped.constBits();
    int numFromBytes = swapped.numBytes();
    int numToCopy = std::min(numFromBytes, numBytes);

    memcpy(m_buffer, from, numToCopy);
    if (numToCopy < numBytes)
    {
        memset(m_buffer + numToCopy, 0x00, numBytes - numToCopy);
    }

    return *this;
}

 

Now we can return a data structure that can be used to construct a cascade::Image.  This structure does not take ownership of the underlying buffer, which allows us to create the structure with impunity while preserving the underlying buffer as long - or as short - as we want:

bb::cascades::PixelBufferData PixelBuffer::getBuffer() const
{
    return PixelBufferData(PixelBufferData&colon;:RGBX,
                           m_sizeInPixels.width(),
                           m_sizeInPixels.height(),
                           m_sizeInPixels.width(),
                           m_buffer);
}

 

 

Updating the UI

Now that we have a PixelBufferData in the correct format, we can create the cascades::Images we want and update the buttons or ImageView.

 

The buttons images are set in the App constructor in app.cpp:

        // Set the image on the circle and the square
        PixelBuffer pixelBuffer;
        button = root->findChild<Button*>("circle");
        pixelBuffer = drawCircle(QSize(50,50));
        button->setImage(pixelBuffer.getBuffer());

        button = root->findChild<Button*>("square");
        pixelBuffer = drawSquare(QSize(50,50));
        button->setImage(pixelBuffer.getBuffer());

 

The ImageView is updated in a similar fashion once the QImage is ready.  Note we chose to use a static PixelBuffer to reuse the buffer between calls:

void App::replaceImageInView(const QImage& qImage)
{
    if (m_finalImage == 0)
        return; // defensive

    static PixelBuffer pixels;
    pixels = qImage;

    m_finalImage->setImage(pixels.getBuffer());
}

 

Some memory management

The app avoids unnecessary copying of buffers.

 

QImage is designed to be copied, as it does copy-on-write of the underlying pixel data.  To ensure we use the same underlying buffers for the stamp images, we read the images once and store them in a map.  When we fetch a copy, we are merely getting a smart pointer and updating a reference count.  The map holds a reference as well, so the underlying data is released when statics are destroyed.

 

We rely on replicant.scaled() creating a copy of the buffer and not touching the original pixel buffer, so that next time we get a clean undistorted and unchanged copy of the original image.

 

PixelBuffer keeps an underlying buffer from one call to the next, so long as the size of the buffer has not changed. We use one static PixelBuffer for the ImageView.  We also use the same QImage scratchpad. This saves some memory allocation/deallocation. The effect might not be noticeable, but this keeps the memory overhead minimal, and in code, this is just a matter of using a static PixelBuffer rather than one on the stack.  The underlying buffer is released when the PixelBuffer object is destroyed.  Using a static variable that is edited relies on the draw routines all executed in one thread.

 

Source Code

The source code for this sample can be downloaded from the BlackBerry® project on Github® here:  https://github.com/blackberry/Cascades-Samples/tree/master/scratchpad

Comments
by New Developer on ‎09-10-2012 02:26 AM

    hi

    Does Qt for playbook supports the swipgesture or pangesture??

Users Online
Currently online: 10 members 549 guests
Please welcome our newest community members: