Text only
15 Aug 2010
 
 
Tools: wrap/unwrap  
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
<?xml version="1.0" encoding="utf-8"?>
<book id="writing_simple_plugin_in_20_minutes" xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="en">
<bookinfo>
<title>Пишем простой плагин за 20 минут</title>
</bookinfo>
<sect1 id="intro">
<title>Введение</title>
<para>Из этого руководства вы узнаете, как написать простенький плагин (назовём его Auscrie — от "Auto Screenshoter") для LeechCraft, проилюстрировав, таким образом, базовые концепции написания плагинов. Наш плагин сможет снимать скриншот окна LeechCraft и либо сохранять его на диск, либо постить на imagebin. Процесс будет осуществляться с помощью кнопки на панели инструментов.</para>
<para>Вы узнаете:
<itemizedlist>
<listitem>как создавать "пустые" плагины и собирать их;</listitem>
<listitem>как добавлять интерфейс пользователя, созданный с помощью Qt Designer;</listitem>
<listitem>о предпочтительных путях взаимодействия с сетью и HTTP в LeechCraft;</listitem>
<listitem>как использовать сообщения LeechCraft для оповещения пользователя о событиях, произошедших в вашем плагине.</listitem>
</itemizedlist>
</para>
</sect1>
<sect1 id="understanding_plugins">
<title>Understanding plugins</title>
<para>Плагины, написанные на C++, являются всего лишь динамическими библиотеками, из которых экспортируется главный экземпляр плагина. Экспорт осуществляется с помощью Q_EXPORT_PLUGIN2. Плагин должен содержать реализацию интерфейса IInfo (для чего используется заголовочный файл <filename>/src/interfaces/iinfo.h</filename> в корне репозитория), чтобы распознаваться как плагин для LeechCraft.</para>
<para>Чтобы узнать больше о написании плагинов для приложений на C++/Qt, обратитесь к руководству "Как создавать плагины Qt".</para>
<para>Также можете обратиться к общему обзору LeechCraft, чтобы узнать больше о его архитектуре и плагинах.</para>
</sect1>
<sect1 id="paths">
<title>Пути</title>
<para>Для удобства, будем работать прямо в директории с исходниками, <filename>/src/plugins/auscrie</filename>. Обычно плагины разрабатываются отдельно от дерева сорцов (обратитесь к документации о трудовом процессе, чтобы узнать больше), но единственным отличием является то, что вам придётся соответствующим образом поменять пути. Например, при запуске CMake нужно будет убедиться, что <parameter>CMAKE_MODULE_PATH</parameter> указывает на директорию, содержащую <filename>FindLeechCraft.cmake</filename>. Переменные CMake устанавливаются с помощью опции командной строки <option>-D</option>, например:<screen><command>cmake -DCMAKE_MODULE_PATH=<replaceable>/some/path/to/LeechCraft/SDK</replaceable></command></screen></para>
<para>Это руководство ориентировано на *NIX. За деталями касательно сборки под Windows обратитесь к соответствующему руководству.</para>
</sect1>
<sect1 id="general_skeleton">
<title>Каркас</title>
<para>В репозитории LeechCraft есть удобный Python-скрипт, <filename>/tools/scripts/genplugin.py</filename>, генерирующий базовый <filename>CMakeLists.txt</filename> и файлы с объявлениями/определениями экземпляра плагина. Запускается он следующим образом:</para>
<screen><command>genplugin.py -a "<replaceable>Plugin Author</replaceable>" -p <replaceable>PluginNameWithoutSpaces</replaceable> -i <replaceable>Comma,Separated,List,Of,Base,Interfaces</replaceable></command></screen>
<para>При запуске с опцией <option>-h</option> будет выдана краткая справка.</para>
<para>Итак, создаём директорию <filename>/src/plugins/auscrie</filename>, переходим в неё и запускаем скрипт (заметьте, что в вашем случае путь к genplugin.py может быть другим):</para>
<screen><command>../../../tools/scripts/genplugin.py -a "Your Name" -p Auscrie -i IToolBarEmbedder</command></screen>
<para>Наследование от <interfacename>IToolBarEmbedder</interfacename> (<filename>/src/interfaces/itoolbarembedder.h</filename>) необходимо для того, чтобы поместить кнопку на панель инструментов.</para>
<para>Этот скрипт сгенерирует базовые файлы, но их будет достаточно для минимального работающего (точнее, загружающегося) плагина. Попробуем-ка собрать и запустить его! Для этого создадим директорию для сборки, запустим в ней <command>cmake</command>, потом <command>make</command>, чтобы собрать плагин, а затем <command>make install</command> от root, чтобы установить его. Из директории с исходниками выполните:</para>
<screen>
<command>mkdir build</command>
<command>cd build</command>
<command>cmake ../</command>
<command>make</command>
<command>sudo make install</command>
</screen>
<para>Здесь использована сборка вне дерева исходников. Вообще это предпочтительный способ, т.к. можно легко очистить дерево исходников, просто удалив директорию сборки, или содержать несколько сборок с разными конфигурациями.</para>
<para>Теперь запустим LeechCraft, откроем диалог Настройки и выберем вкладку Плагины. В списке должен присутствовать наш плагин. Если это не так, проверьте логи (<filename>~/.leechcraft/warning.log</filename>) и свяжитесь с нами.</para>
</sect1>
<sect1 id="basic_stuff">
<title>Базовые вещи</title>
<para>Итак, у нас уже есть базовый плагин. Заполним-ка пробелы.</para>
<para>Во-первых, нужно вписать в <function>GetInfo</function> какое-то описание типа "Простой плагин для снятия скриншотов".</para>
<para>Теперь нужно создать объект QAction, который будет снимать скриншот. Для начала объявим некоторые внутренние переменные и методы:
<itemizedlist>
<listitem><varname>Proxy_</varname> типа <classname>ICoreProxy_ptr</classname>, которая будет содержать указатель на прокси-объект ядра, передаваемый функции <function>Init</function>. Он нужен нам, т.к. через этот объект осуществляется всё взаимодействие с ядром;</listitem>
<listitem><classname>QAction</classname> *<varname>ShotAction_</varname>, который будет инициировать снятие скриншота;</listitem>
<listitem>слот <function>makeScreenshot</function>, который будет запускаться по событию.</listitem>
</itemizedlist>
</para>
<para>Эту инициализацию лучше производить в функции Init, так что давайте допишем туда вот такой код для нашего экшена:</para>
<programlisting language="c++">
<![CDATA[
Proxy_ = proxy;
Dialog_ = new ShooterDialog (Proxy_->GetMainWindow ());
ShotAction_ = new QAction (Proxy_->GetIcon ("screenshot"),
tr ("Make a screenshot"),
this);
connect (ShotAction_,
SIGNAL (triggered ()),
this,
SLOT (makeScreenshot ()));
]]>
</programlisting>
<para><classname>ICoreProxy_ptr</classname> используется для получения правильной иконки из текущей темы.Когда вы будете разрабатывать свои собственные плагины, вам придётся таскать иконки с собой, пока не попадёте в официальный репозиторий. Прокси также используется для получения указателя на главное окно. О <classname>ShooterDialog</classname> мы поговорим позже.</para>
<para>Теперь давайте заполним заглушку <function>GetActions</function>, чтобы она возвращала <varname>ShotAction_</varname>. <function>GetActions</function> будет выглядеть примерно так:</para>
<programlisting language="c++">
<![CDATA[
QList<QAction*> Plugin::GetActions () const
{
QList<QAction*> result;
result << ShotAction_;
return result;
}
]]>
</programlisting>
<para>Если сейчас скомпилировать и установить плагин, мы увидим иконку скриншоттера на панели инструментов, но она пока что ничего не делает.</para>
</sect1>
<sect1 id="initiating_screenshooting">
<title>Инициация получения скриншота</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>Скриншоты — это весело.</title>
<para>Давайте наконец взглянем на слот <function>shoot</function>.</para>
<para>Для получения доступа к окну мы используем объект <varname>Proxy_</varname>, с помощью которого ранее получали доступ к главному окну. Не забудьте подключить заголовочный файл <classname>QMainWindow</classname>, иначе приведение типов из <classname>QMainWindow</classname>* в <classname>QWidget</classname>* не сработает.</para>
<programlisting language="c++">
<![CDATA[
void Plugin::shoot ()
{
ShotAction_->setEnabled (true);
QWidget *mw = Proxy_->GetMainWindow ();
QPixmap pm = QPixmap::grabWidget (mw);
const char *fmt = qPrintable (Dialog_->GetFormat ());
int quality = Dialog_->GetQuality ();
]]>
</programlisting>
<para>После этого у нас есть два варианта: либо сохранить файл на диск, либо отправить его на pastebin. Первый немного проще:</para>
<programlisting language="c++">
<![CDATA[
switch (Dialog_->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;
]]>
</programlisting>
<para>Здесь использован менеджер настроек из ядра, который, вобщем-то, является обёрткой вокруг <classname>QSettings</classname>. Клавиши, начинающиеся с <emphasis>PluginsStorage</emphasis>, могут быть использованы плагинами, ядро не будет использовать их в своих собственных задачах. Использование менеджера настороек ядра нормально для хранения пары настроек, но если вам нужно больше, особенно если вам нужен отдельный диалог, лучше добавить его в ваш плагин.</para>
<para>Если пользовать выбрал загрузку изображения на imagebin, следует вызвать отдельную функцию, Post(), которая обо всём позаботится:</para>
<programlisting language="c++">
<![CDATA[
case ShooterDialog::AUpload:
{
QBuffer buf;
pm.save (&buf,
fmt,
quality);
Post (buf.data ());
}
break;
}
}
]]>
</programlisting>
<para>Теперь нам также следует добавить в <filename>CMakeLists.txt</filename> следующие строки (прямо перед <code>INCLUDE (${QT_USE_FILE})</code>):</para>
<programlisting language="cmake">
<![CDATA[
SET (QT_USE_QTNETWORK TRUE)
]]>
</programlisting>
<para>Это даст нашему плагину возможность работать с сетью, делая видимыми включения из модуля QtNetwork и линкуя плагин с библиотекой QtNetwork. Нам она определённо понадобится, т.к. плагин использует QtNetwork (например, <classname>QNetworkAccessManager</classname> и <classname>QNetworkReply</classname>) для постинга скриншотов.</para>
<para>Так как сейчас нас не интересует имплементация загрузки скриншота на imagebin, мы перенесём этот код в отдельный класс <classname>Poster</classname>, и наша функция <function>Post</function> будет выглядеть очень просто:</para>
<programlisting language="c++">
<![CDATA[
void Plugin::Post (const QByteArray& data)
{
Poster *p = new Poster (data,
Dialog_->GetFormat (),
Proxy_->GetNetworkAccessManager ());
connect (p,
SIGNAL (finished (QNetworkReply*)),
this,
SLOT (handleFinished (QNetworkReply*)));
connect (p,
SIGNAL (error (QNetworkReply*)),
this,
SLOT (handleError (QNetworkReply*)));
}
]]>
</programlisting>
<para>Здесь мы ещё раз использовали <varname>Proxy_</varname>, на этот раз — для получения общего для всего приложения экземпляра <classname>QNetworkAccessManager</classname> с методом <function>GetNetworkAccessManager</function>. Использовать общий экземпляр <classname>QNetworkAccessManager</classname> всегда лучше, так как это позволяет получать доступ к общему кэшу и базе cookie, а также предоставляет возможность оптимизировать запросы путём повтороного использования соединений, например.</para>
<para>Следует также отметить, что в случае, когда вам нужно просто скачать файл (в данном плагине это не нужно, но это довольно часто возникающая задача), достаточно просто сгенерировать соответствующий сигнал, не заботясь о доступе к сети, менеджерах, ответах и прочем. Этот подход детально обсуждается в документе Обзор.</para>
<para>Считается нормальным создание <classname>Poster</classname> в куче, не заботясь об освобождении памяти. Она будет освобождена соответствующими слотами.</para>
<para>Мы подключаемся к сигналам класса <classname>Poster</classname> —
<funcsynopsis>
<funcprototype>
<funcdef><function>finished</function></funcdef>
<paramdef>QNetworkReply *<parameter>reply</parameter></paramdef>
</funcprototype>
</funcsynopsis>
и
<funcsynopsis>
<funcprototype>
<funcdef><function>error</function></funcdef>
<paramdef>QNetworkReply *<parameter>reply</parameter></paramdef>
</funcprototype>
</funcsynopsis>
— для того, чтобы получать извещения о том, что загрузка завершилась (а также чтобы узнать о возможных ошибках). <classname>Poster</classname> генерирует сигнал с параметром — <classname>QNetworkReply</classname>, который изначально излучил соответствующий сигнал.</para>
<para>Взглянем на
<funcsynopsis>
<funcprototype>
<funcdef><function>handleFinished</function></funcdef>
<paramdef>QNetworkReply *<parameter>reply</parameter></paramdef>
</funcprototype>
</funcsynopsis>
(кстати, не забудьте объявить все представленные члены в определении класса):</para>
<programlisting language="c++">
<![CDATA[
void Plugin::handleFinished (QNetworkReply *reply)
{
sender ()->deleteLater ();
QString result = reply->readAll ();
QRegExp re ("<p>You can find this at <a href='([^<]+)'>([^<]+)</a></p>");
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);
}
]]>
</programlisting>
<para>В первую очередь мы указываем посылающему объекту (экземпляру <classname>Poster</classname>, созданному ранее) удалить себя, как только управление будет возвращено циклу обработки событий. Мы не можем просто написать что-то вроде <code>delete sender ();</code>, потому что объекты не могут быть удалены из их же обработчиков сигналов.</para>
<para>После этого мы с помощью <function>readAll</function> получаем страницу, которую вернул сервер, и пытаемся извлечь ссылку на наше свежезагруженное изображение с помощью довольно простого регулярного выражения. Если сделать это не удаётся, мы генерируем уведомление об ошибке с приоритетом Warning и прерываем обработку. Заметьте также, что в сигналах классов и слотов, использующих структуры данных LeechCraft, вы должны использовать полностью определённые имена со всеми пространствами имён, например
<funcsynopsis>
<funcprototype>
<funcdef>void <function>gotEntity</function></funcdef>
<paramdef>const LeechCraft::Entity&amp; <parameter>entity</parameter></paramdef>
</funcprototype>
</funcsynopsis>
В противном случае система метаобъектов Qt не распознает их.</para>
<para>Здесь также используется система сообщений LeechCraft. С её помощью можно отправлять пользователю оповещения о событиях, происходящих в плагине, вроде ошибок или всяких информационных сообщений. Для создания оповещения используется функция <function>LeechCraft::Util::MakeNotification</function>, которая может быть добавлена с помощью <code><![CDATA[#include <plugininterface/util.h>]]></code>. Эта функция принимает три параметра: заголовок, тело и приоритет оповещения. Более детально оповещения и сообщения LeechCraft обсуждаются в документе Обзор.</para>
</sect1>
<sect1 id="conclusion">
<title>Заключение</title>
<para>Вобщем-то, это всё. Теперь у нас есть работающий, полезный плагин. Конечно же, есть вещи, которые стоило бы добавить: например, можно было бы хранить историю всех запощенных скриншотов или показывать красивенький прогрессбар, демонстрирующий загрузку изображения. Также вам наверняка захочется добавить поддержку локализаций, но нашей целью здесь было привыкнуть к концепции плагинов LeechCraft.</para>
</sect1>
</book>