FullStack developer: Qt/C++, Yii2/PHP
20,1
рейтинг
15 августа 2012 в 10:10

Разработка → Система расширений Qt Creator tutorial

Qt*

Предисловие


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

Введение


Это заметка, в которой я хотел бы немного описать архитектуру и систему расширений среды разработки Qt Creator. Изначально, я предполагал лишь перевести документ Writing-Qt-Cretor-plugins.pdf, но так уж вышло, что развитие Qt Creator не стоит на месте и во первых, данный документ уже не столь актуален (сам устарел, API поменялось, куски кода не полные и часто не работают), а во вторых со времени его написания появлись дополнительные возможности расширения Qt Creator, которые хотелось бы описать.

Тем не менее, не будь этого документа, не было бы и данной заметки: из него я взял очень много, вплоть до структуры поста, при этом постаравшись где-то что-то выкидывая/заменяя/добавляя сделать пост актуальным для последней на текущий момент времени версии Qt Creator 2.4.0.

Кому может быть полезен данный документ? В первую очередь это конечно же Qt-программисты, которые выбрали данную IDE как основную среду разработки.

Кроме того, благодаря продуманной системе расширений Qt Creator, данный материал будет полезен тем, кто собрался создавать собственные средства разработки, но не хотят начинать писать их с чистого листа: человек может отключить все ненужные ему расширения Qt Creator и написать свои собственные, пользуясь при этом готовыми примерами в исходниках Qt Creator.

Итак, что же нас ожидает под катом (жирным отмечены готовые разделы):

  1. Сборка Qt Creator
  2. Первое расширение
  3. Добавление новых меню и пунктов меню
  4. Архитектура Qt Creator
  5. Добавление нового редактора (Editor)
  6. Добавление боковой навигационной панели
  7. Добавление страницы в диалог настроек
  8. Добавление фильтра в диалог поиска
  9. Добавление нового типа проекта

Напомню, что Qt Creator является кросс-платформенной свободной IDE для работы с фреймворком Qt, разработанной Trolltech (Nokia). Что не мешает сделать из него простой текстовый редактор с подсветкой синтаксиса, простым отключением всех расширений. Внимание, сотни картинок!

1. Сборка Qt Creator


Сборка всей IDE является достаточно простым действием. Первым делом нам необходимо скачать исходники последней версии Qt Creator. на данный момент это версия 2.4. Скачиваем файл с сайта qt.nokia.com из раздела Downloads/Qt-Creator:
image
Далее, распаковываем полученный архив, создаём в каталоге исходников подкаталог build, переходим в него, запускаем qmake и затем make:
$ tar -xvf qt-creator-2.4.0-src.tar.gz
$ mkdir qt-creator-2.4.0-src/build
$ cd qt-creator-2.4.0-src/build/
$ qmake ../qtcreator.pro –recursive
$ make

Для пользовтелей Windows данный код может отличаться только последней строкой — вместо make нужно будет вызвать mingw32-make или nmake, в зависимости от предпочтений пользователя.

Вот и всё. Вы можете запустить Qt Creator из каталога build/bin.
image
Следует заметить, что это очень важный этап, так как если Вы не соберёте Qt Creator из исходников, Вы не сможете продвинуться дальше и компилировать и тестировать расширения для него.

2. Первое расширение


Как и во многих случаях, изучение системы расширений Qt Creator-а стоит начать с создания очень простого расширения. Сейчас мы попробуем сделать расширение, которое ничего не делает, но на примере нашего DoNothing-расширения мы узнаем о базовых классах Qt Creator, относящихся к написанию расширений и увидим строчку «DoNothing» в списке доступных расширений.
image
image

2.1 Создание проекта расширения Qt Creator

Ранее, кажется вплоть до версии 2.0, для выполнения данного шага, требовалось вручную создать следующие файлы:
  • DoNothingPlugin.pro
  • DoNothingPlugin.h
  • DoNothingPlugin.cpp
  • DoNothingPlugin.pluginspec

Также, тогда следовало либо складывать проект расширения в каталог $$QT_CREATOR_ROOT/src/plugins, либо размещая его в другом месте, необходимо в .pro файле указывать каталоги исходников и сборки Qt Creator. В современных же версиях Qt Creator файлы проекта можно размещать где угодно, так как для его создания появился новый тип мастер — «Модуль Qt Creator». Процесс создания нового расширения в данный момент выглядит следующим образом/
В самом начале, всё как обычно — выбираем тип создаваемого проекта,
image
после вводим название и путь до каталога его размещения
image
а также цель сборки, естественно это Desktop.
image
А вот после этого уже начинается немножко магии — нам нужно заполнить специфичную для модуля информацию:
image
Здесь в общем всё более чем понятно из картинки. Единственные жизненно важные поля здесь — путь к каталогам исходников/сборки Qt Creator. Завершаем работу мастера:
image
И смотрим на получившуюся структуру проекта:
image

2.2 Служебные файлы

На первый взгляд здесь очень много файлов и каталогов, но на самом деле в этом нет ничего страшного. Разберёмся с увиденным выше и начнём с файла проекта:
TARGET = DoNothing
TEMPLATE = lib

DEFINES += DONOTHING_LIBRARY

# DoNothing files

SOURCES += donothingplugin.cpp

HEADERS += donothingplugin.h\
        donothing_global.h\
        donothingconstants.h

OTHER_FILES = DoNothing.pluginspec

# Qt Creator linking

## set the QTC_SOURCE environment variable to override the setting here
QTCREATOR_SOURCES = $$(QTC_SOURCE)
isEmpty(QTCREATOR_SOURCES):QTCREATOR_SOURCES=/home/kafeg/devel/Qt/qt-creator-2.4.0-src

## set the QTC_BUILD environment variable to override the setting here
IDE_BUILD_TREE = $$(QTC_BUILD)
isEmpty(IDE_BUILD_TREE):IDE_BUILD_TREE=/home/kafeg/devel/Qt/qt-creator-2.4.0-src/build

## uncomment to build plugin into user config directory
## <localappdata>/plugins/<ideversion>
##    where <localappdata> is e.g.
##    "%LOCALAPPDATA%\Nokia\qtcreator" on Windows Vista and later
##    "$XDG_DATA_HOME/Nokia/qtcreator" or "~/.local/share/Nokia/qtcreator" on Linux
##    "~/Library/Application Support/Nokia/Qt Creator" on Mac
# USE_USER_DESTDIR = yes

PROVIDER = DoNothingCompany

include($$QTCREATOR_SOURCES/src/qtcreatorplugin.pri)
include($$QTCREATOR_SOURCES/src/plugins/coreplugin/coreplugin.pri)

LIBS += -L$$IDE_PLUGIN_PATH/Nokia

Как видим, в данном сгенерированном файле описывается имя (DoNothing) и тип (библиотека) проекта, указывается три файла заголовков и один файл исходного кода, упоминается pluginspec. Указывается размещение исходников Qt Creator, закомментированные инструкции позволяющие устанавливать собранную библиотеку не в каталог Qt Creator, а в локальную директорию пользователя, описан провайдер, от которого зависит итоговое расположение файлов библиотеки. И наконец включаются базовые для всех расширений файлы qtcreatorplugin.pri и coreplugin.pri, которые уже отвечают за правильную линковку нашего расширения со всеми необходимыми библиотеками.

Следующие файлы donothing_global.h:
#ifndef DONOTHING_GLOBAL_H
#define DONOTHING_GLOBAL_H

#include <QtCore/QtGlobal>

#if defined(DONOTHING_LIBRARY)
#  define DONOTHINGSHARED_EXPORT Q_DECL_EXPORT
#else
#  define DONOTHINGSHARED_EXPORT Q_DECL_IMPORT
#endif

#endif // DONOTHING_GLOBAL_H

и donothingconstants.h:
#ifndef DONOTHINGCONSTANTS_H
#define DONOTHINGCONSTANTS_H

namespace DoNothing {
namespace Constants {

const char * const ACTION_ID = "DoNothing.Action";
const char * const MENU_ID = "DoNothing.Menu";

} // namespace DoNothing
} // namespace Constants

