Mastering Qt 5 学习笔记-dynamically

Mastering Qt 5 学习笔记-dynamically

本项目由两个子项目组成:

filter-plugin-designer:这是一个包含 FilterWidget 类和图像处理代码的 Qt Designer 插件。 这个插件是一个动态库,Qt Creator 将使用它在表单编辑器中提供我们新的 FilterWidget。
image-filter:这是一个使用多个FilterWidget的Qt Widget应用程序。 用户可以从硬盘打开图像,选择过滤器(灰度、模糊等),然后保存过滤后的图像。
我们的过滤器插件设计器将使用第三方库 OpenCV(开源计算机视觉)。 它是一个功能强大的跨平台开源库,用于处理图像。 这是一个概述架构:

Mastering Qt 5 学习笔记-dynamically

您可以将插件视为一种模块,它可以轻松添加到现有软件中。 插件必须遵守特定的接口才能被应用程序自动调用。 在我们的例子中,Qt Designer 是加载 Qt 插件的应用程序。 因此,创建插件可以让我们增强应用程序,而无需修改 Qt Designer 源代码并重新编译它。 插件通常是一个动态库 (.dll/.so),因此它会在运行时由应用程序加载。

现在您对 Qt Designer 插件有了清晰的认识,让我们构建一个! 首先,创建一个名为 ch07-image-filter 的 Subdirs 项目。 然后,您可以添加一个子项目,filter-plugin-designer。 您可以使用 Empty qmake 项目模板,因为我们从头开始这个项目。 这是 filter-plugin-designer.pro 文件:

QT += widgets uiplugin 
CONFIG += plugin 
CONFIG += c++14 
TEMPLATE = lib 
DEFINES += FILTERPLUGINDESIGNER_LIBRARY 
 
TARGET = $$qtLibraryTarget($$TARGET) 
INSTALLS += target 

请注意 QT 和 CONFIG 的 uiplugin 和 plugin 关键字。他们需要创建 Qt Designer 插件。我们将 TEMPLATE 关键字设置为 lib,因为我们正在创建一个动态库。定义 FILTERPLUGINDESIGNER_LIBRARY 将由库的导入/导出机制使用。

默认情况下,我们的 TARGET 是 filter-plugin-designer; $$qtLibraryTarget() 函数将根据您的平台更新它。例如,在 Windows 上将附加后缀“d”(代表调试)。最后,我们将目标附加到 INSTALLS。现在,这条线什么都不做,但我们很快就会描述每个平台的目的地路径;这样,执行 make install 命令会将我们的目标库文件 (.dll/.so) 复制到正确的文件夹中。要在每次编译时自动执行此任务,您可以添加一个新的构建步骤。

现在您的开发环境已准备就绪,我们可以开始有趣的部分了! 我们将使用 OpenCV 实现三个过滤器:

FilterOriginal:这个过滤器什么都不做并返回相同的图片
FilterGrayscale:此过滤器将图片从彩色转换为灰度
FilterBlur:此过滤器使图片平滑

Filter.h

#ifndef FILTER_H
#define FILTER_H

#include <QImage>
//提供图片过滤的接口函数
//所有这些过滤器的父类都是过滤器。 这是这个抽象类
class Filter
{
public:
    Filter();
    virtual ~Filter();    
    virtual QImage process(const QImage& image) = 0;
};
#endif // FILTER_H

process() 是一个纯抽象方法。 所有过滤器都将使用此功能实现特定行为。 让我们从简单的 FilterOriginal 类开始。 这是 FilterOriginal.h:

#ifndef FILTERORIGINAL_H
#define FILTERORIGINAL_H

#include "Filter.h"

class FilterOriginal : public Filter
{
public:
    FilterOriginal();
    ~FilterOriginal();

    QImage process(const QImage& image) override;
};


#endif // FILTERORIGINAL_H

这个类继承了 Filter 并且我们覆盖了 process() 函数。 实现也非常简单。 使用以下内容填充 FilterOriginal.cpp:

#include "FilterOriginal.h"

FilterOriginal::FilterOriginal() :
    Filter()
{
}

FilterOriginal::~FilterOriginal()
{
}
//不进行任何修改
QImage FilterOriginal::process(const QImage& image)
{
    return image;
}

FilterGrayscale.cpp

#include "FilterGrayscale.h"

#include <opencv2/opencv.hpp>

FilterGrayscale::FilterGrayscale() :
    Filter()
{
}

FilterGrayscale::~FilterGrayscale() {}

