Home

Art Editor Example

This example demonstrates how to use the Qt Undo/Redo framework. It is an editor which can be used to create Warhol'esque collages. The complete sources may be found in the example directory. The most rellevant files are shown below.


command.h:
#include <qtundo.h>

class Face;

class CmdChangeColor : public QtCommand
{
    Q_OBJECT

    public:
        CmdChangeColor(Face *face, const QString &color);
        virtual void redo();
        virtual void undo();

    protected:
        virtual bool mergeMeWith(QtCommand *other);

    private:
        QString m_old_color, m_new_color;
        Face *m_face;
};

class CmdChangeImage : public QtCommand
{
    Q_OBJECT

    public:
        CmdChangeImage(Face *face, const QString &image);
        virtual void redo();
        virtual void undo();

    protected:
        virtual bool mergeMeWith(QtCommand *c);

    private:
        QString m_old_image, m_new_image;
        Face *m_face;
};


comand.cpp:
#include "commands.h"
#include "faceedit.h"

CmdChangeColor::CmdChangeColor(Face *face, const QString &color)
{
    m_face = face;
    m_old_color = face->color();
    m_new_color = color;
    setCanMerge(true);
    setDescription("change " + m_face->name() + " " + m_face->image() + "'s color to " + m_new_color);
}

void CmdChangeColor::redo()
{
    m_face->setColor(m_new_color);
}

void CmdChangeColor::undo()
{
    m_face->setColor(m_old_color);
}

bool CmdChangeColor::mergeMeWith(QtCommand *c)
{
    if (qstrcmp(c->className(), className()))
        return false;
    CmdChangeColor *other = (CmdChangeColor*) c;

    if (m_face != other->m_face)
        return false;

    m_new_color = other->m_new_color;
    setDescription("change " + m_face->name() + " " + m_face->image() + "'s  color to " + m_new_color);
    return true;
}

CmdChangeImage::CmdChangeImage(Face *face, const QString &image)
{
    m_face = face;
    m_old_image = face->image();
    m_new_image = image;

    setCanMerge(false);
    setDescription("change " + m_face->name() + " from " + m_old_image + " to " + m_new_image);
}

void CmdChangeImage::redo()
{
    m_face->setImage(m_new_image);
}

void CmdChangeImage::undo()
{
    m_face->setImage(m_old_image);
}

bool CmdChangeImage::mergeMeWith(QtCommand *c)
{
    if (qstrcmp(c->className(), className()))
        return false;
    CmdChangeImage *other = (CmdChangeImage*) c;

    if (m_face != other->m_face)
        return false;

    m_new_image = other->m_new_image;
    setDescription("change " + m_old_image + " to " + m_new_image);
    return true;
}


faceedit.h:
#ifndef FACEEDIT_H
#define FACEEDIT_H

#include <qimage.h>
#include <qmainwindow.h>
#include <qvaluevector.h>
#include <qstring.h>

class QtUndoStack;

class Face
{
    public:
        Face(const QString &name = QString::null);

        void setColor(const QString &color);
        void setImage(const QString &image);
        QString color() const;
        QString image() const;
        QString name() const;

    private:
        QString m_color_name, m_image_name, m_face_name;
        QImage m_image;
        QColor m_color;

    friend class FaceCanvas;
};

class FaceCanvas : public QWidget
{
    Q_OBJECT

    public:
        FaceCanvas(QWidget *parent = 0, const char *name = 0);
        int focusCol() const;
        int focusRow() const;
        void setFocus(int col, int row);
        Face &face(int col, int row);
        Face &focusedFace();

    signals:
        void focusChanged();

    private:
        void paintEvent(QPaintEvent *);
        void mousePressEvent(QMouseEvent *e);

        QValueVector<Face> m_face_list;
        int m_focus_col, m_focus_row;
};

class FaceEdit : public QMainWindow
{
    Q_OBJECT

    public:
        FaceEdit(QWidget *parent = 0, const char *name = 0);
        void setImage(const QString &image);
        void setColor(const QString &color);
        bool isClean();
        Face &focusedFace();

    public slots:
        void clearFaces();
        bool load(const QString &file_name);
        void save();
        void updateCaption();

    signals:
        void cleanChanged();
        void focusChanged();

    private:
        QtUndoStack *m_undo_stack;
        QString m_name;
        FaceCanvas *m_face_canvas;

        static uint m_instance_cnt;
};

#endif


faceedit.cpp:
#include <qpainter.h>
#include <qfile.h>
#include <qmessagebox.h>
#include <qfiledialog.h>
#include <qaction.h>