#endif // DONOTHINGCONSTANTS_H

Здесь думаю всё ясно без дополнительных объяснений. А что не понятно — станет понятно, когда этот код будет необходим нам в дальнейшем. Список служебных файлов можно завершить файлом DoNothing.pluginspec.in:
<plugin name=\"DoNothing\" version=\"0.0.1\" compatVersion=\"0.0.1\">
    <vendor>DoNothingCompany</vendor>
    <copyright>(C) DoNothing Company</copyright>
    <license>DoNothing Company license text here</license>
    <description>DoNothing Company short description of plugin here</description>
    <url>http://www.donothing.com</url>
    <dependencyList>
        <dependency name=\"Core\" version=\"$$QTCREATOR_VERSION\"/>
    </dependencyList>
</plugin>

Данный файл будет в итоге поставляться вместе с бинарным файлом библиотеки и является простым описанием нашего расширения. И вот что он описывает:
  1. Наименование расширения, которое будет использоваться в названии библиотеки, реализующей его. (В нашем случае DoNothing.dll в Windows и libDoNothing.so в Unix)
  2. Версия расширения
  3. Требуемая расширением версия Qt Creator
  4. Наименование вендора
  5. Copyright
  6. Текст лицензии
  7. Описание
  8. URL вендора
  9. Список расширений от которых зависит данное расширение.

2.3 Реализация расширения

Для успешной компиляции пустого проекта, необходимы два основных файла реализации нашего расширения. Что же они из себя представляют? Главное требование — основной класс расширения должен быть унаследован от базового класса IPlugin и переопределять некоторые его методы.
donothingplugin.h
#ifndef DONOTHING_H
#define DONOTHING_H

#include "donothing_global.h"

#include <extensionsystem/iplugin.h>

namespace DoNothing {
namespace Internal {

class DoNothingPlugin : public ExtensionSystem::IPlugin
{
    Q_OBJECT
    
public:
    DoNothingPlugin();
    ~DoNothingPlugin();
    
    bool initialize(const QStringList &arguments, QString *errorString);
    void extensionsInitialized();
    ShutdownFlag aboutToShutdown();
    
private slots:
    void triggerAction();
};

} // namespace Internal
} // namespace DoNothing

#endif // DONOTHING_H

Рассмотрим файл реализации данного кода более подробно:
#include "donothingplugin.h"
#include "donothingconstants.h"

#include <coreplugin/icore.h>
#include <coreplugin/icontext.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/coreconstants.h>

#include <QtGui/QAction>
#include <QtGui/QMessageBox>
#include <QtGui/QMainWindow>
#include <QtGui/QMenu>

#include <QtCore/QtPlugin>

using namespace DoNothing::Internal;

DoNothingPlugin::DoNothingPlugin()
{
    // Create your members
}

DoNothingPlugin::~DoNothingPlugin()
{
    // Unregister objects from the plugin manager's object pool
    // Delete members
}

Конструктор и деструктор используются лишь для инциализации базовых переменных, не являющихся виджетами и/или действиями (Action).
bool DoNothingPlugin::initialize(const QStringList &arguments, QString *errorString)
{
    // Register objects in the plugin manager's object pool
    // Load settings
    // Add actions to menus
    // connect to other plugins' signals
    // "In the initialize method, a plugin can be sure that the plugins it
    //  depends on have initialized their members."
    
    Q_UNUSED(arguments)
    Q_UNUSED(errorString)
    Core::ActionManager *am = Core::ICore::instance()->actionManager();
    
    QAction *action = new QAction(tr("DoNothing action"), this);
    Core::Command *cmd = am->registerAction(action, Constants::ACTION_ID,
                                            Core::Context(Core::Constants::C_GLOBAL));
    cmd->setDefaultKeySequence(QKeySequence(tr("Ctrl+Alt+Meta+A")));
    connect(action, SIGNAL(triggered()), this, SLOT(triggerAction()));
    
    Core::ActionContainer *menu = am->createMenu(Constants::MENU_ID);
    menu->menu()->setTitle(tr("DoNothing"));
    menu->addAction(cmd);
    am->actionContainer(Core::Constants::M_TOOLS)->addMenu(menu);
    
    return true;
}

Функция initialize() вызывается в тот момент, когда Qt Creator решит, что пора инициализировать расширение. Данная функция предназначена для инициализации начального состояния и регистрации всех действий и объектов относящихся к расширению в самом Qt Creator.

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

Всё что нам необходимо знать сейчас — при удачном завершении инициализации данная функция должна вернуть true, при неудачном — вернуть false и в переменную errorString записать человеческим языком сообщение об ошибке.
void DoNothingPlugin::extensionsInitialized()
{
    // Retrieve objects from the plugin manager's object pool
    // "In the extensionsInitialized method, a plugin can be sure that all
    //  plugins that depend on it are completely initialized."
}

Метод extensionsInitialized() вызывается после окончания инициализации. Служит главным образом помощником тем расширениям, которые зависят от текущего.
ExtensionSystem::IPlugin::ShutdownFlag DoNothingPlugin::aboutToShutdown()
{
    // Save settings
    // Disconnect from signals that are not needed during shutdown
    // Hide UI (if you add UI that is not in the main window directly)
    return SynchronousShutdown;
}

Метод aboutToShutdown() вызывается перед тем, как расширение будет выгружено из памяти.
void DoNothingPlugin::triggerAction()
{
    QMessageBox::information(Core::ICore::instance()->mainWindow(),
                             tr("Action triggered"),
                             tr("This is an action from DoNothing."));
}

Q_EXPORT_PLUGIN2(DoNothing, DoNothingPlugin)

2.4 Сборка и тестирование расширения

Для сборки нашего расширения достаточно нажать сочетание клавиш Ctrl+R, после чего расширение будет собрано и установлено в каталоге расширений Qt Ceator, но не запущено, так как Qt Creator не в курсе, как ему запустить данную библиотеку. Исправить это положение можно на странице настройки запуска приложений (Проекты -> Запуск -> Конфигурация запуска -> Программа):
image
Итогом станет запущенный Qt Creator, в списке расширений которого можно увидеть новую строчку:
image
А в каталоге всех расширений появился новый каталог с парой файлов:
kafeg@kafeg-desktop:~/devel/Qt/qt-creator-2.4.0-src/build/lib/qtcreator/plugins/DoNothingCompany$ ls
DoNothing.pluginspec  libDoNothing.so

Вот таким образом мы можем добавить самое элементарное расширение Qt Creator. Идём дальше.

3 Добавление новых меню и пунктов меню


В этой части мы научимся добавлять новые пунткы меню в существующие меню, а также поймём, как создавать свои собственные меню. Но для начала глянем на содержимое панелименю Qt Creator:
image
панель содержит следующие элементы по умолчанию:
  • Файл
    -Создать
    -Открыть
    -Недавние файлы
  • Правка
    -Дополнительно
  • Инструменты
  • Окно
    -Панели вывода
  • Справка

Все остальные пункты меню, к примеру Отладка, Сборка и Анализ являются реализованы в отдельных расширениях и не являются частью набора меню по умолчанию.

Qt-разработчики знают, что меню сами меню реализуются комбинацией классов QMenu и QAction, а их отображением в виде панели занимается класс QMenuBar.

3.1 Core::ActionManager

Базовая часть Qt Creator, это по сути лишь пустое окошко, умеющее загружать расширения. Вся функциональность, предоставляемая Qt Creator реализуется через его расширения. Главное расширение Qt Creator именуется как «core». Без этого расширения Qt Creator не представляет из себя вообще ничего.

Один из главных компонентов расширения «core» — это ActionManager. ActionManager — это объект, отвечающий за регистрацию всех меню, пунктов меню и клавиатурных сочетаний. Собственно, если мы хотим добавить новый пункт меню — мы должны использовать объект ActionManager. Чуть ниже, мы разберёмся как…
Чтобы получить доступ к объекту ActionManager, наш код должен содержать следующее:
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/icore.h>
...
Core::ActionManager* am = Core::ICore::instance()->actionManager();

