16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

Рюшечки для Форточек, или Как написать простое расширение оболочки

23.12.2003

Роман Батищев


Вместо предисловия

Когда пользователь после щелчка правой кнопкой мыши в окне Проводника Windows в открывшемся контекстном меню видит такие команды, как "Добавить в архив..." или "Проверить на наличие вирусов", это воспринимается им как нечто само собой разумеющееся. И уж тем более, никому и в голову не придет задумываться о природе этого явления. Никому, кроме, разве что, разработчика, решившего добавить к своей программе подобного рода свойства, но не имеющего опыта программирования для оболочки Windows.

Целью этой статьи полагалась помощь не только программистам, но и всем желающим, кто имеет навыки программирования и хотел бы на простом примере обработчика для контекстного меню Explorer'а научиться создавать собственные, пока простые, расширения оболочки. Ее прочтение не требует знакомства читателя с технологией COM или интерфейсами shell, но предполагает знание читателем C++ и наличие у него не только MSVC, но и хоть какого-то опыта работы в этой среде разработки.

Приступая к работе

Расширение оболочки - это объект COM, реализованный в форме DLL и надлежащим образом зарегистрированный в системе, вызываемый Windows shell (он же Explorer) в качестве сервера, работающего в адресном пространстве процесса вызвавшего его приложения. Функционально расширения представляют собой обработчики событий для определенного типа объектов, как правило, файлов, либо операций оболочки, не ассоциированных с каким-либо конкретным типом файлов, к примеру, для перетаскивания иконок правой кнопкой мыши.

Когда пользователь, к примеру, щелкает правой кнопкой в окне Explorer'а, система, создав статическое контекстное меню, содержащее только предопределенные команды, ищет в реестре и загружает все зарегистрированные обработчики возникшего события для выбранного типа файла, чтобы те добавили (или не добавили) к меню свои команды.

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

Чтобы заставить Explorer выгружать расширения как можно чаще, в реестре создайте раздел HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Current Version\Explorer\AlwaysUnloadDLL и установите значение по умолчанию равным 1. Это лучшее, что можно сделать под 9x. Счастливые обладатели NT и 2000 могут в разделе HKEY_CURRENT_USER\ Sjftware\Microsoft\Windows\Current Version\Explorer создать параметр типа DWORD с именем DesktopProcess и значением, равным 1. Это позволит запускать для каждого нового окна проводника отдельный процесс, а закрытие этого окна будет приводить к автоматическому освобождению загруженной DLL.

Не забудьте перерегистрироваться в системе или перезагрузить машину, чтобы изменения, сделанные в реестре, вступили в силу.

Реализация расширения оболочки во многом определяется его типом, однако есть ряд общих моментов. К примеру, любая DLL должна экспортировать функцию DllMain() - стандартную точку входа в библиотеку, и DLL, реализующие расширения оболочки, в этом плане не являются исключениями. Помимо DllMain все расширения должны экспортировать функции DllGetClassObject() и DllCanUnloadNow(), которую COM вызывает, чтобы определить, можно ли выгрузить библиотеку из памяти.

Как и все объекты COM, расширения оболочки должны реализовывать методы базового интерфейса IUnknown и, в большинстве случаев, одного из интерфейсов, которые shell использует для инициализации обработчика, IPersistFile или IShellExtInit. Обработчики контекстного меню включают реализацию второго из них.

Возможно, для кого-то названия этих интерфейсов и каких-то экспортируемых функций звучат пугающе или, по крайней мере, сбивают с толку, уводя в сторону от предмета разговора. На самом деле, эти технические детали не столь важны, как может показаться, и для того чтобы повторить предлагаемый проект, их можно полностью проигнорировать. Они могут оказаться интересными для тех, кто после прочтения статьи захочет познать природу рассматриваемых здесь вещей, указав им направление для собственных изысканий.

Что же касается тех, кто вообще ни разу в своей практике не создавал динамически загружаемые библиотеки, то и им не стоит пугаться - использование мастера проектов MSVC позволит создать костяк, имеющий в своем составе весь необходимый исходный код и для точки входа, и для других упомянутых функций.

