10.5.1. QUndoCommand and Image Manipulation

[ fromfile: commandpattern.xml id: undoimagemanip ]

The following example demonstrates the use of QUndoCommand. This program makes use of image manipulation operations.[78] This example uses the QImage class, a hardware-independent representation of an image that enables you to manipulate individual pixels. QImage supports several of the most popular formats for images, including JPEG, a lossy compression system for photographic images that we use in this example.

First, as you see in Example 10.11, we derive a couple of typical image manipulation operations from QUndoCommand. The first operation adjusts the color of each pixel by applying double multipliers to its red, green, and blue components. The second operation replaces half of the image by the mirror image of its other half, either horizontally or vertically, depending on the user-supplied argument. The constructor for each operation takes a reference to the source QImage and instantiates an empty QImage with the same size and format.

Example 10.11. src/undo-demo/image-manip.h

[ . . . . ]
class AdjustColors : public QUndoCommand {
public:
   AdjustColors(QImage& img, double radj, double gadj, double badj)
     : m_Image(img), m_Saved(img.size(), img.format()), m_RedAdj(radj), 
     m_GreenAdj(gadj), m_BlueAdj(badj)   {setText("adjust colors"); }
   virtual void undo();
   virtual void redo();
private:
    QImage& m_Image;
    QImage m_Saved;
    double m_RedAdj, m_GreenAdj, m_BlueAdj;
    void adjust(double radj, double gadj, double badj);

};

class MirrorPixels : public QUndoCommand {
public:
   virtual void undo();
   virtual void redo();
[ . . . . ]

Each of the two operations makes a copy of the original image before changing any of its pixels. Example 10.12 shows the implementation of one of the operation classes, AdjustColors. Its constructor iterates through the pixels of the QImage and calls the pixel() function on each one. pixel() returns the color as an ARGB quad, an unsigned, 8-byte int in the format AARRGGBB, where each pair of bytes represents a component of the color. We operate on this quad (which has been given the typedef QRgb) using the functions qRed(), qGreen(), and qBlue() to tease out the individual values, each between 0 and 255, for the three basic colors.[79] It then replaces the pixel with adjusted values for red, green, and blue. Keep in mind that the color adjustment operation multiplies int values by double values and then assigns the products to int variables, which results in truncation. In other words, the adjustment cannot be reversed by performing the inverse multiplication.

The undo() method reverts to the saved copy of the image. The redo() method calls the pixel-changing function.

Example 10.12. src/undo-demo/image-manip.cpp

[ . . . . ]

void AdjustColors::adjust(double radj, double gadj, double badj) {
   int h(m_Image.height()), w(m_Image.width());
   int r, g, b;
   QRgb oldpix, newpix;
   m_Saved = m_Image.copy(QRect()); // save a copy of entire image   
   for(int y = 0; y < h; ++y) {
      for(int x = 0; x < w; ++x) {
         oldpix = m_Image.pixel(x,y);
         r = qRed(oldpix) * radj;
         g = qGreen(oldpix) * gadj;
         b = qBlue(oldpix) * badj;
         newpix = qRgb(r,g,b);
         m_Image.setPixel(x,y,newpix);
      }
   }
}

void AdjustColors::redo() {
   qDebug() << "AdjustColors::redo()";
   adjust(m_RedAdj, m_GreenAdj, m_BlueAdj);
}

void AdjustColors::undo() {
    qDebug() << "AdjustColors::undo()";
    m_Image = m_Saved.copy(QRect()); 
}

We designed the GUI using QtCreator. The QImage displays on the screen in a QLabel after being converted to a QPixmap. Figure 10.9 shows a photo image before this program worked on it.

Figure 10.9. The Undisturbed Original Photo

The Undisturbed Original Photo

Figure 10.10 shows the unfortunate scene after the AdjustColors and both kinds of MirrorPixels operations have been applied to it.

Figure 10.10. The Undo-Demo Screen

The Undo-Demo Screen

The UndoMainWin class is derived from QMainWindow and makes use of QUndoStack. By default, QtCreator embeds the Ui class as a pointer member of UndoMainWin. In Example 10.13, the private slots started out as stubs that QtCreator generated when we employed the Go to slot feature on widgets and actions from Designer.

Example 10.13. src/undo-demo/undomainwin.h

#ifndef UNDOMAINWIN_H
#define UNDOMAINWIN_H

#include <QMainWindow>
#include <QUndoStack>

class QWidget;
class QLabel;
class QImage;
class QEvent;
namespace Ui {
    class UndoMainWin;
}

class UndoMainWin : public QMainWindow {
    Q_OBJECT
 public:
    explicit UndoMainWin(QWidget* parent = 0);
    ~UndoMainWin();