3.2 Core::ActionContainer

ActionContianer предоставляет меню и панели меню в Qt Creator. Экземпляры данного класса никогда не создаются напрямую, доступ к ним осуществляется через методы ActionManager::createMenu(), ActionManager::createMenuBar() и другие.

Существуют экземпляры ActionContainer, ассоциированные со всеми меню по умолчанию. Для получения экземпляра ActionContainer необходимо использовать подобный код:
#include <coreplugin/coreconstants.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/icore.h>
Core::ActionManager* am = Core::ICore::instance()->actionManager();
Core::ActionContainer* ac = am->actionContainer( ID );

Ниже показана таблица доступных по умолчанию ID, позволяющих получать экземпляры ActionContainer. ID — это переменные типа const char* static в пространстве видимости Core.
Меню ID
File Core::Constants::M_FILE
File -> New Core::Constants::M_FILE_NEW
File -> Open Core::Constants::M_FILE_OPEN
File -> Recent Files Core::Constants::M_FILE_RECENTFILES
Edit Core::Constants::M_EDIT
Edit -> Advanced Core::Constants::M_EDIT_ADVANCED
Tools Core::Constants::M_TOOLS
Window Core::Constants::M_WINDOW
Window Panes Core::Constants::M_WINDOW_PANES
Help Core::Constants::M_HELP

К примеру, если мы хотим получить указатель на меню «Help», то мы должны использовать следующий код:
#include <coreplugin/coreconstants.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/icore.h>
...
Core::ActionManager* am = Core::ICore::instance()->actionManager();
Core::ActionContainer* ac = am->actionContainer( Core::Constants::M_HELP );

3.3 Добавление меню и пунктов меню

Давайте посмотрим, как мы можем добавлять новые пункты меню. Для этого вернёмся к нашей существующей функции initialize() и более подробно глянем на её реализацию.
bool DoNothingPlugin::initialize(const QStringList &arguments, QString *errorString)
{
    Q_UNUSED(arguments)
    Q_UNUSED(errorString)
    Core::ActionManager *am = Core::ICore::instance()->actionManager();
    
    QAction *action = new QAction(tr("DoNothing action"), this);
    Core::Command *cmd = am->registerAction(action, Constants::ACTION_ID,
                                            Core::Context(Core::Constants::C_GLOBAL));
    cmd->setDefaultKeySequence(QKeySequence(tr("Ctrl+Alt+Meta+A")));
    connect(action, SIGNAL(triggered()), this, SLOT(triggerAction()));
    
    Core::ActionContainer *menu = am->createMenu(Constants::MENU_ID);
    menu->menu()->setTitle(tr("DoNothing"));
    menu->addAction(cmd);
    am->actionContainer(Core::Constants::M_TOOLS)->addMenu(menu);
    
    return true;
}

Код показанный здесь теперь становится намного более понятным. Итак, мы получаем указатель на ActionManager, затем создаём новый пункт меню и назначаем ему сочетание клавиш. После чего создаём собственное меню и в него добавляем созданный только что пункт. Стоит также заметить что вот теперь нам и пригодились константы ACTION_ID и MENU_ID.

Далее мы получаем указатель на ActionContainer меню инструментов (M_TOOLS) и добавляем в него наше меню.
Помимо этого, наше сочетание клавиш было внесено в реестр всех сочетаний клавиш приложения и доступно для изменения в его настройках:
image
Следует также заметить, что свои меню можно добавлять куда угодно. К примеру, если нам было бы необходимо добавить наше меню на главную панель меню, то вместо константы M_TOOLS мы использовали бы константу MENU_BAR. А чтобы спозиционировать наше меню внутри другого меню, следовало бы передать дополнительный параметр функции addMenu()
am->actionContainer(Core::Constants::MENU_BAR)->addMenu(am->actionContainer(Core::Constants::M_HELP), menu);

3.3 Реагирование на события пунктов меню

Так, как пункты меню являются экземплярами класса QAction, то мы можем использовать сигналы посылаемые этими объектами, такими, как triggered(bool) или toggled(bool) и реагировать на них создавая слоты. Пример опять таки можно взять из нашего кода выше:
...
connect(action, SIGNAL(triggered()), this, SLOT(triggerAction()));
...
void DoNothingPlugin::triggerAction()
{
    QMessageBox::information(Core::ICore::instance()->mainWindow(),
                             tr("Action triggered"),
                             tr("This is an action from DoNothing."));
}

image

4 Архитектура Qt Creator


Как правило, каждая достаточно большая система имеет свою архитектуру, поняв которую, мы сможем продвигаться дальше куда быстрее. Qt Creator прост и в этой части мы попробуем разобраться в его базовой архитектуре, после чего продолжим разбираться с написанием расширений.

4.1 Ядро Qt Creator

Ядро Qt Creator — это просто менеджер расширений. Вся функциональность предоставляется через них.
image


Базовый функционал Qt Creator реализован в расширении Сore (Core::ICore). Мы уже немного коснулись этого расширения в предыдущей части. Далее, мы будем именовать это базовое расширение как Core. Менеджер расширений (ExtensionSystem::PluginManager) предоставляет простые возможности по взаимодействию расширений, через хуки, которые одни расширения могут предоставлять другим.

4.2 Что такое расширение?

На системном уровне, расширение — это разделяемая библиотека (DLL в Windows, SO в Linux, DYLIB в Mac). С точки зрения разработчика, расширение — это модуль, который:
  1. Наследует и реализует интерфейс ExtensionSystem::IPlugin. Далее по тексту будем звать подобный класс “Plugin Class”.
  2. Экспортирует Plugin Class, используя макрос Q_EXPORT_PLUGIN.
  3. Обеспечивает наличие файла (Plugin Name).pluginspec содержащий метаданные о расширении.
  4. Предоставляет один или более объектов, в использовании которых могут быть заинтересованы другие расширения.
  5. Проверяет доступность одного или более объектов предоставляемых другими расширениями.

Мы уже познакомились с тремя первыми пунктами, но нигде ещё не останавливались на двух последних.

4.2.1 Как получить спиcок доступных объектов?

Доступные объекты хранятся в пуле объектов внутри PluginManager. Метод allObjects() из PluginManager возвращает весь пул объектов как список указателей на QObject. Ниже показан код, с помощью которого мы могли бы вывести список доступных объектов на печать:
#include <extensionsystem/pluginmanager.h>
ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance();
QList<QObject*> objects = pm->allObjects();
QListWidget* listWidget = new QListWidget;
Q_FOREACH(QObject* obj, objects)
{
    QString objInfo = QString("%1 (%2)")
    .arg(obj->objectName())
    .arg(obj->metaObject()->className());
    listWidget->addItem(objInfo);
}
listWidget->show();

Таким образом, после выполнения данного участка кода, мы увидим длинный список доступных нам объектов:
image

Из имён в списке можно сделать вывод, что объекты предоставляются различными расширениями. Думаю теперь мы можем дать более точное определение термину «предоставляемый объект»:

Предоставляемый (экспортируемый) объект — это экземпляр класса QObject (или его потомков) предоставляемый одним расширением и доступный другим расширениям через пул объектов.

4.2.2 Как экспортировать свой объект в список доступных?

Для этой цели существует три пути:
  • IPlugin::addAutoReleasedObject(QObject*)
  • IPlugin::addObject(QObject*)
  • PluginManager::addObject(QObject*)

Методы IPlugin::addObject() и IPlugin::addAutoReleasedObject() по существу вызывают метод PluginManager::addObject().
Методы IPlugin созданы просто ради удобства. Расширениям рекомендуется использовать именно их для добавления объектов. Различия между методами addAutoReleasedObject() и addObject() в том, что объекты добавленные через первый метод буду автоматически уничтожены и удалены из пула доступных объектов (в обратном добавлению порядке) во время уничтожения расширения. Кроме того, в любое время можно вызвать метод IPlugin::removeObject(QObject*) для ручного удаления объекта из пула доступных.

4.2.3 Какие объекты экспортировать?