Итак, в MSVC создайте новый проект, выберите ATL COM AppWizard, назовите проект Proba и оставьте выставленные по умолчанию опции без изменений. Теперь необходимо добавить COM-объект расширения оболочки. Для этого на закладке ClassView окна Workspace щелкните правой кнопкой мыши на названии проекта и выберите в открывшемся меню New ATL Object. В появившемся окне ATL Object Wizard по умолчанию уже выбран Simple Object, поэтому нажмите кнопку Next и в поле Short Name введите ProbaShExt. Остальные поля будут заполнены автоматически. В итоге будет создан класс CProbaShExt, а к проекту добавлены файлы, содержащие базовый код реализации объекта COM. Именно эти файлы подвергнутся наибольшим изменениям.

Теперь осталось добавить свой код.

Интерфейс инициализации

При загрузке расширения Explorer вызовет функцию QueryInterface(), чтобы получить указатель на интерфейс IShellExtInit. Этот интерфейс включает единственную функцию Initialize. Вот ее прототип:

HRESULT Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj, HKEY hkeyProgID).

Explorer использует этот метод, чтобы предоставить расширению различную информацию. Так, pidlFolder - это указатель на идентификатор каталога, в котором размещены файлы, участвующие в операции, через второй параметр - указатель на интерфейс IDataObj - можно получить имена этих файлов, а третий параметр - это дескриптор раздела реестра, в котором сохранена регистрационная информация загруженного расширения. Нам понадобится лишь lpdobj.

Откройте файл ProbaShExt.h и добавьте в него выделенные строки:

// ProbaShExt.h : Declaration of the CProbaShExt
#ifndef __PROBASHEXT_H_
#define __PROBASHEXT_H_
#include <shlobj.h>
#include <comdef.h>
#include "resource.h" // main symbols
/////////////////////////////////////////////////////////////////////////////
// CProbaShExt
class ATL_NO_VTABLE CProbaShExt :
public CComObjectRootEx<CComSingle ThreadModel>,
public CComCoClass<CProbaShExt, &CLSID_ ProbaShExt>,
public IDispatchImpl<IProbaShExt, &IID_ IProbaShExt, &LIBID_PROBALib>,
public IShellExtInit,
public IContextMenu
{
public:
CProbaShExt()
{
}

DECLARE_REGISTRY_RESOURCEID(IDR_PROBASHEXT)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CProbaShExt)
COM_INTERFACE_ENTRY(IProbaShExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
// IProbaShExt
protected:
TCHAR filename[MAX_PATH];
public:
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand) (LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);};
#endif //__PROBASHEXT_H_

Список имен интерфейсов, заключенный между макроопределениями COM_MAP, указывает ATL на то, какие интерфейсы другие программы могут запросить у создаваемой нами DLL. Также объявлена функция Initialize(), упоминавшаяся выше, и три функции, принадлежащие интерфейсу IContextMenu. Использование макроса STDMETHOD, в который "завернуты" объявления этих функций, несколько облегчает работу программиста, позволяя тому не заботиться о соблюдении соглашения о вызовах, принятом в COM.

Результат, возвращаемый объявленной таким образом функцией, имеет тип HRESULT. Это 32-разрядное значение, которое, если не используются предопределенные константы кодов завершения функции, формируется в ее теле при помощи макроса MAKE_HRESULT, которому в качестве параметров передаются код серьезности ошибки, код типа устройства и информационный код.

Для общей проверки того, насколько успешно отработала функция, возвращающая HRESULT, часто применяются макросы SUCCEEDED, дающие истинное значение в случае нормального завершения, и FAILED, возвращающий прямо противоположный результат.

Для простоты и наглядности примера положим целью создаваемого расширения вывод сообщения с именем выбранного файла, и даже если пользователь выделил группу файлов, достаточно отобразить название первого из них.

Как уже упоминалось выше, имена файлов, участвующих в операции, можно получить через второй параметр, передаваемый функции Initialize(). Полученное имя необходимо сохранить для дальнейшего использования. Поэтому к объявлению класса был добавлен защищенный раздел, содержащий объявление буфера filename.

Для придания коду большей гибкости, при желании, вместо единственного строкового буфера можно использовать библиотеку STL или какой-нибудь динамический массив. Те, кто решит использовать в своем расширении классы MFC, не должны забывать добавлять в начало каждой функции, где предполагается их использование, макрос AFX_MANAGE_STATE(AfxGetStaticModuleState()) для инициализации библиотеки. Это необходимо делать до объявлений всех переменных, так как их конструкторы могут вызывать функции MFC.

Теперь перейдите в файл ProbaShExt.cpp и добавьте определение функции Initialize():

