IT Notes

Паттерн Декоратор и создание фильтров изображений в Qt

Декоратор (Decorator) представляет собой немного спорный паттерн. Его почти всегда можно заменить чем-нибудь другим. Например, Компоновщиком или Строителем.

Но все же Декоратор вполне может найти свое место в вашем проекте. Не зря его активно используют при разработке библиотек ввода-вывода. Особенно это заметно в Java.

В основе паттерна Декоратор лежит базовый интерфейс (или абстрактный класс). Для него создаются реализации, выполняющие полезные действия. Декоратор при этом выступает в качестве прозрачной обертки над реализациями, имея тот же самый интерфейс. Он делегирует все вызовы оборачиваемому объекту, но добавляет свои специфические преобразования.

Преимущество от использования Декоратора заключается в том, что он позволяет комбинировать цепочки обернутых объектов произвольной сложности. В результате удается избежать ненужного дублирования кода.

Рассмотрим пример. Создадим небольшое приложение, которое применяет графические фильтры к загруженному изображению. Фильтры могут накладываться, поэтому Декоратор позволит нам динамически создавать комбинированные фильтры, доступные для повторного использования.

Интерфейс графического фильтра

Спроектируем простой интерфейсный класс ImageFilter. Заголовочный файл imagefilter.h:

#ifndef IMAGEFILTER_H
#define IMAGEFILTER_H

#include <QPixmap>

class ImageFilter {
public:
    ImageFilter();
    virtual ~ImageFilter();

    virtual QPixmap apply( const QPixmap& pix ) = 0;

};

#endif // IMAGEFILTER_H

Он имеет всего одну чисто виртуальную функцию apply(), которая принимает на вход QPixmap, и возвращает результат применения фильтра.

Поскольку нам интересны как фильтры по отдельности, так и комбинации фильтров, то каждая реализация станет потенциальным Декоратором. Начнем с фильтра отзеркаливания.

Фильтр отзеркаливания изображения

В Qt довольно легко создать подобный эффект. Объявим класс MirroredImageFilter, наследующий ImageFilter, в mirroredimagefilter.h:

#ifndef MIRROREDIMAGEFILTER_H
#define MIRROREDIMAGEFILTER_H

#include <imagefilter.h>

#include <memory>

class MirroredImageFilter : public ImageFilter {
public:
    explicit MirroredImageFilter( const std::shared_ptr< ImageFilter >& filter = nullptr );

    QPixmap apply( const QPixmap& pix );

private:
    std::shared_ptr< ImageFilter > m_filter;
};

#endif // MIRROREDIMAGEFILTER_H

В качестве параметра конструктор принимает указатель на оборачиваемый фильтр. Если параметр равен nullptr, то класс ничего не декорирует, и выступает в качестве самостоятельной реализации. А вот и она, в файле mirroredimagefilter.cpp:

#include "mirroredimagefilter.h"

MirroredImageFilter::MirroredImageFilter( const std::shared_ptr< ImageFilter >& filter ) :
    m_filter( filter ) {
}

QPixmap MirroredImageFilter::apply( const QPixmap& pix ) {
    QPixmap pixResult = pix;
    if( m_filter ) {
        pixResult = m_filter->apply( pix );
    }

    return QPixmap::fromImage( pixResult.toImage().mirrored( true, false ) );
}

Пробный запуск

Один фильтр уже есть. Проверим его работу. Файл main.cpp:

#include <QApplication>
#include <QFileDialog>
#include <QLabel>

#include "mirroredimagefilter.h"

int main( int argc, char** argv ) {
    QApplication app( argc, argv );
    QString fileName = QFileDialog::getOpenFileName(
        nullptr,
        "Load Image",
        QString(),
        "Images (*.jpg *.png *.bmp)"
    );

    QPixmap pix;
    if( pix.load( fileName ) ) {
        QLabel lblMirrored;
        lblMirrored.setPixmap( MirroredImageFilter().apply( pix ) );
        lblMirrored.show();

        return app.exec();
    }


    return 0;
}

Эта программа уже что-то делает, но пока что Декоратора здесь еще нет. Он появится, когда мы добавим хотя бы еще один фильтр. Займемся этим прямо сейчас.

Фильтр поворота изображения

Пусть второй фильтр-Декоратор умеет осуществлять поворот изображения на указанный угол. Посмотрим на соответствующий заголовочный файл rotatedimagefilter.h:

#ifndef ROTATEDIMAGEFILTER_H
#define ROTATEDIMAGEFILTER_H

#include "imagefilter.h"

#include <memory>

class RotatedImageFilter : public ImageFilter {
public:
    RotatedImageFilter( double angle = 90.0, const std::shared_ptr< ImageFilter >& filter = nullptr );

    QPixmap apply( const QPixmap& pix );

private:
    double m_angle;
    std::shared_ptr< ImageFilter > m_filter;
};

#endif // ROTATEDIMAGEFILTER_H

А вот и реализация в rotatedimagefilter.cpp:

#include "rotatedimagefilter.h"

RotatedImageFilter::RotatedImageFilter( double angle, const std::shared_ptr< ImageFilter >& filter ) :
    m_angle( angle ), m_filter( filter ) {
}

QPixmap RotatedImageFilter::apply( const QPixmap& pix ) {
    QPixmap pixResult = pix;
    if( m_filter ) {
        pixResult = m_filter->apply( pix );
    }

    return pixResult.transformed( QTransform().rotate( m_angle ) );
}

Все вместе

Теперь у нас есть два фильтра. Каждый из них можно использовать отдельно, а можно скомбинировать в цепочку. Попробуем все это на практике (файл main.cpp):

#include <QApplication>
#include <QFileDialog>
#include <QLabel>

#include "mirroredimagefilter.h"
#include "rotatedimagefilter.h"

int main( int argc, char** argv ) {
    QApplication app( argc, argv );
    QString fileName = QFileDialog::getOpenFileName(
        nullptr,
        "Load Image",
        QString(),
        "Images (*.jpg *.png *.bmp)"
    );

    QPixmap pix;
    if( pix.load( fileName ) ) {
        QLabel lblMirrored;
        lblMirrored.setPixmap( MirroredImageFilter().apply( pix ) );
        lblMirrored.show();

        QLabel lblRotated;
        lblRotated.setPixmap( RotatedImageFilter().apply( pix ) );
        lblRotated.show();

        QLabel lblCombined;
        lblCombined.setPixmap(
            RotatedImageFilter( 45.0, std::make_shared< MirroredImageFilter >() ).apply( pix )
        );
        lblCombined.show();

        return app.exec();
    }


    return 0;
}

В результате запуска программы на экране появятся три QLabel: с отзеркаленным изображением, с повернутым на 90 градусов изображением и с изображением, которое было сначала отзеркалено, а потом повернуто на 45 градусов:

image-filter-decorator-sample

Выводы

Поскольку мы создали всего два фильтра, то существенную выгоду от использования паттерна Декоратор увидеть сложно. Но с ростом числа реализаций количество их возможных комбинаций будет возрастать очень быстро. В этом случае Декоратор в сочетании со Стратегией или каким-нибудь другим полиморфным подходом окажется весьма привлекательным решением.

Исходники

 Скачать исходники с примером использования паттерна Декоратор в Qt

Похожие публикации