Расширения могут предоставлять (экспортировать) любые объекты, но обычно экспортируемые объекты структурируются по функциональному признаку. Другие функциональные возможности Qt Creator предоставляются через экспортируемые интерфейсы. Вот некоторые из них:
  • Core::INavigationWidgetFactory
  • Core::IEditor
  • Core::IOptionsPage
  • Core::IOutputPane
  • Core::IWizard

C++ разработчики привычны, к тому, что интерфейсами обычно называют классы, все методы которых являются чисто виртуальными и публичными. В Qt Creator интерфейсы — это потомки QObject которые имеют один или несколько чисто виртуальных методов.

Если расширение имеет объекты, реализующие какой-либо интерфейс, то такой объект должен быть экспортирован. Для примера, если расширение экспортирует реализацию интерфейса INavigationWidgetFactory, то Core автоматически подхватит виджет созданный в этой реализации для показа его на панели навигации. Как пример рассмотрим добавление на панель навигации простого QTableWidget через реализацию интерфейса Core::INavigationWidgetFactory.
#include <coreplugin/inavigationwidgetfactory.h>
#include <QTableWidget>

class NavWidgetFactory : public Core::INavigationWidgetFactory
{
public:
    NavWidgetFactory() { }
    ~NavWidgetFactory() { }
    Core::NavigationView createWidget()
    {
        Core::NavigationView view;
        view.widget = new QTableWidget(50, 3);
        return view;
    }
    QString displayName() const
    {
        return "Spreadsheet";
    }
    int priority() const
    {
        return 0;
    }

    QString id() const
    {
        return "Spreadsheet";
    }
};

bool DoNothingPlugin::initialize(const QStringList &arguments, QString *errorString)
{
    ...

    // Provide a navigation widget factory.
    // Qt Creator’s navigation widget will automatically
    // hook to our INavigationWidgetFactory implementation, which
    // is the NavWidgetFactory class, and show the QTableWidget
    // created by it in the navigation panel.
    addAutoReleasedObject(new NavWidgetFactory);

    return true;
}

А вот и результат:
image

4.2.4 Уведомления о вновь экспортируемых объектах.

Когда вызывается метод PluginManager::addObject(), PluginManager посылает сигнал objectAdded(QObject*). Этот сигнал можно использовать для отслеживания того, какие объекты были вновь добавлены во время работы Qt Creator.

Очевидно, что расширение может обрабатывать данный сигнал только после соединения его с каким-либо из своих слотов, а следовательно, он будет получать уведомления только лишь об объектах, добавленных после выполнения его полной инициализации.

Обычно, слот соединённый с данным сигналом следит за одним или несколькими интерфейсами. К примеру, так может выглядеть код, следящий за появлением нового объекта, реализующего интерфейс INavigationWidgetFactory.
void Plugin::slotObjectAdded(QObject * obj)
{
    INavigationWidgetFactory *factory
            = Aggregation::query<INavigationWidgetFactory>(obj);
    if(factory)
    {
        // use it here...
    }
}

4.2.4 Поиск объектов

Иногда расширению может потребоваться найти в приложении объект, предоставляющий определённую функциональность. Сейчас мы знаем два способа для поиска таких объектов:
  • Метод PluginManager::allObjects(), возвращающий список всех доступных объектов.
  • Подключиться к сигналу PluginManager::objectAdded(), который позволяет получить уведомление о вновь добавляемых объектах.

Используя эти методы, разработчик может следить за объектами. Но как быть с поиском объектов? Для этих целей существует метод PluginManager::getObjects(). К примеру, если мы хотели бы найти все объекты, реализующие интерфейс INavigationWidgetFactory, то мы бы использовали такой код:
ExtensionSystem::PluginManager* pm = ExtensionSystem::PluginManager::instance();
QList<Core::INavigationWidgetFactory*> objects = pm->getObjects<Core::INavigationWidgetFactory>();

4.3 Аггрегация объектов

При помощи Аггрегации мы можем собирать несколько разрозненных QObject в один объект. Обычно, в Qt Creator этот подход используется для объединения нескольких интерфейсов
#include <aggregation/aggregate.h>
class Interface1 : public QObject
{
    Q_OBJECT
public:
    Interface1() { }
    ~Interface1() { }
};
class Interface2 : public QObject
{
    Q_OBJECT
public:
    Interface2() { }
    ~Interface2() { }
};
Aggregation::Aggregate bundle;
bundle.add(new Interface1);
bundle.add(new Interface2);

Теперь объект bundle содержит в себе указатели на два объекта. Для доступа к интерфейсам мы можем использовать такой код:
Interface1* iface1Ptr = Aggregation::query<Interface1>( &bundle );
Interface2* iface2Ptr = Aggregation::query<Interface2>( &bundle );

Можно включать несколько интерфейсов в один бандл:
Aggregation::Aggregate bundle;
bundle.add(new Interface1);
bundle.add(new Interface2);
bundle.add(new Interface1);
bundle.add(new Interface1);
QList<Interface1*> iface1Ptrs = Aggregation::query_all<Interface1>( &bundle );

А также удалять как добавленные интерфейсы, так и сам бандл:
Aggregation::Aggregate* bundle = new Aggregation::Aggregate;
bundle->add(new Interface1);
bundle->add(new Interface2);
Interface1* iface1Ptr = Aggregation::query<Interface1>(bundle);
delete iface1Ptr;
// deletes the bundle and all objects in it
// same as delete bundle

Сейчас может быть не понятно, зачем вообще здесь нужно упоминание об объектах Aggregation::Aggregate, но в следующих частях он нам ещё пригодится для упрощения реализации и структурирования описываемых примеров.

5 Добавление нового редактора (Editor)


На базовом уровне (тот самый Core Plugin), Qt Creator — это не более чем текстовый редактор. Однако, при наличии соответствующих расширений, его возможности существенно расширяются: в текстовом редакторе появляются подсветка синтаксиса для различных языков, средства рефакторинга и навигации по C++ коду и многое другое. Появляются средства для редактирования UI (Qt Designer), JS/QML, файлов ресурсов QRC, файлов проектов (PRO/PRI), а также шестнадцатеричный редактор для редактирования файлов типа EXE/DLL/SO.

В этой части мы разберёмся, как создавать редакторы для собственных форматов файлов на примере формата HTML. Когда мы закончим с редактором, мы загрузим HTML-файл в редактор и посмотрим что получилось.

5.1 Базовые классы и интерфейсы

Для добавления поддержки нового редактора, нам необходимо:
  • Реализовать класс Core::IPlugin, который экспортирует реализацию интерфейса Core::IEditorFactory. Этому мы научились ранее.
  • Реализовать интерфейс Core::IEditorFactory. Этот интерфейс позволит создавать необходимый редактор в зависимости от переданного ему mime-типа.
  • Реализовать сам редактор файла, через реализацию интерфейса Core::IEditor. Эта реализация будет предоставлять виджет, который позволяет редактировать файлы в зависимости от их типа (к примеру: HTML, ODF и другие). редактор должен иметь доступ к файлу, который в текущий момент редактируется/отображается.
  • Реализовать интерфейс Core::IFile, который поможет в работе над загрузкой/сохранением файлов на жестком диске.

Сейчас мы рассмотрим каждый из указанных интерфейсов.

5.1.1 Интерфейс Core::IFile.

Этот интерфейс является абстрактной прослойкой между работой с файлами и пользовательским интерфейсом. Он предоставляет чисто виртуальные методы для загрузки/сохранения файлов, задания имени файла, также помогает приложению определить mime-тип файла и получать значения определённых флагов (к примеру «modified» и «read-only»). этот интерфейс объявлен в файле src/plugins/coreplugin/ifile.h:

Возникает вопрос — зачем нужен IFile, если уже существует QFile? Вот ответ:
  • IFile загружает данные прямиком в Core::IEditor, в отличие от QFile, который загружает файл в QByteArray
  • IFile умеет высылать сигнал modified(), когда пользователь начинает редактировать файл в редакторе, но при этом содержимое остаётся неизменным. QFile высылает bytesWritten() лишь после того, как содержимое диска было изменено.
  • IFile следит за тем, не был ли изменён файл на диске другой программой и если да — предлагает его перезагрузить. QFile подобная функциональность ни к чему.