HRESULT CProbaShExt::Initialize (LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj, HKEY hkeyProgID)
{
FORMATETC fmt = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stg = {TYMED_HGLOBAL};
HDROP hdrop;
HRESULT hr = E_INVALIDARG; // предполагаем неудачное завершение
// Искать данные в формате CF_HDROP,
// содержащем сведения о файлах,
// участвующих в операции
if (SUCCEEDED(lpdobj->GetData(&fmt, &stg)))
{
hdrop = (HDROP)GlobalLock(stg.hGlobal);
if (hdrop)
{
// получаем сведения о числе файлов,
// участвующих в операции
UINT numfiles = DragQueryFile (hdrop, 0xFFFFFFFF, NULL, 0);
if (numfiles)
{
// и узнаем имя первого из них,
// сохранив его в буфере filename
if (DragQueryFile(hdrop, 0, filename, MAX_PATH))
{
// сигнализируем об успешном завершении функции

hr = S_OK;
}
}
}
GlobalUnlock(stg.hGlobal);
ReleaseStgMedium(&stg);
}
return hr;
}

Используя указатель на интерфейс IDataObj, мы вызываем его метод GetData(), передавая в качестве параметров адреса объявленных и инициализированных в начале функции структур FORMATETC и STGMEDIUM, передающих в GetData() сведения о том, что мы ожидаем получить в результате ее работы. Имена файлов хранятся в том же формате, что и при выполнении операции drag'n'drop, поэтому, убедившись в успешном завершении функции GetData(), для получения сведений о числе файлов необходимо вызвать функцию DragQueryFile(). Для того чтобы узнать имя первого файла, снова вызываем DragQueryFile(), на этот раз передавая в качестве второго параметра его индекс в группе, а также адрес и размер буфера, в котором его имя будет сохранено.

В итоге успешного завершения инициализирующей функции Explorer'у будет возвращен код S_OK, и оболочка продолжит работу с нашей DLL, вызывая методы для взаимодействия с контекстным меню.

Изменение меню

Интерфейс IContextMenu имеет в своем составе три метода. Для добавления команд вызывается метод QueryContextMenu(). Вот его прототип:

HRESULT QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags).

Первым параметром функции является дескриптор меню, indexMenu показывает позицию, с которой необходимо производить вставку своих команд, а idCmdFirst и idCmdLast обозначают диапазон допустимых идентификаторов. Для формирования правильного результата, который функция должна вернуть в случае успешного завершения своей работы, необходимо использовать макрос MAKE_HRESULT, в который в качестве третьего параметра передается число, равное разности наибольшего идентификатора и idCmdFirst, увеличенной на единицу.

HRESULT CProbaShExt::QueryContextMenu (HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
// Если uFlags включает CMF_DEFAULTONLY, ничего делать не надо
if (uFlags & CMF_DEFAULTONLY)
{
returnMAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
}
if (InsertMenu(hmenu, indexMenu, MF_BYPOSITION, idCmdFirst, _T("Мое расширение оболочки")))
{
// Если удалось добавить свою команду в меню,
// возвращаем результат, сигнализирующий об успехе
return MAKE_HRESULT (SEVERITY_SUCCESS, FACILITY_NULL, 1);
}
return E_FAIL;
}

Первое, что необходимо сделать, - проверить uFlags на наличие флага CMF_DEFAULTONLY. Если он включен, расширения оболочки не должны модифицировать контекстное меню. В противном случае мы вольны добавить столько команд, сколько необходимо, соблюдая при этом требование не выходить за рамки диапазона допустимых значений их идентификаторов.

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

Отображение подсказки в статусной строке

После того как в меню будут добавлены нужные нам команды, следует ожидать вызова системой еще одного метода IContextMenu, ответственного за предоставление пользователю подсказки, которую Explorer отображает в своей строке состояния. Этот метод называется GetCommandString():

HRESULT GetCommandString(UINT idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax).

Параметр idCmd является индексом, указывающим на то, какая команда из добавленных обработчиком была выбрана пользователем. Учитывая, что наше расширение добавляет в меню только одну строку, этот параметр всегда будет равен 0, а если бы этих команд было, к примеру, 3, idCmd мог бы принимать значения от 0 до 2.

Параметр uFlags обозначает тип информации, который ожидает получить оболочка, pszName - это адрес буфера, принадлежащего оболочке, в котором необходимо сохранить строку, а cchMax - его размер.

В случае успеха функция возвращает оболочке S_OK, в случае неудачи - код ошибки OLE, к примеру, E_FAIL.