#include <qtundo.h>

#include "faceedit.h"
#include "commands.h"

static const int g_grid_cols = 4;
static const int g_grid_rows = 3;
static const int g_image_w = 110;
static const int g_image_h = 110;
static const QString g_unnamed = "Unnamed";
static const QString g_file_header = "FaceEdit_file_v1.0";

static QString chopPath(const QString &path)
{
    int idx = path.findRev("/");
    if (idx == -1)
        idx = path.findRev(QDir::separator());
    if (idx == -1)
        return path;
    return path.mid(idx + 1);
}

/******************************************************************
** class Face
**
** Holds the state (color and image) of a single cell
*/

Face::Face(const QString &name)
{
    m_face_name = name;
    m_color = Qt::black;
    m_color_name = "black";
    m_image_name = "none";
}

void Face::setColor(const QString &color)
{
    m_color_name = color;
    m_color = QColor(m_color_name);
    if (!m_image.isNull())
        m_image.setColor(0, m_color.rgb());
}

void Face::setImage(const QString &image)
{
    m_image_name = image;

    if (m_image_name == "none")
        m_image = QImage();
    else {
        m_image = QImage::fromMimeSource(m_image_name).copy();
        m_image.setColor(0, m_color.rgb());
    }
}

QString Face::color() const
{
    return m_color_name;
}

QString Face::image() const
{
    return m_image_name;
}

QString Face::name() const
{
    return m_face_name;
}

/******************************************************************
** class FaceCanvas
**
** A widget with a grid of faces. Handles painting of faces and focus.
*/

FaceCanvas::FaceCanvas(QWidget *parent, const char *name)
    : QWidget(parent, name)
{
    setFixedSize(g_grid_cols*g_image_w, g_grid_rows*g_image_h);
    int num_faces = g_grid_cols*g_grid_rows;
    for (int i = 0; i < num_faces; ++i) {
        int col = i % g_grid_cols;
        int row = i / g_grid_cols;
        QString name = "(" + QString::number(col) + ", " + QString::number(row) + ")";
        m_face_list.push_back(Face(name));
    }
    m_focus_col = 0;
    m_focus_row = 0;
}

Face &FaceCanvas::face(int col, int row)
{
    return m_face_list.at(row*g_grid_cols + col);
}

void FaceCanvas::paintEvent(QPaintEvent*)
{
    QPainter p(this);

    p.setPen(QPen(Qt::red, 2, Qt::DotLine));

    for (int col = 0; col < g_grid_cols; ++col) {
        for (int row = 0; row < g_grid_rows; ++row) {
            int x = col*g_image_w;
            int y = row*g_image_h;
            const Face &f = face(col, row);
            if (!f.m_image.isNull())
                p.drawImage(x, y, f.m_image, 0, 0, g_image_w,  g_image_h);
            if (col == m_focus_col && row == m_focus_row)
                p.drawRect(x, y, g_image_w, g_image_h);
        }
    }
}

void FaceCanvas::mousePressEvent(QMouseEvent *e)
{
    QPoint pos = e->pos();
    setFocus(pos.x()/g_image_w, pos.y()/g_image_h);
}

Face &FaceCanvas::focusedFace()
{
    return face(m_focus_col, m_focus_row);
}

void FaceCanvas::setFocus(int col, int row)
{
    if (m_focus_col == col && m_focus_row == row)
        return;

    m_focus_col = col;
    m_focus_row = row;
    emit focusChanged();
    update();
}

/******************************************************************
** class FaceEdit
**
** A face editor window. This class owns an undo stack. Also handles
** loading/saving.
*/

uint FaceEdit::m_instance_cnt = 0;

FaceEdit::FaceEdit(QWidget *parent, const char *name)
    : QMainWindow(parent, name, Qt::WDestructiveClose)
{

    m_undo_stack = new QtUndoStack(this);
    connect(m_undo_stack, SIGNAL(cleanChanged(bool)),
                this, SLOT(updateCaption()));
    connect(m_undo_stack, SIGNAL(cleanChanged(bool)),
                this, SIGNAL(cleanChanged()));

    QAction *undo_action = m_undo_stack->createUndoAction(this);
    QAction *redo_action = m_undo_stack->createRedoAction(this);
    QToolBar *tool_bar = new QToolBar(this);
    undo_action->addTo(tool_bar);
    redo_action->addTo(tool_bar);

    m_face_canvas = new FaceCanvas(this);
    connect(m_face_canvas, SIGNAL(focusChanged()), this, SIGNAL(focusChanged()));
    setCentralWidget(m_face_canvas);
    /* Some operations on QtUndoStack result in a series of commands being executed. If each
       command caused update(), we would get flicker. commandExecuted() is emitted whenever
       an operation on QtUndoStack results in one or more commands being executed, but the signal
       is emitted only once. */
    connect(m_undo_stack, SIGNAL(commandExecuted()), m_face_canvas, SLOT(update()));

    m_name = "Unnamed" + QString::number(++m_instance_cnt);
    updateCaption();
}