5.1.2 Интерфейс Core::IEditor.

Реализации этого интерфейса предоставляют редакторы для различных типов файлов. Объявлен в файле src/plugins/coreplugin/editormanager/ieditor.h

Core::IEditor в первую очередь предоставляет доступ к слудеющему:
  • Виджет редактирования (метод Core::IEditor::widget()), который Qt Creator использует для показа содержимого редактируемого файла.
  • Метод Core::IEditor::file(), являющийся указателем на Core::IFile, который Qt Creator может использовать для работы с диском.
  • Опциональная панель инструментов, которую Qt Creator может показывать когда редактор становится активным.
  • Текущая позиция курсора в файле (Core::IEditor::currentLine() и Core::IEditor::currentColumn()).
  • Имя, которое отображается в списке открытых файлов,

Из скриншота ниже видно, где что находится.
image

5.1.3 Интерфейс Core::IEditorFactory

Реализации этого интерфейса показывают предоставляют методы для создания экземпляров Core::IEditor для поддерживаемых mime-типов. Объявление в src/plugins/coreplugin/editormanager/ieditorfactory.h.

Метод IEditorFactory::mimeType() возвращает mime-тип поддерживаемый редактором. Метод IEditorFactory::createEditor() создаёт конкретный редактор.

5.1.4 Класс Core::MimeDatabase

Класс Core::MimeDatabase хранит все поддерживаемые в Qt Creator mime-типы. Также он помогает получить mime-тип определённого файла. Пример:
#include <coreplugin/mimedatabase.h>
Core::ICore* core = Core::ICore::instance();
Core::MimeDatabase* mdb = core->mimeDatabase();
Core::MimeType type1 = mdb->findByFile( QFileInfo("C:/Temp/sample.html") );
qDebug("File Type for sample.html = %s", qPrintable(type1.type()));
Core::MimeType type2 = mdb->findByFile( QFileInfo("C:/Temp/TextEdit/Main.cpp") );
qDebug("File Type for Main.cpp = %s", qPrintable(type2.type()));
Core::MimeType type3 = mdb->findByFile( QFileInfo("C:/Temp/TextEdit/TextEdit.pro") );
qDebug("File Type for TextEdit.pro = %s", qPrintable(type3.type()));

И вывод:
File Type for sample.html = text/plain
File Type for Main.cpp = text/x-c++src
File Type for TextEdit.pro = text/plain

Для работы Core::MimeDatabase использует суффикс файла, паттерны и другие магические действия, позволяющие как-то получить mime-тип файла. Давайте рассмотрим процедуру открытия файла по его mime-типу на примере:
  1. Пользователь открывает файл (Файл -> Открыть).
  2. Qt Creator использует Core::MimeDatabase для определения mime-type выбранного файла.
  3. Qt Creator просматривает все Core::IEditorFactory на предмет наличия у них необходимого ему mime-типа.
  4. Qt Creator просит найденный Core::IEditorFactory создать экземпляр Core::IEditor.
  5. Виджет, возвращаемый Core::IEditor::widget() отображается в рабочей области.
  6. Вызывается метод Core::IEditor::open() с именем файла, выбранным на первом шаге.

5.1.5 Добавление нового mime-типа

Если мы хотим добавить поддержку нового редактора, мы должны зарегистрировать mime-тип, поддерживаемый нашим новым редактором в Core::MimeDatabase. Для этого есть несколько путей, сейчас мы рассмотрим самый простой, через создание XML-файла. К примеру, мы хотим зарегистрировать text/html mime-тип и ассоциировать его с файлами *.html. Мы создаём XML файл и сохраняем его под именем text-html-mimetype.xml:
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
<mime-type type="text/html">
<sub-class-of type="text/plain"/>
<comment>HTML File</comment>
<glob pattern="*.html"/>
</mime-type>
</mime-info>

Теперь мы можем зарегистрировать данный mime-тип через метод Core::MimeDatabase::addMimeTypes():
Core::ICore* core = Core::ICore::instance();
Core::MimeDatabase* mdb = core->mimeDatabase();
QString errMsg;
bool success = mdb->addMimeTypes("text-html-mimetype.xml", errMsg);

Как только мы это сделали — Qt Creator тут же стал ассоциировать все *.html файлы с mime-типом text/html.

5.2 Расширяем Qt Creator редактором HTML.

Что же, немного остановившись на теории, мы можем приступить к созданию редактора, поддерживающего просмотр/изменение HTML-файлов. Для реализации задуманного нам понадобится создать несколько классов:
Класс базовый класс Описание
HtmlEditorWidget QTabWidget Это будет виджет с двумя табами, на первом показывается итоговый результат, а на втором исходник документа.
HtmlFile Core::IFile Реализация интерфейса IFile для HtmlEditorWidget.
HtmlEditor Core::IEditor Реализует IEditor для управления HtmlEditorWidget и его взаимодействием с HtmlFile.
HtmlEditorFactory Core::IEditorFactory Реализует IEditorFactoryдля создания экземпляров IEditor на основе mime-типа “text/html”.
HtmlEditorPlugin Core::IPlugin Реализует IPlugin для связи всего вышеперечисленного с Qt Creator.

Итак, создаём новый проект модуля Qt Creator, называем его к примеру HTMLEditor и читаем далее.

5.2.1 Реализация HTML Editor Widget

По умолчанию, Qt Creator использует простой текстовый редактор для просмотра HTML-файлов. Мы же делаем двух-табовый редактор, на одном табе которого можно просматривать страничку, а на другом — редактировать её.
#ifndef HTMLEDITORWIDGET_H
#define HTMLEDITORWIDGET_H

#include <QTabWidget>
#include <QWebView>
#include <QPlainTextEdit>

struct HtmlEditorWidgetData {
    QWebView *webView;
    QPlainTextEdit *textEdit;
    bool modified;
    QString path;
};

class HtmlEditorWidget : public QTabWidget
{
    Q_OBJECT
public:
    HtmlEditorWidget(QWidget* parent = 0);
    ~HtmlEditorWidget();
    void setContent(const QByteArray& ba, const QString& path=QString());
    QByteArray content() const;
    QString title() const;
protected slots:
    void slotCurrentTabChanged(int tab);
    void slotContentModified();
signals:
    void contentModified();
    void titleChanged(const QString&);
private:
    HtmlEditorWidgetData* d;
};

#endif // HTMLEDITORWIDGET_H

Конструктор занимается созданием двух виджетов QWebView и QPlainTextEdit, затем он добавляет их как вкладки на текущий виджет и создаёт три коннекта:
  1. Когда пользователь переходит от редактирования исходника к просмотру, содержимое QWeBView должно быть обновлено
  2. Когда пользователь изменяет содержимое, то необходимо выслать сигнал
  3. И наконец нужно следить за изменением заголовка QWebView.

#include "htmleditorwidget.h"
HtmlEditorWidget::HtmlEditorWidget(QWidget* parent)
    :QTabWidget(parent)
{
    d = new HtmlEditorWidgetData;
    d->webView = new QWebView;
    d->textEdit = new QPlainTextEdit;
    addTab(d->webView, "Preview");
    addTab(d->textEdit, "Source");
    //setTabPosition(QTabWidget::South);
    setTabShape(QTabWidget::Triangular);
    d->textEdit->setFont( QFont("Courier", 12) );
    connect(this, SIGNAL(currentChanged(int)),
            this, SLOT(slotCurrentTabChanged(int)));
    connect(d->textEdit, SIGNAL(textChanged()),
            this, SLOT(slotContentModified()));
    connect(d->webView, SIGNAL(titleChanged(QString)),
            this, SIGNAL(titleChanged(QString)));

    d->modified = false;
}

Деструктор лишь удаляет приватный объект:
HtmlEditorWidget::~HtmlEditorWidget()
{
    delete d;
}