GetCommandString() может быть вызван системой также и для того, чтобы получить так называемые "глаголы" (verbs) для строк меню, однако, в большинстве случаев и, в частности, в нашем, такие вызовы можно проигнорировать.

HRESULTCProbaShExt::GetCommand String (UINT idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax)
{
USES_CONVERSION;
// Значение idCmd должно всегда равняться 0,
// так как нами была добавлена только одна команда
if (idCmd)
{
return E_INVALIDARG;
}
// Если Explorer ожидает получить строку подсказки,
// ее необходимо скопировать в предоставленный им буфер
if (uFlags & GCS_HELPTEXT)
{
LPCTSTR text = _T("Эта команда добавлена моим расширением оболочки");
if (uFlags & GCS_UNICODE)
{
// Если Explorer ожидает получить запрошенную строку
// в формате Unicode, ее необходимо конвертировать в
// строку Unicode и использовать соответствующую функцию API,
lstrcpynW ( (LPWSTR) pszName, T2CW(text), cchMax );
}
else
{
// А если Explorer хочет строку в формате ANSI,
// то мы делаем то же самое, только конвертирующий макрос
// функцию копирования выбираем соответствующие
lstrcpynA ( pszName, T2CA(text), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}

Итак, если оболочка запрашивает у нас текст подсказки, остается выяснить, в каком формате должна быть представлена возвращаемая через принадлежащий Explorer'у буфер строка, ANSI или Unicode, и надлежащим образом использовать конвертирующие макросы T2CA или T2CW и соответствующие версии функций API.

Макроопределение USES_CONVERSION в самом начале функции объявляет локальную переменную, которую используют в своей работе упомянутые макросы.

Обработка команды

Рассмотрим последний метод интерфейса IContextMenu, вызываемый оболочкой, когда пользователь выбирает в меню команду, добавленную расширением. Он носит название InvokeComand(), ниже приведен его прототип:

HRESULTInvokeCommand(LPCMINVOKE COMMANDINFO pici).

Единственным параметром, передаваемым в метод системой, является указатель на структуру типа CMINVOKECOMMANDINFO или CMINVOKE COMMANDINFOEX, которая, по сути, является расширенной версией первой структуры, адаптированной для работы со строками в формате Unicode.

Нас интересует поле структуры lpVerb, через который в функцию передается глагол или индекс, идентифицирующий команду. Нас интересует индекс команды, который может равняться только нулю, так как нашим расширением к меню была добавлена одна-единственная команда.

HRESULT CProbaShExt::InvokeCommand (LPCMINVOKECOMMANDINFO pici)
{
// Проверим сперва, не является ли lpVerb
// указателем на строку, и, если да, прервем выполнение функции
if (HIWORD(pCmdInfo->lpVerb))
{
return E_INVALIDARG;
}
// Проверим индекс команды, который может равняться только 0 (см. выше).
switch (LOWORD(pici->lpVerb))
{
case 0:
{
TCHAR msg[MAX_PATH + 32];
wsprintf(msg, _T("Имя выбранного фала: \"%s\""), filename);
MessageBox(pCmdInfo->hwnd, msg, _T("Мое расширение оболочки"), MB_ICONINFORMATION);
return S_OK;
}
default:
{
// Этого не может быть,
// потому что этого не может быть
return E_INVALIDARG;
}
}
}

Регистрация расширения оболочки

Теперь, когда в проекте реализованы методы всех необходимых интерфейсов, как рассказать оболочке о существовании нашего расширения?

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

Чтобы созданный нами обработчик вызывался для всех файлов, необходимо зарегистрировать его под ключом "*", в разделе ContextMenuHandlers, а если есть желание заставить Explorer вызывать наш обработчик и для каталогов, необходимо добавить сведения о нашем сервере и в раздел Folder.

Пришло время обратить внимание на генерируемый ATL COM AppWizard скрипт для регистрации расширения - файл ProbaShExt.rgs. Он содержит инструкции о том, какие разделы и параметры должны быть добавлены в реестр при регистрации сервера и, соответственно, удалены из него в результате обратной процедуры.

В нашем случае необходимо создать ключ HKEY_CLASSES_ROOT\*\ShellEx\ContextMenuHandlers\ ProbaShExt и присвоить его параметру по умолчанию значение, равное GUID нашего расширения.

GUID - это уникальный глобальный идентификатор, который автоматически генерируется средой при создании нового проекта с использованием ATL COM AppWizard, в ином случае можно использовать утилиту guidgen, входящую в состав SDK. При этом получаемое 128-битное значение можно считать уникальным в глобальном масштабе только в том случае, если на вашей машине установлена сетевая карта, и если эта карта не успела выйти из строя к моменту генерации GUID.

Строка из шестнадцатеричных цифр в фигурных скобках в файле ProbaShExt.rgs и есть GUID нового расширения, а сам файл необходимо подвергнуть незначительной доработке. Добавьте перед последней закрывающей фигурной скобкой следующие строки, в которых {11111111-2222-3333-4444-555555555555} необходимо заменить на GUID вашего проекта:

NoRemove *
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ProbaShExt = s '{11111111-2222-3333-4444-555555555555}'
}
}
}

