Пишем простой плагин за 20 минут Введение Из этого руководства вы узнаете, как написать простенький плагин (назовём его Auscrie — от "Auto Screenshoter") для LeechCraft, проилюстрировав, таким образом, базовые концепции написания плагинов. Наш плагин сможет снимать скриншот окна LeechCraft и либо сохранять его на диск, либо постить на imagebin. Процесс будет осуществляться с помощью кнопки на панели инструментов. Вы узнаете: как создавать "пустые" плагины и собирать их; как добавлять интерфейс пользователя, созданный с помощью Qt Designer; о предпочтительных путях взаимодействия с сетью и HTTP в LeechCraft; как использовать сообщения LeechCraft для оповещения пользователя о событиях, произошедших в вашем плагине. Understanding plugins Плагины, написанные на C++, являются всего лишь динамическими библиотеками, из которых экспортируется главный экземпляр плагина. Экспорт осуществляется с помощью Q_EXPORT_PLUGIN2. Плагин должен содержать реализацию интерфейса IInfo (для чего используется заголовочный файл /src/interfaces/iinfo.h в корне репозитория), чтобы распознаваться как плагин для LeechCraft. Чтобы узнать больше о написании плагинов для приложений на C++/Qt, обратитесь к руководству "Как создавать плагины Qt". Также можете обратиться к общему обзору LeechCraft, чтобы узнать больше о его архитектуре и плагинах. Пути Для удобства, будем работать прямо в директории с исходниками, /src/plugins/auscrie. Обычно плагины разрабатываются отдельно от дерева сорцов (обратитесь к документации о трудовом процессе, чтобы узнать больше), но единственным отличием является то, что вам придётся соответствующим образом поменять пути. Например, при запуске CMake нужно будет убедиться, что CMAKE_MODULE_PATH указывает на директорию, содержащую FindLeechCraft.cmake. Переменные CMake устанавливаются с помощью опции командной строки , например:cmake -DCMAKE_MODULE_PATH=/some/path/to/LeechCraft/SDK Это руководство ориентировано на *NIX. За деталями касательно сборки под Windows обратитесь к соответствующему руководству. Каркас В репозитории LeechCraft есть удобный Python-скрипт, /tools/scripts/genplugin.py, генерирующий базовый CMakeLists.txt и файлы с объявлениями/определениями экземпляра плагина. Запускается он следующим образом: genplugin.py -a "Plugin Author" -p PluginNameWithoutSpaces -i Comma,Separated,List,Of,Base,Interfaces При запуске с опцией будет выдана краткая справка. Итак, создаём директорию /src/plugins/auscrie, переходим в неё и запускаем скрипт (заметьте, что в вашем случае путь к genplugin.py может быть другим): ../../../tools/scripts/genplugin.py -a "Your Name" -p Auscrie -i IToolBarEmbedder Наследование от IToolBarEmbedder (/src/interfaces/itoolbarembedder.h) необходимо для того, чтобы поместить кнопку на панель инструментов. Этот скрипт сгенерирует базовые файлы, но их будет достаточно для минимального работающего (точнее, загружающегося) плагина. Попробуем-ка собрать и запустить его! Для этого создадим директорию для сборки, запустим в ней cmake, потом make, чтобы собрать плагин, а затем make install от root, чтобы установить его. Из директории с исходниками выполните: mkdir build cd build cmake ../ make sudo make install Здесь использована сборка вне дерева исходников. Вообще это предпочтительный способ, т.к. можно легко очистить дерево исходников, просто удалив директорию сборки, или содержать несколько сборок с разными конфигурациями. Теперь запустим LeechCraft, откроем диалог Настройки и выберем вкладку Плагины. В списке должен присутствовать наш плагин. Если это не так, проверьте логи (~/.leechcraft/warning.log) и свяжитесь с нами. Базовые вещи Итак, у нас уже есть базовый плагин. Заполним-ка пробелы. Во-первых, нужно вписать в GetInfo какое-то описание типа "Простой плагин для снятия скриншотов". Теперь нужно создать объект QAction, который будет снимать скриншот. Для начала объявим некоторые внутренние переменные и методы: Proxy_ типа ICoreProxy_ptr, которая будет содержать указатель на прокси-объект ядра, передаваемый функции Init. Он нужен нам, т.к. через этот объект осуществляется всё взаимодействие с ядром; QAction *ShotAction_, который будет инициировать снятие скриншота; слот makeScreenshot, который будет запускаться по событию. Эту инициализацию лучше производить в функции Init, так что давайте допишем туда вот такой код для нашего экшена: GetMainWindow ()); ShotAction_ = new QAction (Proxy_->GetIcon ("screenshot"), tr ("Make a screenshot"), this); connect (ShotAction_, SIGNAL (triggered ()), this, SLOT (makeScreenshot ())); ]]> ICoreProxy_ptr используется для получения правильной иконки из текущей темы.Когда вы будете разрабатывать свои собственные плагины, вам придётся таскать иконки с собой, пока не попадёте в официальный репозиторий. Прокси также используется для получения указателя на главное окно. О ShooterDialog мы поговорим позже. Теперь давайте заполним заглушку GetActions, чтобы она возвращала ShotAction_. GetActions будет выглядеть примерно так: Plugin::GetActions () const { QList result; result << ShotAction_; return result; } ]]> Если сейчас скомпилировать и установить плагин, мы увидим иконку скриншоттера на панели инструментов, но она пока что ничего не делает. Инициация получения скриншота</tite> <para>В слоте мы запустим простой диалог, запрашивающий опции скриншота. Когда пользователь нажимает ОК, мы отключаем экшн (мы включим его, когда скриншот будет готов) и стартуем таймер в соответствии с таймаутом, заданным пользователем в диалоге:</para> <programlisting language="c++"> <![CDATA[ void Plugin::makeScreenshot () { if (Dialog_->exec () != QDialog::Accepted) return; ShotAction_->setEnabled (false); QTimer::singleShot (Dialog_->GetTimeout () * 1000, this, SLOT (shoot ())); } ]]> </programlisting> <para>Мы создали Dialog_ в Init() для того, чтобы хранить его между вызовами makeScreenshot(). То, что диалог доступен из любой функции, имеет свои преимущества: например, нет никакой нужды в хранении параметров скриншота, вроде формата или качества, так как мы можем запросить их в любой момент</para> <para>Написание диалога является довольно простой задачей для любого, кто когда-либо использовал Qt Designer, так что я не стану рассматривать это здесь. Впрочем, стоит рассказать о том, как добавлять формы в проекты, работающие с CMake. Нужно определить переменную, которая будет содержать список форм (в нашем случае это <varname>FORMS</varname>), добавить <filename>.h</filename>- и <filename>.cpp</filename>-файлы в список заголовочных файлов и исходников, вызвать функцию <function>QT4_WRAP_UI</function> для запуска на формах команды <command>uic</command>, а затем добавить результаты в список зависимостей плагина. Таким образом, середина <function>CMakeLists.txt</function> будет выглядеть так:</para> <programlisting language="cmake"> <![CDATA[ SET (SRCS auscrie.cpp shooterdialog.cpp ) SET (HEADERS auscrie.h shooterdialog.h ) SET (FORMS shooterdialog.ui ) QT4_WRAP_CPP (MOC_SRCS ${HEADERS}) QT4_WRAP_UI (UIS_H ${FORMS}) ADD_LIBRARY (leechcraft_auscrie SHARED ${COMPILED_TRANSLATIONS} ${SRCS} ${MOC_SRCS} ${UIS_H} ) ]]> </programlisting> </sect1> <sect1 id="shooting_is_fun"> <title>Скриншоты — это весело. Давайте наконец взглянем на слот shoot. Для получения доступа к окну мы используем объект Proxy_, с помощью которого ранее получали доступ к главному окну. Не забудьте подключить заголовочный файл QMainWindow, иначе приведение типов из QMainWindow* в QWidget* не сработает. setEnabled (true); QWidget *mw = Proxy_->GetMainWindow (); QPixmap pm = QPixmap::grabWidget (mw); const char *fmt = qPrintable (Dialog_->GetFormat ()); int quality = Dialog_->GetQuality (); ]]> После этого у нас есть два варианта: либо сохранить файл на диск, либо отправить его на pastebin. Первый немного проще: GetAction ()) { case ShooterDialog::ASave: { QString path = Proxy_->GetSettingsManager ()-> Property ("PluginsStorage/Auscrie/SavePath", QDir::currentPath () + "01." + Dialog_->GetFormat ()) .toString (); QString filename = QFileDialog::getSaveFileName (mw, tr ("Save as"), path, tr ("%1 files (*.%1);;All files (*.*)") .arg (Dialog_->GetFormat ())); if (!filename.isEmpty ()) { pm.save (filename, fmt, quality); Proxy_->GetSettingsManager ()-> setProperty ("PluginsStorage/Auscrie/SavePath", filename); } } break; ]]> Здесь использован менеджер настроек из ядра, который, вобщем-то, является обёрткой вокруг QSettings. Клавиши, начинающиеся с PluginsStorage, могут быть использованы плагинами, ядро не будет использовать их в своих собственных задачах. Использование менеджера настороек ядра нормально для хранения пары настроек, но если вам нужно больше, особенно если вам нужен отдельный диалог, лучше добавить его в ваш плагин. Если пользовать выбрал загрузку изображения на imagebin, следует вызвать отдельную функцию, Post(), которая обо всём позаботится: Теперь нам также следует добавить в CMakeLists.txt следующие строки (прямо перед INCLUDE (${QT_USE_FILE})): Это даст нашему плагину возможность работать с сетью, делая видимыми включения из модуля QtNetwork и линкуя плагин с библиотекой QtNetwork. Нам она определённо понадобится, т.к. плагин использует QtNetwork (например, QNetworkAccessManager и QNetworkReply) для постинга скриншотов. Так как сейчас нас не интересует имплементация загрузки скриншота на imagebin, мы перенесём этот код в отдельный класс Poster, и наша функция Post будет выглядеть очень просто: GetFormat (), Proxy_->GetNetworkAccessManager ()); connect (p, SIGNAL (finished (QNetworkReply*)), this, SLOT (handleFinished (QNetworkReply*))); connect (p, SIGNAL (error (QNetworkReply*)), this, SLOT (handleError (QNetworkReply*))); } ]]> Здесь мы ещё раз использовали Proxy_, на этот раз — для получения общего для всего приложения экземпляра QNetworkAccessManager с методом GetNetworkAccessManager. Использовать общий экземпляр QNetworkAccessManager всегда лучше, так как это позволяет получать доступ к общему кэшу и базе cookie, а также предоставляет возможность оптимизировать запросы путём повтороного использования соединений, например. Следует также отметить, что в случае, когда вам нужно просто скачать файл (в данном плагине это не нужно, но это довольно часто возникающая задача), достаточно просто сгенерировать соответствующий сигнал, не заботясь о доступе к сети, менеджерах, ответах и прочем. Этот подход детально обсуждается в документе Обзор. Считается нормальным создание Poster в куче, не заботясь об освобождении памяти. Она будет освобождена соответствующими слотами. Мы подключаемся к сигналам класса Poster finished QNetworkReply *reply и error QNetworkReply *reply — для того, чтобы получать извещения о том, что загрузка завершилась (а также чтобы узнать о возможных ошибках). Poster генерирует сигнал с параметром — QNetworkReply, который изначально излучил соответствующий сигнал. Взглянем на handleFinished QNetworkReply *reply (кстати, не забудьте объявить все представленные члены в определении класса): deleteLater (); QString result = reply->readAll (); QRegExp re ("

You can find this at ([^<]+)

"); if (!re.exactMatch (result)) { Entity e = Util::MakeNotification ("Auscrie", tr ("Page parse failed"), PWarning_); emit gotEntity (e); return; } QString pasteUrl = re.cap (1); Entity e = Util::MakeNotification ("Auscrie", tr ("Image pasted: %1, the URL was copied to the clipboard") .arg (pasteUrl), PInfo_); QApplication::clipboard ()->setText (pasteUrl, QClipboard::Clipboard); QApplication::clipboard ()->setText (pasteUrl, QClipboard::Selection); emit gotEntity (e); } ]]>
В первую очередь мы указываем посылающему объекту (экземпляру Poster, созданному ранее) удалить себя, как только управление будет возвращено циклу обработки событий. Мы не можем просто написать что-то вроде delete sender ();, потому что объекты не могут быть удалены из их же обработчиков сигналов. После этого мы с помощью readAll получаем страницу, которую вернул сервер, и пытаемся извлечь ссылку на наше свежезагруженное изображение с помощью довольно простого регулярного выражения. Если сделать это не удаётся, мы генерируем уведомление об ошибке с приоритетом Warning и прерываем обработку. Заметьте также, что в сигналах классов и слотов, использующих структуры данных LeechCraft, вы должны использовать полностью определённые имена со всеми пространствами имён, например void gotEntity const LeechCraft::Entity& entity В противном случае система метаобъектов Qt не распознает их. Здесь также используется система сообщений LeechCraft. С её помощью можно отправлять пользователю оповещения о событиях, происходящих в плагине, вроде ошибок или всяких информационных сообщений. Для создания оповещения используется функция LeechCraft::Util::MakeNotification, которая может быть добавлена с помощью ]]>. Эта функция принимает три параметра: заголовок, тело и приоритет оповещения. Более детально оповещения и сообщения LeechCraft обсуждаются в документе Обзор.
Заключение Вобщем-то, это всё. Теперь у нас есть работающий, полезный плагин. Конечно же, есть вещи, которые стоило бы добавить: например, можно было бы хранить историю всех запощенных скриншотов или показывать красивенький прогрессбар, демонстрирующий загрузку изображения. Также вам наверняка захочется добавить поддержку локализаций, но нашей целью здесь было привыкнуть к концепции плагинов LeechCraft.