Метод setContent() задаёт содержимое webView и textEdit. А метод content() соответственно возвращает содержимое. Метод title() возвращает строку которая будет отображаться в списке открытых файлов. Ну и два последних метода обрабатывают связи с сигналами, созданные в конструкторе.
void HtmlEditorWidget::setContent(const QByteArray& ba, const QString& path)
{
    if(path.isEmpty())
        d->webView->setHtml(ba);
    else
        d->webView->setHtml("file:///" + path);
    d->textEdit->setPlainText(ba);
    d->modified = false;
    d->path = path;
}

QByteArray HtmlEditorWidget::content() const
{
    QString htmlText = d->textEdit->toPlainText();
    return htmlText.toAscii();
}

QString HtmlEditorWidget::title() const
{
    return d->webView->title();
}

void HtmlEditorWidget::slotCurrentTabChanged(int tab)
{
    if(tab == 0 && d->modified)
        setContent( content(), d->path );
}

void HtmlEditorWidget::slotContentModified()
{
    d->modified = true;
    emit contentModified();
}

5.2.2 Реализация Core::IFile

Мы реализуем этот интерфейс в классе HtmlFile. Этот класс будет реализовывать несколько виртуальных методов из Core::IFile и сможет задавать флаг modified, чтобы отражать статус редактируемого документа.
#ifndef HTMLFILE_H
#define HTMLFILE_H

#include <coreplugin/ifile.h>
#include "htmleditorconstants.h"

struct HtmlFileData;
class HtmlEditor;
class HtmlEditorWidget;
class HtmlFile : public Core::IFile
{
    Q_OBJECT
public:
    HtmlFile(HtmlEditor* editor, HtmlEditorWidget* editorWidget);
    ~HtmlFile();
    void setModified(bool val=true);
    bool isModified() const;
    QString mimeType() const;
    bool save(QString *errorString, const QString &fileName, bool autoSave);
    bool reload(QString *errorString, ReloadFlag flag, ChangeType type);
    void rename(const QString &newName);
    bool open(const QString &fileName);
    void setFilename(const QString& filename);
    QString fileName() const;
    QString defaultPath() const;
    QString suggestedFileName() const;
    QString fileFilter() const;
    QString fileExtension() const;
    bool isReadOnly() const;
    bool isSaveAsAllowed() const;
    void modified(ReloadBehavior* behavior);

protected slots:
    void modified() { setModified(true); }
private:
    HtmlFileData* d;
};

struct HtmlFileData
{
    HtmlFileData()
        : mimeType(HTMLEditor::Constants::C_HTMLEDITOR_MIMETYPE),
          editorWidget(0), editor(0), modified(false) { }
    const QString mimeType;
    HtmlEditorWidget* editorWidget;
    HtmlEditor* editor;
    QString fileName;
    bool modified;
};

#endif // HTMLFILE_H

В конструкторе просто выставляем необходимые нам ассоциации. В деструкторе удаляем приватный объект.
#include "htmlfile.h"
#include <QFile>
#include <QFileInfo>
#include "htmleditor.h"
#include "htmleditorwidget.h"

HtmlFile::HtmlFile(HtmlEditor* editor, HtmlEditorWidget* editorWidget)
    : Core::IFile(editor)
{
    d = new HtmlFileData;
    d->editor = editor;
    d->editorWidget = editorWidget;
}

HtmlFile::~HtmlFile()
{
    delete d;
}

Метод setModified() выставляет флаг modified и высылает сигнал changed()
void HtmlFile::setModified(bool val)
{
    if(d->modified == val)
        return;
    d->modified = val;
    emit changed();
}

bool HtmlFile::isModified() const
{
    return d->modified;
}

Отсюда возвращаем mime-тип этого класса.
QString HtmlFile::mimeType() const
{
    return d->mimeType;
}

Метод save() вызывается при выборе пункта меню Файл -> Сохранить или по сочетанию клавиш Ctrl+S. Он сохраняет содержимое HtmlEditorWidget (из textEdit) в текущем ассоциированном файле. затем флаг modified выставляется в false.
bool HtmlFile::save(const QString &fileName)
{
    QFile file(fileName);
    if(file.open(QFile::WriteOnly))
    {
        d->fileName = fileName;
        QByteArray content = d->editorWidget->content();
        file.write(content);
        setModified(false);
        return true;
    }
    return false;
}

Метод open() вызывается при выборе пункта Файл -> Открыть. либо Ctrl+O. Загружает файл и передаёт его в фукнцию setContent() нашего виджета редактирования.
bool HtmlFile::open(const QString &fileName)
{
    QFile file(fileName);
    if(file.open(QFile::ReadOnly))
    {
        d->fileName = fileName;
        QString path = QFileInfo(fileName).absolutePath();
        d->editorWidget->setContent(file.readAll(), path);
        d->editor->setDisplayName(d->editorWidget->title());
        return true;
    }
    return false;
}

bool HtmlFile::reload(QString *errorString, ReloadFlag flag, ChangeType type)
{
    return open(d->fileName);
}

void HtmlFile::rename(const QString &newName)
{
    QFile file(d->fileName);
    file.rename(newName);
    setFilename(newName);
}

void HtmlFile::setFilename(const QString& filename)
{
    d->fileName = filename;
}

QString HtmlFile::fileName() const
{
    return d->fileName;
}

QString HtmlFile::defaultPath() const
{
    return QString();
}

QString HtmlFile::suggestedFileName() const
{
    return QString();
}

QString HtmlFile::fileFilter() const
{
    return QString();
}

QString HtmlFile::fileExtension() const
{
    return QString();
}

bool HtmlFile::isReadOnly() const
{
    return false;
}

bool HtmlFile::isSaveAsAllowed() const
{
    return true;
}

void HtmlFile::modified(ReloadBehavior* behavior)
{
    Q_UNUSED(behavior);
}

5.2.3 Реализация Core::IEditor

Мы реализуем IEditor, чтобы дать Qt Creator возможность использовать наш виджет редактора html и ассоциировать его с HtmlFile.
#ifndef HTMLEDITOR_H
#define HTMLEDITOR_H

#include <coreplugin/editormanager/ieditor.h>
#include <QToolBar>

struct HtmlEditorData;
class HtmlFile;
class HtmlEditorWidget;
class HtmlEditor : public Core::IEditor
{
    Q_OBJECT
public:
    HtmlEditor(HtmlEditorWidget* editorWidget);
    ~HtmlEditor();
    bool createNew(const QString& /*contents*/ = QString());
    QString displayName() const;
    IEditor* duplicate(QWidget* /*parent*/);
    bool duplicateSupported() const;
    Core::IFile* file();
    bool isTemporary() const;
    const char* kind() const;
    bool open(const QString& fileName = QString()) ;
    bool restoreState(const QByteArray& /*state*/);
    QByteArray saveState() const;
    void setDisplayName(const QString &title);
    QToolBar* toolBar();
    // From Core::IContext
    QWidget* widget();
    Core::Context context() const;
    QString id() const;
protected slots:
    void slotTitleChanged(const QString& title)
    { setDisplayName(title); }
private:
    HtmlEditorData* d;
};

Объект HtmlEditorData хранит указатели на объекты HtmlEditorWidget и HtmlFile, переменная displayName используется хранения имени редактора, отображаемого пользователю.
struct HtmlEditorData
{
    HtmlEditorData() : editorWidget(0), file(0) { }
    HtmlEditorWidget* editorWidget;
    QString displayName;
    HtmlFile* file;
    Core::Context context;
};

#endif // HTMLEDITOR_H

Конструктор инициализирует себя, создаёт экземпляр HtmlFile и проставляет ассоциации между всеми тремя объектами. деструктор уже по традиции лишь удаляет приватный объект.
#include "htmleditor.h"
#include "htmlfile.h"
#include "htmleditorwidget.h"