QImage FilterGrayscale::process(const QImage& image)
{
    // QImage => cv::mat
    cv::Mat tmp(image.height(),
                image.width(),
                CV_8UC4,
                (uchar*)image.bits(),
                image.bytesPerLine());

    cv::Mat resultMat;
    cv::cvtColor(tmp, resultMat, cv::COLOR_BGR2GRAY);
    // cv::mat => QImage
    QImage resultImage((const uchar *) resultMat.data,
                       resultMat.cols,
                       resultMat.rows,
                       resultMat.step,
                       QImage::Format_Grayscale8);
    return resultImage.copy();
}

在Qt框架中,我们使用QImage类来操作图片。 在 OpenCV 世界中,我们使用 Mat 类,所以第一步是从 QImage 源创建一个正确的 Mat 对象。 OpenCV 和 Qt 都处理许多图像格式。

通道数:灰度图片只需要一个通道(白色强度),而彩色图片需要三个通道(红、绿、蓝)。 您甚至需要四个通道来处理不透明度 (alpha) 像素信息。
位深度:用于存储像素颜色的位数。
通道顺序:最常见的顺序是RGB和BGR。 Alpha 可以放置在颜色信息之前或之后。
例如,OpenCV 图像格式 CV_8UC4 表示无符号 8 位的四个通道,非常适合 alpha 彩色图片。 在我们的例子中,我们使用兼容的 Qt 和 OpenCV 图像格式在 Mat 中转换我们的 QImage。

Mastering Qt 5 学习笔记-dynamically

请注意,某些 QImage 类格式还取决于您的平台字节序。 上表适用于小端系统。 对于 OpenCV,顺序始终相同:BGRA。 在我们的项目示例中不是必需的,但您可以按如下方式交换蓝色和红色通道:

// with OpenCV 
cv::cvtColor(mat, mat, CV_BGR2RGB); 

// with Qt 
QImage swapped = image.rgbSwapped(); 

OpenCV Mat 和 Qt QImage 类默认执行浅层构造/复制。 这意味着只有元数据被真正复制; 像素数据是共享的。 要创建图片的深层副本,您必须调用 copy() 函数:

// with OpenCV 
mat.clone(); 
 
// with Qt 
image.copy();

我们从 QImage 类创建了一个名为 tmp 的 Mat 类。 请注意,tmp 不是图像的深层副本; 它们共享相同的数据指针。 然后,我们可以调用 OpenCV 函数使用 cv::cvtColor() 将图片从彩色转换为灰度。 最后,我们从灰度 resultMat 元素创建一个 QImage 类。 在这种情况下, resultMat 和 resultImage 也共享相同的数据指针。 完成后,我们返回 resultImage 的深层副本。

FilterBlur.cpp

#include "FilterBlur.h"

#include <opencv2/opencv.hpp>

FilterBlur::FilterBlur() :
    Filter()
{
}

FilterBlur::~FilterBlur() {}

QImage FilterBlur::process(const QImage& image)
{
    // QImage => cv::mat
    cv::Mat tmp(image.height(),
                image.width(),
                CV_8UC4,
                (uchar*)image.bits(),
                image.bytesPerLine());

    int blur = 17;
    cv::Mat resultMat;
    cv::GaussianBlur(tmp,
                     resultMat,
                     cv::Size(blur, blur),
                     0.0,
                     0.0);

    // cv::mat => QImage
    QImage resultImage((const uchar *) resultMat.data,
                       resultMat.cols,
                       resultMat.rows,
                       resultMat.step,
                       QImage::Format_RGB32);
    return resultImage.copy();
}

从 QImage 到 Mat 的转换是一样的。 处理不同,因为我们使用 cv::GaussianBlur() OpenCV 函数来平滑图片。 模糊是高斯模糊使用的内核大小。 您可以增加此值以获得更柔和的图片,但只能使用奇数和正数。 最后,我们将 Mat 转换为 QImage 并将深度副本返回给调用者。

使用 FilterWidget 设计 UI

我们的过滤器类已经实现,我们现在可以创建我们的自定义窗口。这个窗将接收输入、源和缩略图。然后立即处理缩略图以显示过滤器的预览。如果用户点击小部件,它将处理源图片并用过滤后的图片触发信号。此窗口稍后将被拖放到 Qt Creator 的表单编辑器中。这就是为什么我们将提供带有 getter 和 setter 的属性以从 Qt Creator 中选择过滤器。

Mastering Qt 5 学习笔记-dynamically

titleLabel 是 QWidget 之上的 QLabel。 下面,thumbnailLabel 将显示过滤后的图片缩略图。

#ifndef FILTERWIDGET_H
#define FILTERWIDGET_H