 public slots:
    void displayImage(const QImage& img);

 private:
    Ui::UndoMainWin* ui;
    QLabel* m_ImageDisplay;
    QImage m_Image;
    QUndoStack m_Stack;

private slots:
    void on_redoButton_clicked();
    void on_openButton_clicked();
    void on_actionAdjust_Color_triggered();
    void on_actionUndo_The_Last_Action_triggered();
    void on_actionHorizontal_Mirror_triggered();
    void on_actionVertical_Mirror_triggered();
    void on_actionQuit_triggered();
    void on_actionSave_triggered();
    void on_actionClose_triggered();
    void on_saveButton_clicked();
    void on_quitButton_clicked();
    void on_adjustColorButton_clicked();
    void on_undoButton_clicked();
    void on_verticalMirrorButton_clicked();
    void on_horizontalMirrorButton_clicked();
    void on_actionOpen_triggered();
};

#endif // UNDOMAINWIN_H

In Example 10.14 you can see the implementation style that QtCreator uses to tie the parts together. Notice also the compact completions of the private slots that were listed in Example 10.13.

QImage is optimized for pixel manipulation. QPixmap uses video memory and is the class used by various widgets for images that need to be on-screen. As mentioned earlier, you can convert a QImage to a QPixmap and display it in a QLabel.

Example 10.14. src/undo-demo/undomainwin.cpp

[ . . . . ]
#include "image-manip.h"
#include "ui_undomainwin.h"
#include "undomainwin.h"

UndoMainWin::UndoMainWin(QWidget *parent) 
: QMainWindow(parent), ui(new Ui::UndoMainWin), 
  m_ImageDisplay(new QLabel(this)), m_Image(QImage()) {
  ui->setupUi(this);
  m_ImageDisplay->setMinimumSize(640,480);
}

UndoMainWin::~UndoMainWin() {
  delete ui; 1
}

void UndoMainWin::displayImage(const QImage &img) {
    m_ImageDisplay->setPixmap(QPixmap::fromImage(img)); 
}

void UndoMainWin::on_actionOpen_triggered() {
    m_Image.load(QFileDialog::getOpenFileName());
    displayImage(m_Image);
}

void UndoMainWin::on_horizontalMirrorButton_clicked() {
    MirrorPixels* mp = new MirrorPixels(m_Image, true);
    m_Stack.push(mp);
    displayImage(m_Image);
}


void UndoMainWin::on_adjustColorButton_clicked() {
    double radj(ui->redSpin->value()), gadj(ui->greenSpin->value()), 
    badj(ui->blueSpin->value());
    AdjustColors* ac = new AdjustColors(m_Image, radj, gadj, badj);
    m_Stack.push(ac);
    displayImage(m_Image);
}
[ . . . . ]

1

Neither a QObject nor a child, it must be deleted explicitly.




[78] This example was inspired by the work of Guzdial and Ericson [Guzdial07] and their MediaComp project.

[79] The fourth pair holds the value for the alpha component, which expresses the transparency of the pixel. It has a default value of ff (255), i.e., opaque.