HtmlEditor::HtmlEditor(HtmlEditorWidget* editorWidget)
    : Core::IEditor(editorWidget)
{
    d = new HtmlEditorData;
    d->editorWidget = editorWidget;
    d->file = new HtmlFile(this, editorWidget);
    //Core::UniqueIDManager* uidm = Core::UniqueIDManager::instance();
    d->context = *(new Core::Context(HTMLEditor::Constants::C_HTMLEDITOR));
            //<< uidm->uniqueIdentifier(HTMLEditor::Constants::C_HTMLEDITOR);
    connect(d->editorWidget, SIGNAL(contentModified()),
            d->file, SLOT(modified()));
    connect(d->editorWidget, SIGNAL(titleChanged(QString)),
            this, SLOT(slotTitleChanged(QString)));
    connect(d->editorWidget, SIGNAL(contentModified()),
            this, SIGNAL(changed()));
}

HtmlEditor::~HtmlEditor()
{
    delete d;
}

Три простые функции для мапинга.
QWidget* HtmlEditor::widget()
{
    return d->editorWidget;
}

Core::Context HtmlEditor::context() const
{
    return d->context;
}

Core::IFile* HtmlEditor::file()
{
    return d->file;
}

Метод createNew() обнуляет содержимое HtmlEditorWidget и HtmlFile.
bool HtmlEditor::createNew(const QString& contents)
{
    Q_UNUSED(contents);
    d->editorWidget->setContent(QByteArray());
    d->file->setFilename(QString());
    return true;
}

Метод open() передаёт в HtmlFile имя файла для открытия.
bool HtmlEditor::open(const QString &fileName)
{
    return d->file->open(fileName);
}

Возвращает тип редактора.
const char* HtmlEditor::kind() const
{
    return HTMLEditor::Constants::C_HTMLEDITOR;
}

displayName используется для отображения имени файла в ComboBox.
QString HtmlEditor::displayName() const
{
    return d->displayName;
}

void HtmlEditor::setDisplayName(const QString& title)
{
    if(d->displayName == title)
        return;
    d->displayName = title;
    emit changed();
}

Остальные методы не так важны.
bool HtmlEditor::duplicateSupported() const
{
    return false;
}

Core::IEditor* HtmlEditor::duplicate(QWidget* parent)
{
    Q_UNUSED(parent);
    return 0;
}

QByteArray HtmlEditor::saveState() const
{
    return QByteArray();
}

bool HtmlEditor::restoreState(const QByteArray& state)
{
    Q_UNUSED(state);
    return false;
}

QToolBar* HtmlEditor::toolBar()
{
    return 0;
}

bool HtmlEditor::isTemporary() const
{
    return false;
}

QString HtmlEditor::id() const
{
    return QString();
}

5.2.4 Реализуем Core::IEditorFactory

Класс HtmlEditorFactory будет реализовывать интерфейс Core::IEditorFactory.
#ifndef HTMLEDITORFACTORY_H
#define HTMLEDITORFACTORY_H

#include <coreplugin/editormanager/ieditorfactory.h>
#include <QStringList>
#include "htmleditorplugin.h"

struct HtmlEditorFactoryData;

class HtmlEditorFactory : public Core::IEditorFactory
{
    Q_OBJECT
public:
    HtmlEditorFactory(HTMLEditor::Internal::HTMLEditorPlugin* owner);
    ~HtmlEditorFactory();
    QStringList mimeTypes() const;
    QString kind() const;
    Core::IEditor* createEditor(QWidget* parent);
    Core::IFile* open(const QString &fileName);
private:
    HtmlEditorFactoryData* d;
};

#endif // HTMLEDITORFACTORY_H

Структура HtmlEditorFactoryData содержит private-данные класса HtmlEditorFactory. Конструктор этой структуры инициализирует её с mime-типом содержащемся в HTMLEditor::Constants::C_HTMLEDITOR_MIMETYPE. Также он инициализирует тип редактора значением HTMLEditor::Constants::C_HTMLEDITOR. Все константы объявляются в файле htmleditorconstants.h.
#include "htmleditorfactory.h"
#include "htmleditorconstants.h"
#include <coreplugin/editormanager/editormanager.h>
#include "htmleditorwidget.h"
#include <QStringList>
#include <coreplugin/editormanager/ieditor.h>
#include "htmleditor.h"

struct HtmlEditorFactoryData
{
    HtmlEditorFactoryData()
        : kind(HTMLEditor::Constants::C_HTMLEDITOR)
    {
        mimeTypes << QString(HTMLEditor::Constants::C_HTMLEDITOR_MIMETYPE);
    }
    QString kind;
    QStringList mimeTypes;
};

Далее капелька простых привычных методов.
HtmlEditorFactory::HtmlEditorFactory(HTMLEditor::Internal::HTMLEditorPlugin* owner)
    :Core::IEditorFactory(owner)
{
    d = new HtmlEditorFactoryData;
}

HtmlEditorFactory::~HtmlEditorFactory()
{
    delete d;
}

QStringList HtmlEditorFactory::mimeTypes() const
{
    return d->mimeTypes;
}

QString HtmlEditorFactory::kind() const
{
    return d->kind;
}

Метод open() передаёт имя файла менеджеру редакторов, который в свою очередь либо создаёт и открывает новый редактор, либо открывает один из уже существующих.
Core::IFile* HtmlEditorFactory::open(const QString& fileName)
{
    Core::EditorManager* em = Core::EditorManager::instance();
    Core::IEditor* iface = em->openEditor(fileName, d->kind);
    return iface ? iface->file() : 0;
}

Этот метод возвращает экземпляр класса HtmlEditor.
Core::IEditor* HtmlEditorFactory::createEditor(QWidget* parent)
{
    HtmlEditorWidget* editorWidget = new HtmlEditorWidget(parent);
    return new HtmlEditor(editorWidget);
}

5.2.5 Основной класс расширения

Мы реализуем класс HtmlEditorPlugin используя те знания, которые мы получили во второй части. Правда немного изменим метод initialize().
bool HTMLEditorPlugin::initialize(const QStringList &arguments, QString *errorString)
{
    Q_UNUSED(arguments)
    Q_UNUSED(errorString)

    Core::ICore* core = Core::ICore::instance();
    Core::MimeDatabase* mdb = core->mimeDatabase();

    QString *errMsg = new QString();
    if(!mdb->addMimeTypes(":/text-html-mimetype.xml", errMsg))
        return false;
    addAutoReleasedObject(new HtmlEditorFactory(this));

    return true;
}

6 Добавление боковой навигационной панели


Навигационная панель в Qt Creator, это область, которая может содержать такие компоненты, как:
  • Проекты
  • Открытые документы
  • Закладки
  • Файловая система
  • Обзор классов
  • Обзор
  • Иерархия типов
  • и другие...

Кроме того, мы можем одновременно использовать не одну, а несколько навигационных панелей.
image

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

6.1 Интефейс Core::INavigationWidgetFactory

Итак, насколько мы помним, для добавления новой навигационной панели нам необходимо создать свою реализацию интерфейса Core::INavigationWidgetFactory, находящегося в файле plugins/corelib/inavigationwidgetfactory.h.

6.2 Готовим виджет для панели

Вновь создаём проект модуля Qt Creator. И назовём мы его FTPBrowser. Да, именно потому, что в изначальном документе был этот пример. также нам следует заглянуть по этой ссылке, чтобы скачать класс FtpDirModel.

Теперь создаём в нашем проекте новый класс формы и приводим форму к такому виду:
image
Когда пользователь вводит путь к FTP-директории в строку FTP Path и нажимает Go, то содержимое данного FTP тут же загружается в QTreeView находящийся чуть ниже. Я для примера назвал этот виджет FTPBrowserForm. Исходник формы приводить не буду, его можно будет потом забрать вместе со всеми остальными. Переходим сразу к реализации интерфейса.
#include <coreplugin/inavigationwidgetfactory.h>
class FtpViewNavigationWidgetFactory
        : public Core::INavigationWidgetFactory
{
public:
    FtpViewNavigationWidgetFactory() { }
    ~FtpViewNavigationWidgetFactory() { }
    Core::NavigationView createWidget();
    QString displayName() const
    {
        return "FTP View";
    }
    int priority() const
    {
        return 0;
    }

    QString id() const
    {
        return "Spreadsheet";
    }
};