Аббревиатура HKCR изменяемого файла обозначает ключ реестра HKEY_CLASSES_ROOT, в котором система хранит сведения о зарегистрированных файлах. NoRemove перед названием разделов говорит о том, что этот раздел не должен удаляться при отмене регистрации сервера, в отличие от тех, перед именами которых стоит ForceRemove. Знак равенства, стоящий после имени раздела ProbaShExt, и остальная часть строки обозначает присвоение параметру по умолчанию значения, указанного в одинарных кавычках.

Под NT и Windows 2000 необходимо также добавить сведения о регистрируемом расширении в список так называемых одобренных расширений. В противном случае обработчик никогда не будет загружаться для пользователей, не имеющих прав и привилегий администратора. Этот список сохраняется в реестре в разделе HKEY_LOCAL_MACHINE\ Software\Microsoft\Windows\Current Version\Shell Extensions\Approved. В нем также необходимо создать строковый параметр с именем, равным GUID регистрируемого сервера. Значение параметра не играет никакой роли. Однако, если заглянуть в реестр и просмотреть содержимое этого ключа, можно увидеть, что значением таких параметров обычно является описание сервера и его предназначения. Тем не менее, эти шедевры редко кто читает, и вас никто не заставляет поступать так же.

Для внесения сведений о новом сервере COM в список одобренных расширений можно воспользоваться функциями DllRegisterServer() и DllUnregisterServer(). В этом случае речь идет о простом доступе к реестру с использованием функций WinAPI, поэтому пример кода будет уже излишним. Особо ленивым можно предложить воспользоваться все тем же скриптом ProbaShExt.rgs и добавить в него еще несколько строчек под акронимом HKLM (для тех, кто еще не догадался сам, HKLM - это сокращение от HKEY_LOCAL_MACHINE).

После построения проекта среда разработки автоматически зарегистрирует расширение, а для его регистрации на других машинах можно воспользоваться утилитой regsvr32.exe, которая может оказаться полезной и в том случае, если вы захотите уничтожить следы присутствия пробного обработчика в вашей собственной системе. Запущенная с ключом /u и следующим за ним именем DLL, она вызовет DllUnregisterServer() и, таким образом, отменит регистрацию библиотеки.

Отладка расширения

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

Откройте диалоговое окно свойств проекта и в поле ввода "Executable for debug session" введите полный путь к Explorer'у. Под 2000 и NT, у тех, кто не поленился залезть в реестр и создать параметр DesktopProcess со значением, равным 1, для каждого нового окна проводника будет запускаться отдельный процесс. При закрытии этого окна используемая системой DLL будет автоматически выгружаться из памяти, поэтому у таких пользователей проблем с перестройкой проекта возникать не должно.

Немного сложнее обстоят дела у тех, кто работает под Windows 9x. Перед запуском отладчика необходимо завершить работу оболочки. Для этого в меню "Пуск" выберите "Завершение работы", нажмите Ctrl+Alt+Shift и щелкните на кнопке отмены. Об остановке функционирования Windows shell будет свидетельствовать исчезновение с экрана монитора панели задач. Теперь можно вернуться в MSVC и запустить отладку. После завершения отладочной сессии системный проводник можно запустить из командной строки, что восстановит обычную работу оболочки.

Опыт приходит с практикой. Создавая свои собственные расширения, старайтесь тестировать их, что называется, по полной программе под управлением разных версий Windows, так как не все типы расширений доступны в ее прежних версиях и не всегда одни и те же параметры, передаваемые системой в экспортируемые функции, несут одинаковую смысловую нагрузку как под NT, так и в 9x.

В заключение

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

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



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты