Жасмин Бланшет - QT 4: программирование GUI на С++
В конце мы используем макрос Q_DECLARE_INTERFACE() для назначения некоторого идентификатора интерфейсу. Этот идентификатор обычно имеет четыре компонента: инвертированное имя домена, определяющее создателя интерфейса, имя приложения, имя интерфейса и номер версии. При любом изменении интерфейса (например, при добавлении новой виртуальной функции или при изменении сигнатуры существующей функции) мы должны не забыть увеличить номер версии; в противном случае приложение может завершиться аварийно при попытке получения доступа к старой версии подключаемого модуля.
Это приложение реализуется в виде класса TextArtDialog. Мы будем показывать только тот программный код, который связан с применением подключаемых модулей. Давайте начнем с конструктора:
01 TextArtDialog::TextArtDialog(const QString &text, QWidget *parent)
02 : QDialog(parent)
03 {
04 listWidget = new QListWidget;
05 listWidget->setViewMode(QListWidget::IconMode);
06 listWidget->setMovement(QListWidget::Static);
07 listWidget->setIconSize(QSize(260, 80));
08 …
09 loadPlugins();
10 populateListWidget(text);
11 …
12 }
Конструктор создает виджет QListWidget, содержащий список доступных эффектов. Он вызывает закрытую функцию loadPlugins() для поиска и загрузки всех подключаемых модулей, реализующих интерфейс TextArtInterface, и заполняет список виджетов с помощью вызова другой закрытой функции — populateListWidget().
01 void TextArtDialog::loadPlugins()
02 {
03 QDir pluginDir(QApplication::applicationDirPath());
04 #if defined(Q_OS_WIN)
05 if (pluginDir.dirName().toLower() == "debug"
06 || pluginDir.dirName().toLower() == "release")
07 pluginDir.cdUp();
08 #elif defined(Q_OS_MAC)
09 if (pluginDir.dirName() == "MacOS") {
10 pluginDir.cdUp();
11 pluginDir.cdUp();
12 pluginDir.cdUp();
13 }
14 #endif
15 if (!pluginDir.cd("plugins"))
16 return;
17 foreach (QString fileName, pluginDir.entryList(QDir::Files)) {
18 QPluginLoader loader(pluginDir.absoluteFilePath(fileName));
19 if (TextArtInterface *interface =
20 qobject_cast<TextArtInterface *>(loader.instance()))
21 interfaces.append(interface);
22 }
23 }
В функции loadPlugins() мы пытаемся загрузить все файлы, находящиеся в каталоге приложения plugins. (В Windows исполняемый модуль приложения обычно находится в подкаталоге debug или release, поэтому поднимаемся на один каталог выше. В Mac OS X учитываем структуру группового каталога (bundle directory).)
Если файл, который мы пытаемся загрузить, является подключаемым модулем Qt и имеет ту же саму версию Qt, какую имеет приложение, функция QPluginLoader::instance() возвратит указатель QObject *, ссылающийся на подключаемый модуль Qt. Используем qobject_cast<T>() для проверки реализации в подключаемом модуле интерфейса TextArtInterface. При всяком успешном приведении типа мы добавляем интерфейс к списку интерфейсов приложения TextArtDialog (который имеет тип QList<TextArtInterface *>).
Для некоторых приложений может потребоваться загрузка двух или более различных интерфейсов, и в этом случае программный код по получении этих интерфейсов мог бы выглядеть следующим образом:
01 QObject *plugin = loader.instance();
02 if (TextArtInterface *i = qobject_cast<TextArtInterface *>(plugin))
03 textArtInterfaces.append(i);
04 if (BorderArtInterface *i = qobject_cast<BorderArtInterface *>(plugin))
05 borderArtInterfaces.append(i);
06 if (TextureInterface *i = qobject_cast<TextureInterface *>(plugin))
07 textureInterfaces.append(i);
Тип одного подключаемого модуля может успешно приводиться к нескольким указателям интерфейсов, поскольку подключаемые модули могут обеспечивать несколько интерфейсов, используя множественное наследование.
01 void TextArtDialog::populateListWidget(const QString &text)
02 {
03 QSize iconSize = listWidget->iconSize();
04 QPen pen(QColor("darkseagreen"));
05 QLinearGradient gradient(0, 0, iconSize.width() / 2,
06 iconSize.height() / 2);
07 gradient.setColorAt(0.0. QColor("darkolivegreen"));
08 gradient.setColorAt(0.8, QColor("darkgreen"));
09 gradient.setColorAt(1.0, QColor("lightgreen"));
10 QFont font("Helvetica", iconSize.height(), QFont::Bold);
11 foreach (TextArtInterface *interface, interfaces) {
12 foreach (QString effect, interface->effects()) {
13 QListWidgetItem *item = new QListWidgetItem(
14 effect, listWidget);
15 QPixmap pixmap = interface->applyEffect(effect,
16 text, font, iconSize, pen, gradient);
17 item->setData(Qt::DecorationRole, pixmap);
18 }
19 }
20 listWidget->setCurrentRow(0);
21 }
Функция populateListWidget() начинается с создания некоторых переменных, передаваемых функции applyEffect(), в частности пера, линейного градиента и шрифта. Затем она просматривает в цикле все интерфейсы TextArtInterface, найденные функцией loadPlugins(). Для любого эффекта, обеспечиваемого каждым интерфейсом, создается новый элемент QListWidgetItem, текст которого определяет название создаваемого им эффекта, и создается QPixmap, используя applyEffect().
В данном разделе мы увидели, как можно загружать подключаемые модули, вызывая в конструкторе функцию loadPlugins(), и как можно их использовать в функции populateListWidget(). Программный код элегантно обрабатывает ситуации, когда подключаемые модули вообще не обеспечивают интерфейс TextArtInterface или когда только один из них или несколько обеспечивают такой интерфейс. Более того, другие подключаемые модули могут добавляться позже. При каждом запуске приложения производится загрузка всех подключаемых модулей, имеющих нужный интерфейс. Это позволяет легко расширять функциональность приложения без изменения самого приложения.
Написание подключаемых к приложению модулей
Подключаемый к приложению модуль является подклассом QObject и интерфейсов, которые он собирается обеспечить. Прилагаемый к этой книге компакт-диск содержит два подключаемых модуля, предназначенных для приложения Text Art, представленного в предыдущем разделе, и показывающих, что это приложение правильно работает с несколькими подключаемыми модулями.
Здесь мы рассмотрим программный код только одного из них — Basic Effects Plugin (модуль основных эффектов). Предполагаем, что исходный код подключаемого модуля находится в каталоге basiceffectsplugin и что приложение Text Art находится в параллельном каталоге textart. Ниже приводится объявление класса подключаемого модуля:
01 class BasicEffectsPlugin
02 : public QObject, public TextArtInterface
03 {
04 Q_OBJECT
05 Q_INTERFACES(TextArtInterface)
06 public:
07 QStringList effects() const;
08 QPixmap applyEffect(const QString &effect, const QString &text,
09 const QFont &font, const QSize &size,
10 const QPen &pen, const QBrush &brush);
11 };
Этот подключаемый модуль реализует только один интерфейс — TextArtInterface. Кроме Q_OBJECT необходимо использовать макрос Q_INTERFACES() для каждого интерфейса, для которого создается подкласс, чтобы обеспечить безболезненное восприятие компилятором moc оператора приведения типа qobject_cast<T>().
01 QStringList BasicEffectsPlugin::effects() const
02 {
03 return QStringList() << "Plain" << "Outline" << "Shadow";
04 }
Функция effects() возвращает список текстовых эффектов, поддерживаемых подключаемым модулем. Этот подключаемый модуль обеспечивает три эффекта, поэтому возвращаем список, содержащий имена каждого из них.
Функция applyEffect() обеспечивает функциональность подключаемого модуля и слегка запутанна, поэтому рассмотрим ее по частям:
01 QPixmap BasicEffectsPlugin::applyEffect(const QString &effect,
02 const QString &text, const QFont &font, const QSize &size,
03 const QPen &pen, const QBrush &brush)
04 {
05 QFont myFont = font;
06 QFontMetrics metrics(myFont);
07 while ((metrics.width(text) > size.width() ||
08 metrics.height() > size.height())
09 && myFont.pointSize() > 9) {
10 myFont.setPointSize(myFont.pointSize() - 1);
11 metrics = QFontMetrics(myFont);
12 }
Мы хотим обеспечить по мере возможности достаточность указанного размера для размещения заданного текста. По этой причине используем метрики шрифта и, если текст оказывается слишком большим, входим в цикл, где уменьшаем размер, пока он не окажется подходящим или не достигнет 9 точек, что соответствует нашему минимальному размеру.
13 QPixmap pixmap(size);
14 QPainter painter(&pixmap);
15 painter.setFont(myFont);