Метод createWidget() лишь создаёт новый виджет и возвращает указатель на него. Метод displayName() возвращает строчку для показа пользователю.
Core::NavigationView FtpViewNavigationWidgetFactory::createWidget()
{
    Core::NavigationView view;
    view.widget = new FTPBrowserForm;
    return view;
}

Вот что у нас получилось:
image

6.3 Сохранение и восстановление состояния панелей

Иногда может понадобиться сохранять/загружать состояние панелей после перезапуска Qt Creator. Для этих целей, в интерфейсе INavigationWidgetFactory существует две специальные виртуальные функции:
void saveSettings(int position, QWidget *widget);
void restoreSettings(int position, QWidget *widget);

Переопределяя эти два метода, мы можем с лёгкостью управлять состоянием наших виджетов. Вот небольшой пример из нашего же кода:
void FtpViewNavigationWidgetFactory::saveSettings(int position, QWidget *widget)
{
    FTPBrowserForm* ftpExp = qobject_cast<FTPBrowserForm*>(widget);
    if(!ftpExp)
        return;
    QSettings *settings = Core::ICore::instance()->settings();
    settings->setValue("FtpView.URL", ftpExp->url().toString());
}

void FtpViewNavigationWidgetFactory::restoreSettings(int position, QWidget *widget)
{
    FTPBrowserForm* ftpExp = qobject_cast<FTPBrowserForm*>(widget);
    if(!ftpExp)
    return;
    QSettings *settings = Core::ICore::instance()->settings();
    QString urlStr = settings->value("FtpView.URL").toString();
    ftpExp->setUrl( QUrl(urlStr) );
}

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

Фуф… на этом пока всё… оставшаяся часть статьи будет опубликована… когда и если будет дописана =)
Виталий Петров @vitaly_KF
карма
84,1
рейтинг 20,1
FullStack developer: Qt/C++, Yii2/PHP
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (25)

  • +5
    Эпично. Вы молодец.
    • +2
      Спасибо)
  • 0
    О. Пока спец по сей среде тут, спрошу. Есть ли способ эту боковую панель перенести куда-нибудь вниз? А то на вертикально повёрнутом мониторе получается весьма вырвиглазно и ни разу не удобно.
    • +1
      Внизу как бы находится другой элемент интерфейса, а именно Output Pane.
      Вот здесь пример: habrahabr.ru/post/101311/
      Перенести же саму навигационную панель (как я понял перетаскиванием или чем-то подобным) к сожалению нельзя.
      • 0
        Ну, при работе в той же студии или даже monodevelop, места хватает внизу на всё. Пока что для меня сей печальный факт является фатальным недостатком QtCreator, ибо в целом сия IDE весьма хороша.
        • 0
          Честно говоря, впервые слышу о том, что мешает именно боковая панель навигации. Как правило на современных мониторах больше проблем создают именно панели располагаемые горизонтально…

          Максимум что можно сделать, это использовать Alt+0 или переназначить эту горячую клавишу — она прячет/показывает панель.
          • +3
            У меня нормальный монитор с нормальной подставкой, которая позволяет повернуть его на 90 градусов и получить 1200х1920 (10:16) что очень, очень удобно для кодирования, ибо строчек видно в разы больше. Все нормальные IDE позволяют раскидывать окошки как мне удобно и только QtCreator с упорством маньяка съедает треть экрана (там ещё слева панелька Unity висит, но она совершенно не мешает в остальных IDE) своей боковой панелью. Жутко бесит, хочется взять и удалить.
            • 0
              Можно написать feature-request в их трекер. И собрать достаточное количество голосов. Не думаю, что с реализацией будут проблемы, было бы желание =).
              Можно даже самому написать патч, а потом им послать.
  • +2
    Это самый офигезный туториал по расширениям в Qt Creator. Вот где вы были год назад, когда я баловался с ними?)) Однозначно огромное спасибо!
    • +2
      Благодарю! Год назад я с этим и разбирался… Но тогда так и не дописал.
  • +1
    Осилил всё-таки. Спасибо за ваш труд, достаточно полезный для некоторых.
    • +1
      Спасибо, старался. Планирую дописать если получится.
  • +1
    Пост огромный, но все же считаю правильным, что такого типа посты-туториалы не разбиты на части. Еще бы пункты меню, приведенные в начале, ссылались на соответствующие разделы в статье, было бы вообще замечательно.

    А за статью огромное спасибо, очень пригодится, ведь я как раз один из тех, у кого Creator является основной средой разработки )
    • +1
      Буду рад, если информация окажется полезной. правда я не уверен насчёт её актуальности на данный момент.
      • 0

        Прошло 4 года… а заобновить не хотите? ;-)

  • +1
    Картинки на серверах VK — это, конечно, не очень хорошо. Поменяйте хотя бы *.vkontakte.ru на *.userapi.com, если не затруднит.
    • +1
      Попробую чуть позже. А в чём конкретно проблема, скажите пожалуйста?
      • +3
        В том, что доступ к VK на работе заблокирован :/
  • 0
    Присоединяюсь ко мнению, что это самый крутой пост про расширения QtCreator, который можно найти. Огромное спасибо за труд, очень ждем продолжения. Маленькое замечание только по реализации — удаляя приватный объект, в частности HtmlEditorWidgetData, вы забываете, что он содержит указатели на объекты в куче. Вроде бы, такие объекты у вас вообще нигде не удаляются (и parent у них нулевой)
    • 0
      Большое спасибо за похвалу, я старался =)

      Продолжение вряд ли появится, так как свободного времени у меня с каждым днём всё меньше и меньше.

      А по поводу удаления этого объекта, там ведь описан деструктор с таким кодом:
      HtmlEditorWidget::~HtmlEditorWidget()
      {
          delete d;
      }
      
      • 0
        Верно, но d — указатель на структуру, часть полей которой — указатели, инициализируемые, например, так:

            d->webView = new QWebView;
            d->textEdit = new QPlainTextEdit;
        

        Я не увидел, где удаляются webView и textEdit. Автоматически они также не уничтожаться, так как свойство parent у обоих нулевое.
      • 0
        Может быть вам будет не сложно пояснить, зачем вы вообще выносите данные класса в отдельную структуру? Я вижу, да, что так смотрится аккуратнее, проявляются некоторые преимущества pimpl. Но ведь во-первых это не совсем pimpl, так как по указателю доступна не реализация класса, а только его данные; во-вторых, почти все достоинства уйдут, как только мы решим проблемы, которые нужно решить: 1) опишем полноценный деструктор такой структуры с данными 2) так как деструктор, скорее всего, будет просто удалять объекты, доступные из структуры по указателю, то нам придется обеспечить инвариант: каждый такой указатель либо указывает на реально выделенную память, либо нулевой. То есть, мы как минимум опишем конструктор этой структуры 3) структуру лучше спрятать nested-классом в класс, её использующий, ведь нечего ей делать в глобальном пространстве имен — мы так и поступим; в-третьих, мы автоматически получаем и недостатки pimpl — вы лишаете себя возможности контролировать константность методов — они не смогут менять указатель на структуру, но смогут делать что угодно с ее содержимым. А там всё.
        • +2
          Пожалуйста, посмотрите в начало поста, всё что я сделал это наполовину перевёл/перепроверил работоспособность кода из большущего документа, описывающего систему расширений Qt Creator. Я не ставил своей целью оптимизацию кода или ещё что-то подобное, главное — это разъяснение именно работы с плагинами креатора. Поэтому многое не учёл, согласен.
      • +1
        Хотя в данном конкретном случае, если я правильно понимаю, проблему можно решить так:

            d->webView = new QWebView(this);
            d->textEdit = new QPlainTextEdit(this);
        
      • +1
        Может, кому-то поможет диаграммка, кратко излагающая, кто кого создает, и кто кого (по parent-ссылкам) удаляет:



        (исходный VisualParadigm-файл)

        На диаграмме можно увидеть IDocument, который пришел на смену IFile в QtCreator 2.5

        Вероятно, стоит пояснить, что QWidget *, которого ожидает в качестве аргумента EditorFactory::createEditor, это как раз указатель на нечто, обозначенное как MainWidget

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.