#include <QWidget>
#include <QImage>
#include <memory>

#include "Filter.h"
#include "filter-plugin-designer_global.h"

namespace Ui {
class FilterWidget;
}

class FILTERPLUGINDESIGNERSHARED_EXPORT FilterWidget : public QWidget
{
    Q_OBJECT
	//公开枚举需要使用 Q_ENUM() 宏进行注册,因此属性编辑器将显示一个组合框,允许您从 Qt Creator 中选择过滤器类型
    Q_ENUMS(FilterType)
    //我们还使用 Qtproperty 系统将窗口标题和当前过滤器类型公开给 Qt Creator 的属性编辑器
    Q_PROPERTY(QString title READ title WRITE setTitle)
    Q_PROPERTY(FilterType filterType READ filterType WRITE setFilterType)

public:
    //顶部使用 enumFilterType 定义所有可用的过滤器类型
    enum FilterType { Original, Blur, Grayscale };

    explicit FilterWidget(QWidget *parent = 0);
    ~FilterWidget();
	//process() 函数,它将使用当前过滤器来修改源图片
    void process();

    void setSourcePicture(const QImage& sourcePicture);
    void updateThumbnail(const QImage& sourceThumbnail);

    QString title() const;
    FilterType filterType() const;

public slots:
    void setTitle(const QString& tile);
    void setFilterType(FilterType filterType);

signals:
    //pictureProcessed() 信号将用过滤后的图片通知应用程序
    void pictureProcessed(const QImage& picture);

protected:
    void mousePressEvent(QMouseEvent*) override;

private:
    Ui::FilterWidget *ui;
    std::unique_ptr<Filter> mFilter;
    FilterType mFilterType;
    
	//底部列出了该类中使用的图片和缩略图 QImage 变量
    QImage mDefaultSourcePicture;
    QImage mSourcePicture;
    QImage mSourceThumbnail;

    QImage mFilteredPicture;
    QImage mFilteredThumbnail;
};

#endif // FILTERWIDGET_H

FilterWidget.cpp

#include "FilterWidget.h"
#include "ui_FilterWidget.h"

#include "FilterBlur.h"
#include "FilterGrayscale.h"
#include "FilterOriginal.h"

using namespace std;
//这里是构造函数和析构函数
FilterWidget::FilterWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FilterWidget),
    mFilterType(Original),
	//默认源图片加载了图像处理文献中经常使用Lenna的嵌入图片
	//图片在资源文件filter-plugin-designer.qrc中
    mDefaultSourcePicture(":/lenna.jpg"),
    mSourcePicture(),
	//mSourceThumbnail函数使用Lenna的缩放图片进行初始化
    mSourceThumbnail(mDefaultSourcePicture.scaled(QSize(256, 256),
                     Qt::KeepAspectRatio,
                     Qt::SmoothTransformation)),
    mFilteredPicture(),
    mFilteredThumbnail()
{
    ui->setupUi(this);
    //构造函数调用setFilterType()函数来初始化原始过滤器
    setFilterType(Original);
}

FilterWidget::~FilterWidget()
{
    delete ui;
}
//我们调用当前过滤器的process()以从当前源图片更新我们过滤后的图片
void FilterWidget::process()
{
    mFilteredPicture = mFilter->process(mSourcePicture);
    //然后我们用过滤后的图片触发 pictureProcessed() 信号
    emit pictureProcessed(mFilteredPicture);
}
//setSourcePicture()函数是一个简单的setter
//由应用程序使用新的源图片调用
void FilterWidget::setSourcePicture(const QImage& sourcePicture)
{
    mSourcePicture = sourcePicture;
}
//updateThumbnail()方法将过滤新的源缩略图并显示它
void FilterWidget::updateThumbnail(const QImage& sourceThumbnail)
{
    mSourceThumbnail = sourceThumbnail;
    mFilteredThumbnail = mFilter->process(mSourceThumbnail);
    QPixmap pixmap = QPixmap::fromImage(mFilteredThumbnail);
    ui->thumbnailLabel->setPixmap(pixmap);
}

void FilterWidget::setTitle(const QString& tile)
{
    ui->titleLabel->setText(tile);
}

void FilterWidget::setFilterType(FilterWidget::FilterType filterType)
{
    if (filterType == mFilterType && mFilter) {
        return;
    }
    mFilterType = filterType;

    switch (filterType) {
        case Original:
            mFilter = make_unique<FilterOriginal>();
            break;

        case Blur:
            mFilter = make_unique<FilterBlur>();
            break;

        case Grayscale:
            mFilter = make_unique<FilterGrayscale>();
            break;

        default:
            break;
    }

    updateThumbnail(mSourceThumbnail);
}

