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.

<hr> command.h:

    /****************************************************************************
    **
    ** Copyright (C) 2003-2006 Trolltech ASA. All rights reserved.
    **
    ** This file is part of a Qt Solutions component.
    **
    ** Licensees holding valid Qt Solutions licenses may use this file in
    ** accordance with the Qt Solutions License Agreement provided with the
    ** Software.
    **
    ** See http://www.trolltech.com/products/qt/addon/solutions/
    ** or email [email protected] for information about Qt Solutions
    ** License Agreements.
    **
    ** Contact [email protected] if any conditions of this licensing are
    ** not clear to you.
    **
    ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    **
    ****************************************************************************/
    #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;
    };

<hr> comand.cpp:

    /****************************************************************************
    **
    ** Copyright (C) 2003-2006 Trolltech ASA. All rights reserved.
    **
    ** This file is part of a Qt Solutions component.
    **
    ** Licensees holding valid Qt Solutions licenses may use this file in
    ** accordance with the Qt Solutions License Agreement provided with the
    ** Software.
    **
    ** See http://www.trolltech.com/products/qt/addon/solutions/
    ** or email [email protected] for information about Qt Solutions
    ** License Agreements.
    **
    ** Contact [email protected] if any conditions of this licensing are
    ** not clear to you.
    **
    ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    **
    ****************************************************************************/
    #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)
    {
        CmdChangeColor *other = qobject_cast<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)
    {
        CmdChangeImage *other = qobject_cast<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;
    }

<hr> faceedit.h:

    /****************************************************************************
    **
    ** Copyright (C) 2003-2006 Trolltech ASA. All rights reserved.
    **
    ** This file is part of a Qt Solutions component.
    **
    ** Licensees holding valid Qt Solutions licenses may use this file in
    ** accordance with the Qt Solutions License Agreement provided with the
    ** Software.
    **
    ** See http://www.trolltech.com/products/qt/addon/solutions/
    ** or email [email protected] for information about Qt Solutions
    ** License Agreements.
    **
    ** Contact [email protected] if any conditions of this licensing are
    ** not clear to you.
    **
    ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    **
    ****************************************************************************/
    #ifndef FACEEDIT_H
    #define FACEEDIT_H

    #include <QImage>
    #include <QMainWindow>
    #include <QVector>
    #include <QString>

    class QtUndoStack;

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

        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);
        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);

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

    class FaceEdit : public QMainWindow
    {
    Q_OBJECT

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

        QtUndoStack *undoStack() const;

    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

<hr> faceedit.cpp:

    /****************************************************************************
    **
    ** Copyright (C) 2003-2006 Trolltech ASA. All rights reserved.
    **
    ** This file is part of a Qt Solutions component.
    **
    ** Licensees holding valid Qt Solutions licenses may use this file in
    ** accordance with the Qt Solutions License Agreement provided with the
    ** Software.
    **
    ** See http://www.trolltech.com/products/qt/addon/solutions/
    ** or email [email protected] for information about Qt Solutions
    ** License Agreements.
    **
    ** Contact [email protected] if any conditions of this licensing are
    ** not clear to you.
    **
    ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    **
    ****************************************************************************/
    #include <QFile>
    #include <QTextStream>
    #include <QPainter>
    #include <QMessageBox>
    #include <QFileDialog>
    #include <QAction>
    #include <QMouseEvent>
    #include <QToolBar>

    #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.lastIndexOf("/");
        if (idx == -1)
            idx = path.lastIndexOf(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(":/images/" + m_image_name + ".png");
            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)
        : QWidget(parent)
    {
        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.append(Face(name));
        }
        m_focus_col = 0;
        m_focus_row = 0;
    }

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

    static QRect fixRect(const QRect &r)
    {
        return QRect(r.x(), r.y(), r.width() - 1, r.height() - 1);
    }

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

        QColor sel_color = palette().color(QPalette::Highlight);
        QPen pen(sel_color, 0);
        p.setPen(pen);
        sel_color.setAlpha(32);
        p.setBrush(sel_color);

        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;
                QRect r(x, y, g_image_w, g_image_h);

                const Face &f = face(col, row);
                if (f.m_image.isNull())
                    p.fillRect(r, palette().brush(QPalette::Background));
                else
                    p.drawImage(r, f.m_image);

                if (col == m_focus_col && row == m_focus_row)
                    p.drawRect(fixRect(r));
            }
        }
    }

    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)
        : QMainWindow(parent)
    {
        setAttribute(Qt::WA_DeleteOnClose, true);
        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;
        addToolBar(Qt::TopToolBarArea, tool_bar);
        tool_bar->addAction(undo_action);
        tool_bar->addAction(redo_action);

        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();
    }

    QtUndoStack *FaceEdit::undoStack() const
    {
        return m_undo_stack;
    }

    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(QIODevice::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;
                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(QIODevice::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';
            }
        }

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

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

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

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


Copyright © 2008 Nokia Corporation and/or its subsidiary(-ies) Trademarks
Qt Solutions