本项目由两个子项目组成:
filter-plugin-designer:这是一个包含 FilterWidget 类和图像处理代码的 Qt Designer 插件。 这个插件是一个动态库,Qt Creator 将使用它在表单编辑器中提供我们新的 FilterWidget。
image-filter:这是一个使用多个FilterWidget的Qt Widget应用程序。 用户可以从硬盘打开图像,选择过滤器(灰度、模糊等),然后保存过滤后的图像。
我们的过滤器插件设计器将使用第三方库 OpenCV(开源计算机视觉)。 它是一个功能强大的跨平台开源库,用于处理图像。 这是一个概述架构:
您可以将插件视为一种模块,它可以轻松添加到现有软件中。 插件必须遵守特定的接口才能被应用程序自动调用。 在我们的例子中,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。
请注意,某些 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 中选择过滤器。
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
所在的组
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));
}