void FaceEdit::clearFaces()
{
    /* Implemented using a macro command. We push a MacroBegin, then
       a chain of commands which clears the image and color of each
       box in turn, and finally a MacroEnd. */
    m_undo_stack->push(new QtCommand(QtCommand::MacroBegin, "Clear faces"));

    for (int col = 0; col < g_grid_cols; ++col) {
        for (int row = 0; row < g_grid_rows; ++row) {
            Face &f = m_face_canvas->face(col, row);
            m_undo_stack->push(new CmdChangeImage(&f, "none"));
            m_undo_stack->push(new CmdChangeColor(&f, "black"));
        }
    }

    m_undo_stack->push(new QtCommand(QtCommand::MacroEnd));
}

void FaceEdit::setImage(const QString &image)
{
    Face &f = m_face_canvas->focusedFace();
    if (f.image() == image)
        return;

    /* Storing a pointer to the target object in a command is dangerous, since the object
       may get deleted. However, FaceCanvas keeps its grid of Faces for the duration of
       its life. If that was not the case, we would have to store the coordinates of the
       face instead. */
    m_undo_stack->push(new CmdChangeImage(&f, image));
}

void FaceEdit::setColor(const QString &color)
{
    Face &f = m_face_canvas->focusedFace();
    if (f.color() == color)
        return;

    m_undo_stack->push(new CmdChangeColor(&f, color));
}



bool FaceEdit::load(const QString &file_name)
{
    QFile file(file_name);
    if (!file.open(IO_ReadOnly)) {
        QMessageBox::warning(this, "File error", "Could not open:\n" + file_name,
                                QMessageBox::Ok, QMessageBox::NoButton);
        return false;
    }

    QTextStream stream(&file);

    QString header;
    stream >> header;
    if (header != g_file_header) {
        QMessageBox::warning(this, "File error", "This is not a FaceEdit file:\n" + file_name,
                                QMessageBox::Ok, QMessageBox::NoButton);
        return false;
    }

    for (int col = 0; col < g_grid_cols; ++col) {
        for (int row = 0; row < g_grid_rows; ++row) {
            Face &f = m_face_canvas->face(col, row);
            QString color;
            QString image;
            stream >> color >> image;
            if (stream.device()->status() != IO_Ok
                    || color.isNull()
                    || image.isNull()) {
                QMessageBox::warning(this, "File error", "Error reading:\n" + file_name,
                                        QMessageBox::Ok, QMessageBox::NoButton);
                return false;
            }
            f.setColor(color);
            f.setImage(image);
        }
    }

    m_name = file_name;
    m_undo_stack->clear();
    updateCaption();

    return true;
}

void FaceEdit::save()
{
    QString file_name = m_name;
    if (file_name.startsWith(g_unnamed)) {
        file_name = QFileDialog::getSaveFileName();
        if (file_name.isNull())
            return;
    }

    QFile file(file_name);
    if (!file.open(IO_WriteOnly)) {
        QMessageBox::warning(this, "File error", "Could not create:\n" + file_name,
                                QMessageBox::Ok, QMessageBox::NoButton);
        return;
    }
    QTextStream stream(&file);

    stream << g_file_header << '\n';

    for (int col = 0; col < g_grid_cols; ++col) {
        for (int row = 0; row < g_grid_rows; ++row) {
            Face &f = m_face_canvas->face(col, row);

            stream << f.color() << ' ' << f.image() << '\n';
            if (stream.device()->status() != IO_Ok) {
                QMessageBox::warning(this, "File error", "Error writing:\n" + file_name,
                                        QMessageBox::Ok, QMessageBox::NoButton);
                return;
            }
        }
    }

    m_name = file_name;
    m_undo_stack->setClean();
}

void FaceEdit::updateCaption()
{
    QString caption = chopPath(m_name);
    if (!isClean())
        caption += " (modified)";
    setCaption(caption);
}

bool FaceEdit::isClean()
{
    return m_undo_stack->isClean();
}

Face &FaceEdit::focusedFace()
{
    return m_face_canvas->focusedFace();
}


Copyright © 2003-2006 TrolltechTrademarks
Qt Solutions