QString FilterWidget::title() const
{
    return ui->titleLabel->text();
}

FilterWidget::FilterType FilterWidget::filterType() const
{
    return mFilterType;
}

void FilterWidget::mousePressEvent(QMouseEvent*)
{
    process();
}

FilterWidget 类已完成 我们现在必须向 Qt Designer 插件系统注册 FilterWidget。 此粘合代码是使用 QDesignerCustomWidgetInterface 的子类制作的。

FilterPluginDesigner.h

#ifndef FILTERPLUGINDESIGNER_H
#define FILTERPLUGINDESIGNER_H

#include <QtUiPlugin/QDesignerCustomWidgetInterface>
//FilterPlugin 类继承自两个类:
//QDesignerCustomWidgetInterface 类将 FilterWidget 信息正确暴露给插件系统
class  FilterPluginDesigner : public QObject, public QDesignerCustomWidgetInterface
{
    //QDesignerCustomWidgetInterface 类有两个新的宏:
    Q_OBJECT
   	//Q_PLUGIN_METADATA() 宏对类进行注释
    //以向元对象系统指示过滤器的唯一名称
    Q_PLUGIN_METADATA(IID "org.masteringqt.imagefilter.FilterWidgetPluginInterface")
    //Q_INTERFACES() 宏告诉元对象系统当前类已经实现了哪个接口
    Q_INTERFACES(QDesignerCustomWidgetInterface)
public:
    FilterPluginDesigner(QObject* parent = 0);
	//Qt Designer 现在能够检测我们的插件。 我们现在必须提供有关插件本身的信息
    QString name() const override;
    QString group() const override;
    QString toolTip() const override;
    QString whatsThis() const override;
    QString includeFile() const override;
    QIcon icon() const override;
    bool isContainer() const override;
    QWidget* createWidget(QWidget* parent) override;
    bool isInitialized() const override;
    void initialize(QDesignerFormEditorInterface* core) override;

private:
    bool mInitialized;
};

#endif // FILTERPLUGINDESIGNER_H

FilterPluginDesigner.cpp

#include "FilterPluginDesigner.h"
#include "FilterWidget.h"
FilterPluginDesigner::FilterPluginDesigner(QObject* parent) :
    QObject(parent),
    mInitialized(false)
{
}
//它们中的大多数只会返回一个QString值,该值将显示在Qt设计器UI的适当位置。
QString FilterPluginDesigner::name() const
{
    return "FilterWidget";
}

 

QString FilterPluginDesigner::toolTip() const
{
    return "A filtered picture";
}

QString FilterPluginDesigner::whatsThis() const
{
    return "The filter widget applies an image effect";
}

//函数将被 uic(用户界面编译器)调用以生成对应于 .ui 文件的头文件
//这个函数在 Qt Designer 和 FilterWidget 之间架起了桥梁
//当您在 .ui 文件中添加 FilterWidget 类时
QString FilterPluginDesigner::includeFile() const
{
    return "FilterWidget.h";
}

QIcon FilterPluginDesigner::icon() const
{
    return QIcon(":/icon.jpg");
}

bool FilterPluginDesigner::isContainer() const
{
    return false;
}
//当您在.ui文件中添加FilterWidget类时QtDesigner将调用createWidget()函数以获得FilterWidget类的实例并显示其内容
QWidget* FilterPluginDesigner::createWidget(QWidget* parent)
{
    return new FilterWidget(parent);
}

bool FilterPluginDesigner::isInitialized() const
{
    return mInitialized;
}
//这个函数没有做太多事情
//QDesignerFormEditorInterface* 参数值得一些解释
//这个指针由 Qt Designer 提供,可以通过函数访问一些 Qt Designer 的组件
void FilterPluginDesigner::initialize(QDesignerFormEditorInterface* /*core*/)
{
    if (mInitialized)
        return;

    mInitialized = true;
}

actionEditor():这个函数是actionEditor(设计器的底部面板)

formWindowManager():这个函数是让你创建一个新的窗体窗口的接口

objectInspector():这个函数是你的布局的分层表示(设计器的右上角面板)

propertyEditor():这个函数是当前选中的widget的所有可编辑属性的列表(设计器的右下面板)

topLevel():这个函数是设计器的顶层widget

Mastering Qt 5 学习笔记-dynamically

所在的组

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

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

namespace Ui {
class MainWindow;
}

class FilterWidget;
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    //当用户单击 actionOpenPicture 时将调用它
    void loadPicture();
protected:
    void resizeEvent(QResizeEvent* event) override;
