Files:
The Scratch Pad example demonstrates how to take a geometrical shape or image and replicate it, using N rows and M columns to produce a new resulting image based on N*M.
In this example we'll learn how to use the QPainter and QImage class' to replicate and create new images based on some given input.
The sample application provides an UI consisting of the image, image buttons to allow for various image selections and two TextFields that allow the user to input the row and column data values.
ImageView { horizontalAlignment: HorizontalAlignment.Center preferredHeight: 500 preferredWidth: 700 minWidth: 700 minHeight: 500 image: _scratchpad.image }
This is the ImageView that displays the resulting image based on user input and selection.
// Container with images to select Container { topMargin: 50 layout: StackLayout { orientation: LayoutOrientation.LeftToRight } Button { imageSource: "asset:///images/Blue_20Nose_20Thumb.png" onClicked: { _scratchpad.object = "images/Blue_20Nose_20Thumb.png" } } Button { imageSource: "asset:///images/Zeppelin_Thumb.png" onClicked: { _scratchpad.object = "images/Zeppelin_Thumb.png" } } Button { imageSource: "asset:///images/warning.png" onClicked: { _scratchpad.object = "images/warning.png" } } Button { text: "\u25CB" // unicode char for white circle onClicked: { _scratchpad.object = "circle" } } Button { text: "\u25A1" // unicode char for white square onClicked: { _scratchpad.object = "square" } } }
Whenever the user clicks one of these buttons, the scratchpad's object property is set and the image is updated and modified according to the new image selection and user inputs(rows, columns).
TextField { id: rowText topMargin: 50 hintText: qsTr("Enter # rows (1 or more) of replicants") inputMode: TextFieldInputMode.NumbersAndPunctuation input { submitKey: SubmitKey.EnterKey onSubmitted: { _scratchpad.rows = rowText.text } } } TextField { id: columnText hintText: qsTr("Enter # columns (1 or more) of replicants") inputMode: TextFieldInputMode.NumbersAndPunctuation input { submitKey: SubmitKey.EnterKey onSubmitted: { _scratchpad.columns = columnText.text } } }
The rows and columns TextField set the ScratchPads properties respectively. Once these have been set, the image is automatically updated in the onTextChanged signal handler and invoke the updateImage() of the App object when setColumns() or setRows() is invoked.
The App is the central class of the application that creates the UI and provides the properties that allow for image selection and replication.
App::App() : m_rows(1) , m_columns(1) { QmlDocument *qml = QmlDocument::create("asset:///main.qml"); qml->setContextProperty("_scratchpad", this); AbstractPane *root = qml->createRootObject<AbstractPane>(); Application::instance()->setScene(root); }
In the constructor the member variables are initialized and the UI is created.
void App::setObject(const QString &object) { if (m_object == object) return; m_object = object; emit objectChanged(); updateImage(); }
This method is called when the object property is updated whenever the user clicks on one of the buttons, with the new image selection as the argument. This in turn invokes updateImage() which reflects the changes through the ImageView.
void App::setRows(const QString &text) { bool ok = false; int rows = text.toInt(&ok); if (!ok || (rows <= 0)) rows = 1; if (m_rows == rows) return; m_rows = rows; emit rowsChanged(); updateImage(); } void App::setColumns(const QString &text) { bool ok = false; int columns = text.toInt(&ok); if (!ok || (columns <= 0)) columns = 1; if (m_columns == columns) return; m_columns = columns; emit columnsChanged(); updateImage(); }
These methods are invoked in the onTextChanged() signal handler, when its sets the scratchpad objects rows/columns property, using the property callback mechanism. Each of these methods invokes updateImage() after setting their respective values, which will reflect the changes in the ImageView.
QSize App::desiredReplicantSize() const { const int numPixelsX = workingImageSize.width() / m_columns; const int numPixelsY = workingImageSize.height() / m_rows; return QSize(numPixelsX, numPixelsY); }
The method calculates the image size, before its replicated, so that the final image is created according to the requested rows*column size.
void App::updateImage() { QImage replicant; if (m_object == QLatin1String("circle")) replicant = Draw::drawCircle(desiredReplicantSize()); else if (m_object == QLatin1String("square")) replicant = Draw::drawSquare(desiredReplicantSize()); else { // Load from file replicant.load(QString::fromLatin1("app/native/assets/%1").arg(m_object)); replicant = replicant.scaled(desiredReplicantSize()); } const QImage finalImage = Draw::replicate(workingImageSize, replicant, m_rows, m_columns).rgbSwapped(); const bb::ImageData imageData = bb::ImageData::fromPixels(finalImage.bits(), bb::PixelFormat::RGBX, finalImage.width(), finalImage.height(), finalImage.bytesPerLine()); m_image = bb::cascades::Image(imageData); emit imageChanged(); }
This method decides how to set the proper image, by generating the url location or calling the methods to draw the geometrical shape, and calling into the Draw namespace to provide the final image result.
This namespace is responsible for drawing the geometrical shapes and producing the image replications.
QImage Draw::drawCircle(const QSize &size) { // Create an image of the appropriate size. // The underlying data is reference counted so is cleaned up as needed. QImage image(size, QImage::Format_RGB32); image.fill(Qt::black); // Pick an arbitrary size for the circle const int centerX = size.width() / 2; const int centerY = size.height() / 2; const int radius = std::min(centerX, centerY) * 2 / 3; const int diameter = radius * 2; // Draw the circle! QPainter painter(&image); painter.setPen(Qt::white); painter.drawEllipse(centerX-radius, centerY-radius, diameter, diameter); return image; }
This method calculates the (x,y) coordinates, the radius and diameter based on the provided image size to draw a circle using the QPainter class.
QImage Draw::drawSquare(const QSize &size) { QImage image(size, QImage::Format_RGB32); image.fill(Qt::black); // Pick an arbitrary size for the square const int centerX = size.width() / 2; const int centerY = size.height() / 2; const int w = size.width() * 2 / 3; const int h = size.height() * 2 / 3; // Draw the square! QPainter painter(&image); painter.setPen(Qt::white); painter.drawRect(centerX - w/2, centerY - h/2, w, h); return image; }
This method calculates the (x,y) coordinates, the width, and height based on the provided image size to draw a square using the QPainter class.
QImage Draw::replicate(const QSize &destinationSize, const QImage &replicant, int numRows, int numCols) { if (numRows <= 0 || numCols <= 0) return QImage(); // illegal call QImage destination(destinationSize, QImage::Format_RGB32); const int numPixelsX = destinationSize.width() / numCols; const int numPixelsY = destinationSize.height() / numRows; const int extraPixelsX = destination.width() - (numPixelsX * numCols); const int extraPixelsY = destination.height() - (numPixelsY * numRows); const int offsetX = extraPixelsX / 2; const int offsetY = extraPixelsY / 2; // Fill the extra pixels and overwrite the last contents // in case there is any transparency destination.fill(Qt::black); // Replicate QPainter painter(&destination); for (int rows = 0; rows < numRows; ++rows) { const int y = offsetY + rows * numPixelsY; for (int cols = 0; cols < numCols; ++cols) { painter.drawImage(offsetX + cols * numPixelsX, y, replicant); } } return destination; }
This method calculates the size of the final image and the x/y offsets on this image required for placement of the original image(scaled version) according to row*column pattern.