演示如何使用插件扩展 Qt 应用程序。
A plugin is a dynamic library that can be loaded at run-time to extend an application. Qt makes it possible to create custom plugins and to load them using QPluginLoader . To ensure that plugins don't get lost, it is also possible to link them statically to the executable. The Plug & Paint example uses plugins to support custom brushes, shapes, and image filters. A single plugin can provide multiple brushes, shapes, and/or filters.
If you want to learn how to make your own application extensible through plugins, we recommend that you start by reading this overview, which explains how to make an application use plugins. Afterwards, you can read the 基本工具 and Extra Filters overviews, which show how to implement static and dynamic plugins, respectively.
插件和描绘由以下类组成:
MainWindow
是
QMainWindow
subclass that provides the menu system and that contains a
PaintArea
作为中心 Widget。
PaintArea
是
QWidget
that allows the user to draw using a brush and to insert shapes.
PluginDialog
is a dialog that shows information about the plugins detected by the application.
BrushInterface
,
ShapeInterface
,和
FilterInterface
are abstract base classes that can be implemented by plugins to provide custom brushes, shapes, and image filters.
We will start by reviewing the interfaces defined in
interfaces.h
. These interfaces are used by the Plug & Paint application to access extra functionality. They are implemented in the plugins.
class BrushInterface { public: virtual ~BrushInterface() = default; virtual QStringList brushes() const = 0; virtual QRect mousePress(const QString &brush, QPainter &painter, const QPoint &pos) = 0; virtual QRect mouseMove(const QString &brush, QPainter &painter, const QPoint &oldPos, const QPoint &newPos) = 0; virtual QRect mouseRelease(const QString &brush, QPainter &painter, const QPoint &pos) = 0; };
The
BrushInterface
class declares four pure virtual functions. The first pure virtual function,
brushes()
, returns a list of strings that identify the brushes provided by the plugin. By returning a
QStringList
而不是
QString
, we make it possible for a single plugin to provide multiple brushes. The other functions have a
brush
parameter to identify which brush (among those returned by
brushes()
) is used.
mousePress()
,
mouseMove()
,和
mouseRelease()
接受
QPainter
and one or two
QPoint
s, and return a
QRect
identifying which portion of the image was altered by the brush.
The class also has a virtual destructor. Interface classes usually don't need such a destructor (because it would make little sense to
delete
the object that implements the interface through a pointer to the interface), but some compilers emit a warning for classes that declare virtual functions but no virtual destructor. We provide the destructor to keep these compilers happy.
class ShapeInterface { public: virtual ~ShapeInterface() = default; virtual QStringList shapes() const = 0; virtual QPainterPath generateShape(const QString &shape, QWidget *parent) = 0; };
The
ShapeInterface
类声明
shapes()
function that works the same as
BrushInterface
's
brushes()
函数,和
generateShape()
function that has a
shape
parameter. Shapes are represented by a
QPainterPath
, a data type that can represent arbitrary 2D shapes or combinations of shapes. The
parent
parameter can be used by the plugin to pop up a dialog asking the user to specify more information.
class FilterInterface { public: virtual ~FilterInterface() = default; virtual QStringList filters() const = 0; virtual QImage filterImage(const QString &filter, const QImage &image, QWidget *parent) = 0; };
The
FilterInterface
类声明
filters()
function that returns a list of filter names, and a
filterImage()
function that applies a filter to an image.
#define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0" Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid) #define ShapeInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0" Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid) #define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0" Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid)
To make it possible to query at run-time whether a plugin implements a given interface, we must use the
Q_DECLARE_INTERFACE()
macro. The first argument is the name of the interface. The second argument is a string identifying the interface in a unique way. By convention, we use a "Java package name" syntax to identify interfaces. If we later change the interfaces, we must use a different string to identify the new interface; otherwise, the application might crash. It is therefore a good idea to include a version number in the string, as we did above.
The
基本工具
plugin and the
Extra Filters
plugin shows how to derive from
BrushInterface
,
ShapeInterface
,和
FilterInterface
.
A note on naming: It might have been tempting to give the
brushes()
,
shapes()
,和
filters()
functions a more generic name, such as
keys()
or
features()
. However, that would have made multiple inheritance impractical. When creating interfaces, we should always try to give unique names to the pure virtual functions.
The
MainWindow
类是标准
QMainWindow
subclass, as found in many of the other examples (e.g.,
应用程序
). Here, we'll concentrate on the parts of the code that are related to plugins.
void MainWindow::loadPlugins() { const auto staticInstances = QPluginLoader::staticInstances(); for (QObject *plugin : staticInstances) populateMenus(plugin);
The
loadPlugins()
function is called from the
MainWindow
constructor to detect plugins and update the
Brush
,
Shapes
,和
Filters
menus. We start by handling static plugins (available through
QPluginLoader::staticInstances
())
To the application that uses the plugin, a Qt plugin is simply a QObject . That QObject implements plugin interfaces using multiple inheritance.
pluginsDir = QDir(QCoreApplication::applicationDirPath()); #if defined(Q_OS_WIN) if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release") pluginsDir.cdUp(); #elif defined(Q_OS_MAC) if (pluginsDir.dirName() == "MacOS") { pluginsDir.cdUp(); pluginsDir.cdUp(); pluginsDir.cdUp(); } #endif pluginsDir.cd("plugins");
The next step is to load dynamic plugins. We initialize the
pluginsDir
member variable to refer to the
plugins
subdirectory of the Plug & Paint example. On Unix, this is just a matter of initializing the
QDir
variable with
QApplication::applicationDirPath
(), the path of the executable file, and to do a
cd
(). On Windows and macOS, this file is usually located in a subdirectory, so we need to take this into account.
const auto entryList = pluginsDir.entryList(QDir::Files); for (const QString &fileName : entryList) { QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); QObject *plugin = loader.instance(); if (plugin) { populateMenus(plugin); pluginFileNames += fileName; } }
使用 QDir::entryList () to get a list of all files in that directory. Then we iterate over the result using a range-based for loop and try to load the plugin using QPluginLoader .
The QObject provided by the plugin is accessible through QPluginLoader::instance (). If the dynamic library isn't a Qt plugin, or if it was compiled against an incompatible version of the Qt library, QPluginLoader::instance () returns a null pointer.
若 QPluginLoader::instance () is non-null, we add it to the menus.
brushMenu->setEnabled(!brushActionGroup->actions().isEmpty()); shapesMenu->setEnabled(!shapesMenu->actions().isEmpty()); filterMenu->setEnabled(!filterMenu->actions().isEmpty()); }
At the end, we enable or disable the Brush , Shapes ,和 Filters menus based on whether they contain any items.
void MainWindow::populateMenus(QObject *plugin) { auto iBrush = qobject_cast<BrushInterface *>(plugin); if (iBrush) addToMenu(plugin, iBrush->brushes(), brushMenu, &MainWindow::changeBrush, brushActionGroup); auto iShape = qobject_cast<ShapeInterface *>(plugin); if (iShape) addToMenu(plugin, iShape->shapes(), shapesMenu, &MainWindow::insertShape); auto iFilter = qobject_cast<FilterInterface *>(plugin); if (iFilter) addToMenu(plugin, iFilter->filters(), filterMenu, &MainWindow::applyFilter); }
For each plugin (static or dynamic), we check which interfaces it implements using
qobject_cast
(). First, we try to cast the plugin instance to a
BrushInterface
; if it works, we call the private function
addToMenu()
with the list of brushes returned by
brushes()
. Then we do the same with the
ShapeInterface
和
FilterInterface
.
void MainWindow::aboutPlugins() { PluginDialog dialog(pluginsDir.path(), pluginFileNames, this); dialog.exec(); }
The
aboutPlugins()
slot is called on startup and can be invoked at any time through the
About Plugins
action. It pops up a
PluginDialog
, providing information about the loaded plugins.
The
addToMenu()
function is called from
loadPlugin()
to create
QAction
s for custom brushes, shapes, or filters and add them to the relevant menu. The
QAction
is created with the plugin from which it comes from as the parent; this makes it convenient to get access to the plugin later.
void MainWindow::changeBrush() { auto action = qobject_cast<QAction *>(sender()); if (!action) return; auto iBrush = qobject_cast<BrushInterface *>(action->parent()); if (!iBrush) return; const QString brush = action->text(); paintArea->setBrush(iBrush, brush); }
The
changeBrush()
slot is invoked when the user chooses one of the brushes from the
Brush
menu. We start by finding out which action invoked the slot using
QObject::sender
(). Then we get the
BrushInterface
out of the plugin (which we conveniently passed as the
QAction
's parent) and we call
PaintArea::setBrush()
采用
BrushInterface
and the string identifying the brush. Next time the user draws on the paint area,
PaintArea
will use this brush.
void MainWindow::insertShape() { auto action = qobject_cast<QAction *>(sender()); if (!action) return; auto iShape = qobject_cast<ShapeInterface *>(action->parent()); if (!iShape) return; const QPainterPath path = iShape->generateShape(action->text(), this); if (!path.isEmpty()) paintArea->insertShape(path); }
The
insertShape()
is invoked when the use chooses one of the shapes from the
Shapes
menu. We retrieve the
QAction
that invoked the slot, then the
ShapeInterface
关联
QAction
, and finally we call
ShapeInterface::generateShape()
to obtain a
QPainterPath
.
void MainWindow::applyFilter() { auto action = qobject_cast<QAction *>(sender()); if (!action) return; auto iFilter = qobject_cast<FilterInterface *>(action->parent()); if (!iFilter) return; const QImage image = iFilter->filterImage(action->text(), paintArea->image(), this); paintArea->setImage(image); }
The
applyFilter()
slot is similar: We retrieve the
QAction
that invoked the slot, then the
FilterInterface
associated to that
QAction
, and finally we call
FilterInterface::filterImage()
to apply the filter onto the current image.
The
PaintArea
class contains some code that deals with
BrushInterface
, so we'll review it briefly.
void PaintArea::setBrush(BrushInterface *brushInterface, const QString &brush) { this->brushInterface = brushInterface; this->brush = brush; }
在
setBrush()
,只需存储
BrushInterface
and the brush that are given to us by
MainWindow
.
void PaintArea::mouseMoveEvent(QMouseEvent *event) { if ((event->buttons() & Qt::LeftButton) && lastPos != QPoint(-1, -1)) { if (brushInterface) { QPainter painter(&theImage); setupPainter(painter); const QRect rect = brushInterface->mouseMove(brush, painter, lastPos, event->position().toPoint()); update(rect); } lastPos = event->position().toPoint(); } }
在
鼠标移动事件处理程序
,我们调用
BrushInterface::mouseMove()
function on the current
BrushInterface
, with the current brush. The mouse press and mouse release handlers are very similar.
The
PluginDialog
class provides information about the loaded plugins to the user. Its constructor takes a path to the plugins and a list of plugin file names. It calls
findPlugins()
to fill the QTreeWdiget with information about the plugins:
void PluginDialog::findPlugins(const QString &path, const QStringList &fileNames) { label->setText(tr("Plug & Paint found the following plugins\n" "(looked in %1):") .arg(QDir::toNativeSeparators(path))); const QDir dir(path); const auto staticInstances = QPluginLoader::staticInstances(); for (QObject *plugin : staticInstances) populateTreeWidget(plugin, tr("%1 (Static Plugin)") .arg(plugin->metaObject()->className())); for (const QString &fileName : fileNames) { QPluginLoader loader(dir.absoluteFilePath(fileName)); QObject *plugin = loader.instance(); if (plugin) populateTreeWidget(plugin, fileName); } }
The
findPlugins()
非常类似
MainWindow::loadPlugins()
。它使用
QPluginLoader
to access the static and dynamic plugins. Its helper function
populateTreeWidget()
使用
qobject_cast
() to find out which interfaces are implemented by the plugins:
void PluginDialog::populateTreeWidget(QObject *plugin, const QString &text) { auto pluginItem = new QTreeWidgetItem(treeWidget); pluginItem->setText(0, text); pluginItem->setExpanded(true); QFont boldFont = pluginItem->font(0); boldFont.setBold(true); pluginItem->setFont(0, boldFont); if (plugin) { auto iBrush = qobject_cast<BrushInterface *>(plugin); if (iBrush) addItems(pluginItem, "BrushInterface", iBrush->brushes()); auto iShape = qobject_cast<ShapeInterface *>(plugin); if (iShape) addItems(pluginItem, "ShapeInterface", iShape->shapes()); auto iFilter = qobject_cast<FilterInterface *>(plugin); if (iFilter) addItems(pluginItem, "FilterInterface", iFilter->filters()); } }
The
基本工具
plugin is built as a static plugin, to ensure that it is always available to the application. This requires using the
Q_IMPORT_PLUGIN
() macro somewhere in the application (in a
.cpp
file) and specifying the plugin in the
.pro
文件。
For Plug & Paint, we have chosen to put
Q_IMPORT_PLUGIN
() 在
main.cpp
:
#include "mainwindow.h" #include <QApplication> #include <QtPlugin> Q_IMPORT_PLUGIN(BasicToolsPlugin) int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }
The argument to Q_IMPORT_PLUGIN () is the plugin name, which corresponds with the name of the class that declares metadata for the plugin with Q_PLUGIN_METADATA ().
在
.pro
file, we need to specify the static library. Here's the project file for building Plug & Paint:
TARGET = plugandpaint DESTDIR = .. QT += widgets HEADERS = interfaces.h \ mainwindow.h \ paintarea.h \ plugindialog.h SOURCES = main.cpp \ mainwindow.cpp \ paintarea.cpp \ plugindialog.cpp LIBS = -L../plugins macx-xcode { LIBS += -lpnp_basictools$($${QMAKE_XCODE_LIBRARY_SUFFIX_SETTING}) } else { android { LIBS += -lpnp_basictools_$${QT_ARCH} } else { LIBS += -lpnp_basictools } if(!debug_and_release|build_pass):CONFIG(debug, debug|release) { mac:LIBS = $$member(LIBS, 0) $$member(LIBS, 1)_debug win32:LIBS = $$member(LIBS, 0) $$member(LIBS, 1)d } }
The
LIBS
line variable specifies the library
pnp_basictools
located in the
../plugandpaint/plugins/basictools
directory. (Although the
LIBS
syntax has a distinct Unix flavor,
qmake
supports it on all platforms.)
The
CONFIG()
code at the end is necessary for this example because the example is part of the Qt distribution and Qt can be configured to be built simultaneously in debug and in release modes. You don't need to for your own plugin applications.
This completes our review of the Plug & Paint application. At this point, you might want to take a look at the 基本工具 范例插件。