private slots:
    //这个函数是mCurrentWidget::pictureProcessed()调用的槽,用来显示过滤后的图片。
    void displayPicture(const QImage& picture);
    void saveAsPicture();
private:
    //这个函数负责初始化mFilters
    void initFilters();
	//这个函数处理pictureLabel里面mCurrentPixmap的显示
    void updatePicturePixmap();
private:
    Ui::MainWindow *ui;
    //该元素是加载的图片
    //为避免浪费 CPU 周期,mSourcePicture 将仅调整一次大小
    QImage mSourcePicture;
    //该元素是从 mSourcePicture 生成的缩略图
    QImage mSourceThumbnail;
    //并且每个FilterWidget实例将处理此缩略图而不是全分辨率图片
    QImage& mFilteredPicture;
    //该元素是pictureLabel 小部件中当前显示的QPixmap
    QPixmap mCurrentPixmap;
    //这个元素是当前应用的过滤器
    //每次用户单击不同的 FilterWidget 时,该指针都会更新
    FilterWidget* mCurrentFilter;
    //这个元素是我们添加到 MainWindow.ui 的 FilterWidget 类的 QVector
    QVector<FilterWidget*> mFilters;
};

#endif // MAINWINDOW_H

MainWindow.cpp

#include "MainWindow.h"
#include "ui_MainWindow.h"

#include <QFileDialog>
#include <QPixmap>
#include <QDir>

#include "FilterWidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    mSourcePicture(),
    mSourceThumbnail(),
    mFilteredPicture(mSourcePicture),
    mCurrentPixmap(),
    mCurrentFilter(nullptr),
    mFilters()
{
    ui->setupUi(this);
    ui->actionSaveAs->setEnabled(false);
    ui->pictureLabel->setMinimumSize(1, 1);
    connect(ui->actionOpenPicture, &QAction::triggered,
            this, &MainWindow::loadPicture);
    connect(ui->actionSaveAs, &QAction::triggered,
            this, &MainWindow::saveAsPicture);
    connect(ui->actionExit, &QAction::triggered,
            this, &QMainWindow::close);
    initFilters();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::initFilters()
{
    mFilters.push_back(ui->filterWidgetOriginal);
    mFilters.push_back(ui->filterWidgetBlur);
    mFilters.push_back(ui->filterWidgetGrayscale);

    for (int i = 0; i < mFilters.size(); ++i) {
        connect(mFilters[i], &FilterWidget::pictureProcessed,
                this, &MainWindow::displayPicture);
    }
    mCurrentFilter = mFilters[0];
}

void MainWindow::loadPicture()
{
    //mSourcePicture 方法是使用 QFileDialog 加载的
    QString filename = QFileDialog::getOpenFileName(this,
                                                    "Open Picture",
                                                    QDir::homePath(),
                                                    tr("Images (*.png *.jpg)"));
    if (filename.isEmpty()) {
        return;
    }

    ui->actionSaveAs->setEnabled(true);
    mSourcePicture = QImage(filename);
    //并且 mSourceThumbnail 是从这个输入生成的
    mSourceThumbnail = mSourcePicture.scaled(QSize(256, 256),
                                             Qt::KeepAspectRatio,
                                             Qt::SmoothTransformation);
    for (int i = 0; i <mFilters.size(); ++i) {
        mFilters[i]->setSourcePicture(mSourcePicture);
        mFilters[i]->updateThumbnail(mSourceThumbnail);
    }
	//且 mCurrentFilter 元素通过调用它的 process() 函数来触发
    mCurrentFilter->process();
}

void MainWindow::resizeEvent(QResizeEvent* /*event*/)
{
    updatePicturePixmap();
}

void MainWindow::displayPicture(const QImage& picture)
{
    mFilteredPicture = picture;
    mCurrentPixmap = QPixmap::fromImage(picture);
    updatePicturePixmap();
}

void MainWindow::saveAsPicture()
{
    QString filename = QFileDialog::getSaveFileName(this,
                                                    "Save Picture",
                                                    QDir::homePath(),
                                                    tr("Images (*.png *.jpg)"));
    if (filename.isEmpty()) {
        return;
    }
    mFilteredPicture.save(filename);
}

void MainWindow::updatePicturePixmap()
{
    if (mCurrentPixmap.isNull()) {
        return;
    }
    ui->pictureLabel->setPixmap(
                mCurrentPixmap.scaled(ui->pictureLabel->size(),
                                      Qt::KeepAspectRatio,
                                      Qt::SmoothTransformation));
}

上一篇:c – 将QVideoFrame转换为QImage


下一篇:QT QImage