|
|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте, уважаемые подписчики!
Unicode и Windows9x/Me Автор: Paul Bludov"Nothing is impossible!" Professor Hubert J. Farnsworth Демонстрационный проект
Unicode98 (ATL ActiveX, 32k) Лирическое вступлениеОднажды, в пятницу вечером, я получил письмо, из которого следовало, что заказчик очень хочет, чтобы проект, над которым я работаю, мог бы быть запущен из Windows98. "А, может, Вам еще и поддержку Microsoft ® Windows ™ версии 2.0 подавай?", - подумал я, но, тем не менее, решил попытаться. Первое, что пришло мне на ум, это просто перекомпилировать все 30 модулей, из которых состоит проект, без директивы препроцессора _UNICODE. Идея здравая, но пришла в мою голову с опозданием месяцев в 6. К сожалению, некоторые из разработчиков, участвовавших в этом проекте, не понимают, как выяснилось, разницу между LPCTSTR и LPCWSTR. Некоторые даже умудрились использовать LPCTSTR при описании интерфейсов! Представляете, что получится, если код, реализующий некий интерфейс, трактует строки как двухбайтовые, а код, пользующийся этим интерфейсом, считает их однобайтовыми? Дело сильно осложнилось наличием нескольких библиотек, исходным кодом которых я не располагал, и, как следствие, не мог их пересобрать. Возможно, что если бы я располагал двумя-тремя неделями, я бы расставил по коду бесконечное количество перекладываний из пустого в порожнее и наоборот, но, увы. Времени у меня было в обрез, и я начал искать другой путь. Настало время разобраться с давным-давно вышедшей библиотекой поддержки уникода для Windows9x/Me. Библиотека UnicowsИдея, реализованная в этой библиотеке, довольно проста: весь API, рассчитанный на двухбайтовые строки, эмулируется специальными заглушками, преобразующими все строковые параметры из двухбайтовых в однобайтовые, затем вызывается реализованная в Windows9x/Me неуникодная функция, а результат снова перекладывается в двухбайтовые строки. Именно таким образом в WindowsNT реализована поддержка "старого", неуникодного API. В этой системе однобайтовые строки превращаются в двухбайтовые, вызывается соответствующая функция, а результат снова урезают до однобайтовых строк. Странно, что подобный механизм не был встроен в Windows9x/Me изначально. Unicows.dll занимает всего 200k и реализует почти 500 заглушек для работы с двухбайтовыми строками. Давайте попробуем эту замечательную библиотеку. Первая программа с использованием unicowsСначала создадим простой ATL проект и добавим в него ActiveX конторол. Теперь добавим поддержку уникода для Windows9x/Me. Процедура "прикручивания" unicows довольно сложная и интуитивно-непонятная. Но сводится она к тому, чтобы добавить Unicows.lib к списку прочих библиотек, причем непременно в самое начало:
Плюс нужно добавить вот такую строку куда-нибудь в StdAfx.cpp: #pragma comment(linker, "/nod:kernel32.lib /nod:advapi32.lib /nod:user32.lib /nod:gdi32.lib Компилируем, запускаем и... не работает! А как же! Я ведь совершенно забыл, что сначала нужно скопировать unicows.dll в системный каталог Windows. Инсталлируем unicows, запускаем... работает! Но, к сожалению, только Debug-версия. Release-версия никак не может создать окно для контрола. Небольшое расследование показало, что в этом виноват макрос _ATL_DLL, из-за которого CWindowImpl::Create вызывает функцию AtlModuleRegisterWndClassInfoW из ATL.DLL, а та, в свою очередь, обращается напрямую к RegisterClassExW из USER32.DLL. Вызов не попадает в unicows, потому что ATL.DLL ничего про нее не знает. Unicows подменяет вызовы только в тех модулях, в сборке которых участвовала unicows.lib
Проблема решается простым отключением _ATL_DLL. Это будет стоить всего лишь в 10к, на которые "подрастет" наш модуль. Если Вас не пугает необходимость статически линковать все необходимые библиотеки (типа MFC), можете дальше не читать. Впрочем, у Вас есть хорошая возможность расширить немного свой кругозор. Итак, продолжим. Как устроена unicowsВозникает вполне уместный вопрос: "А каким образом это все устроено"? Очень просто. Хитрые манипуляции с unicows.lib необходимы для того, чтобы подменить уникодные функции из модулей kernel32, advapi32, user32, gdi32, shell32, comdlg32, version, mpr, rasapi32, winmm, winspool, vfw32, secur32, oleacc, oledlg и sensapi на функции с аналогичными именами из unicows. А все функции из unicows.lib выглядят примерно так:
UNICOWSAPI ATOM WINAPI user32_RegisterClassExW_Thunk(IN CONST WNDCLASSEXW *lpwcx)
{
ResolveThunk("user32", "RegisterClassExW", RegisterClassExW,
Пусть Вас не пугает странное название этой функции. В .def файле она переименовывается просто в RegisterClassExW, который и находит линковщик.
ATOM WINAPI Unicows_RegisterClassExW (IN CONST WNDCLASSEXW *lpwcx)
{
// Делаем, что хотим
}
Тогда линковщик, найдя две разных функции, но с одинаковыми именами, отдаст предпочтение той, что находится в нашей программе.
До вызова ResolveThunk, значение RegisterClassExW совпадает с user32_RegisterClassExW_Thunk, а внутри этого вызова происходит изменение этого указателя на функцию из USER32, для WindowsNT/2k/XP, либо на Unicows_RegisterClassExW, для Windows9x/Me с установленной unicows.dll, либо на GodotFailRegisterClassExW, для Windows9x/Me без unicows.dll. В любом случае, user32_RegisterClassExW_Thunk ужи не будет никогда вызываться. Первый и последний вызов user32_RegisterClassExW_Thunk был осуществлен через указатель на эту функцию – RegisterClassExW, и значение по этому адресу было исправлено посредством вызова ResolveThunk. Интересно, что в случае WindowsNT/2k/XP загрузки unicows.dll в память не происходит. ResolveThunk и функции-заглушки находятся в нашей программе. Фактически, осуществляется отложенное связывание функций. Это означает, что никакой лишней работы в случае с уникодной версией Windows не будет. Unicows работает "прозрачно" в этих ОС. В Windows9x/Me имеет место небольшая задержка для инициализации unicows, не заметная, впрочем, на фоне общей "задумчивости" этих ОС. Альтернативное использование unicowsУ стандартного механизма подключения unicows есть большой недостаток: он бесполезен, если имеются уже готовые модули в виде DLL, а не в виде исходных файлов. Помните AtlModuleRegisterWndClassInfoW и как с ней пришлось бороться? Так вот, есть способ лучше. Можно загрузить DLL в память и поправить ее таблицу импорта (см Форматы РЕ и COFF объектных файлов). Для этого придется просмотреть все импортируемые функции и, если функция с таким же именем присутствует в unicows.dll, переправить обращение к этой функции в unicows.dll. Напишем две функции. Первая загружает в память процесса unicows.dll и настраивает таблицы экспортируемых функций. Вторая исправляет таблицу импорта указанного модуля. Листинг №1 Перенаправление функций в unicows.dll
#define MakePtr(cast, base, offset) (cast)((DWORD_PTR)(base) + (DWORD_PTR)(offset))
//@//////////////////////////////////////////////////////////////////////////
// Глобальные переменные
static BOOL g_bUnicodeOS = FALSE;
static HMODULE g_hModuleUnicows = NULL;
static LPWORD g_pdwOrd = NULL;
static LPDWORD g_pdwAddrs = NULL;
static LPDWORD g_pdwNames = NULL;
static DWORD g_dwNames = 0;
//@//////////////////////////////////////////////////////////////////////////
// Вспомогательная функция, возвращает адрес функции из unicows.dll
// с указанным именем, если такая имеется
static LPDWORD GetFunctionAddress(LPCSTR azName)
{
for (DWORD dwName = 0; dwName < g_dwNames; dwName++)
{
if (0 == ::lstrcmpiA(MakePtr(LPSTR, g_hModuleUnicows, g_pdwNames[dwName]), azName))
return MakePtr(LPDWORD, g_hModuleUnicows, g_pdwAddrs[g_pdwOrd[dwName]]);
}
return NULL;
}
//@//////////////////////////////////////////////////////////////////////////
// Вспомогательная функция, возвращает имя функции по ее номеру
// для указанного модуля
static LPCSTR GetNameFromOrdinal(HANDLE hModule, DWORD dwOrdinal)
{
if (!hModule)
return FALSE; // Нет такого модуля
// Находим таблицу экспорта
PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);
if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER))
|| IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
PIMAGE_NT_HEADERS pNTHeaders =
MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);
if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS))
|| IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
IMAGE_DATA_DIRECTORY& expDir =
pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
PIMAGE_EXPORT_DIRECTORY pExpDir =
MakePtr(PIMAGE_EXPORT_DIRECTORY, hModule, expDir.VirtualAddress);
LPWORD pdwOrd = MakePtr(LPWORD, hModule, pExpDir->AddressOfNameOrdinals);
LPDWORD pdwNames = MakePtr(LPDWORD, hModule, pExpDir->AddressOfNames);
dwOrdinal -= pExpDir->Base;
for (DWORD iName = 0; iName < pExpDir->NumberOfNames; iName++)
{
// Проверяем все номера по порядку
if (dwOrdinal == pdwOrd[iName])
return MakePtr(LPSTR, hModule, pdwNames[iName]);
}
return NULL;
}
//@//////////////////////////////////////////////////////////////////////////
// Функция инициализации unicows.dll
BOOL _UnicowsInit()
{
if (0 == (0x80000000 & ::GetVersion()))
{
// WinNT/2k/XP: Тут нам делать нечего
g_bUnicodeOS = TRUE;
return ::SetLastError(0), TRUE;
}
g_hModuleUnicows = ::LoadLibraryA("Unicows.dll");
if (!g_hModuleUnicows)
return FALSE; // Ошибка при загрузке Unicows.dll
// Находим таблицу экспорта
PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(g_hModuleUnicows);
if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER))
|| IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
PIMAGE_NT_HEADERS pNTHeaders =
MakePtr(PIMAGE_NT_HEADERS, g_hModuleUnicows, pDosHeader->e_lfanew);
if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS))
|| IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
IMAGE_DATA_DIRECTORY& expDir =
pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
PIMAGE_EXPORT_DIRECTORY pExpDir =
MakePtr(PIMAGE_EXPORT_DIRECTORY, g_hModuleUnicows, expDir.VirtualAddress);
// Запонимаем таблицу имен и адресов
g_pdwOrd = MakePtr(LPWORD, g_hModuleUnicows, pExpDir->AddressOfNameOrdinals);
g_pdwNames = MakePtr(LPDWORD, g_hModuleUnicows, pExpDir->AddressOfNames);
g_pdwAddrs = MakePtr(LPDWORD, g_hModuleUnicows, pExpDir->AddressOfFunctions);
g_dwNames = pExpDir->NumberOfNames;
return TRUE;
}
//@//////////////////////////////////////////////////////////////////////////
// Функция перенаправляющая вызовы в unicows.dll
BOOL _UnicowsRebindImports(HMODULE hModule)
{
if (g_bUnicodeOS)
return ::SetLastError(0), FALSE;
// Находим таблицу импорта
PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);
if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER))
|| IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
PIMAGE_NT_HEADERS pNTHeaders =
MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);
if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS))
|| IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE;
IMAGE_DATA_DIRECTORY& impDir =
pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR pImpDesc =
MakePtr(PIMAGE_IMPORT_DESCRIPTOR, hModule, impDir.VirtualAddress),
pEnd = pImpDesc + impDir.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1;
while(pImpDesc < pEnd)
{
// Для каждого импортируемого модуля
if (pImpDesc->OriginalFirstThunk)
{
PIMAGE_THUNK_DATA pNamesTable =
MakePtr(PIMAGE_THUNK_DATA, hModule, pImpDesc->OriginalFirstThunk);
while(pNamesTable->u1.AddressOfData)
{
LPCSTR azFunctionName;
if (IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal))
{
// Функция импортируется по номеру, вычисляем имя
azFunctionName = GetNameFromOrdinal(
::GetModuleHandleA(MakePtr(LPSTR, hModule, pImpDesc->Name))
, IMAGE_ORDINAL(pNamesTable->u1.Ordinal));
if (!azFunctionName)
continue;
}
else
{
PIMAGE_IMPORT_BY_NAME pName = MakePtr(PIMAGE_IMPORT_BY_NAME,
hModule,
pNamesTable->u1.AddressOfData); azFunctionName = LPCSTR(pName->Name);
} LPDWORD
dwAddr =
GetFunctionAddress(azFunctionName); <
BR
> if (dwAddr)
{
// Эта функция есть в Unicows.dll
LPDWORD *pProc = MakePtr(LPDWORD *, pNamesTable,
pImpDesc->FirstThunk - pImpDesc->OriginalFirstThunk);
// Возможно, кто-то это уже сделал
if (*pProc != dwAddr)
{
// Отключаем защиту
DWORD dwOldProtect = 0;
if (::VirtualProtect(pProc, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect))
{
*pProc = dwAddr;
// Восстанавливаем защиту
::VirtualProtect(pProc, sizeof(DWORD), dwOldProtect, &dwOldProtect);
}
}
}
}
}
pImpDesc++;
}
return TRUE;
}
С помощью такого кода мы можем не только переправить "на ходу" вызовы уникодных функций из нашего модуля, но и из любого чужого, например ATL.DLL. Проблема запаздыванияК сожалению, Windows, при загрузке DLL, автоматически вызывает функцию DllMain() с параметром DLL_PROCESS_ATTACH и этот вызов произойдет до того, как мы сможем поправить таблицу импорта этой DLL. В случае с ATL.DLL ничего страшного не происходит, версия этой библиотеки для Windows9x/Me не уникодная, но экспортирует несколько уникодных функций. Для истинно уникодных DLL, к исходному коду которых нет доступа, у меня сработал такой трюк: сначала я вручную вызывал DllMain с параметром DLL_PROCESS_DETACH, затем настраивал таблицу импорта и снова вызывал DllMain, но уже с параметром DLL_PROCESS_ATTACH. К счастью, хотя эти DLL и не могли как следует проинициализироваться при первом вызове DllMain загрузчиком Windows, они делали это молча. Не выдавая пугающих сообщений об ошибках. Возможно, что Вам повезет меньше. Тогда путь только один: писать свой собственный загрузчик, загружающий нужный модуль в память, настраивающий все нужные таблицы, поддержку уникода и лишь потом вызывающий DllMain. Интересно, что ::LoadLibraryEx() умеет загружать модули в память не вызывая DllMain, но... только в WindowsNT/2k/XP! Windows9x/Me флаг DONT_RESOLVE_DLL_REFERENCES не поддерживает. Уникодная версия atl.dll, например, выдает страшное предупреждение и возвращает FALSE в DllMain, завершая работу приложения. Подобная, но, к счастью, разрешимая проблема имеется и для EXE приложений. Дело в том, что выполнение программы начинается не с WinMain, а с некоторой функции, инициализирующей CRT, а затем уже вызывающей WinMain. Подробнее здесь. Все, что нам нужно, это задать свою точку входа в программу:
и написать эту самую _UnicowsEntry: Листинг №2 Собственная точка входа в программу
extern "C" void _UnicowsEntry()
{
if (_UnicowsInit())
{
_UnicowsRebindImports(::GetModuleHandleA(NULL));
_UnicowsRebindImports(::GetModuleHandleA("ATL"));
}
else
{
::MessageBox(NULL, _T("Ошибка инициализации UNICOWS.DLL"), NULL, MB_OK | MB_ICONSTOP);
::ExitProcess(-1);
}
#ifdef _UNICODE
void wWinMainCRTStartup();
wWinMainCRTStartup();
#else // _UNICODE
void WinMainCRTStartup();
WinMainCRTStartup();
#endif // _UNICODE
}
Проблема отложенной загрузкиОтложенная загрузка (подробнее) означает, что некоторые функции не присутствуют в таблице импорта, а связываются с нужными модулями по мере необходимости. Этот механизм очень похож на тот, который реализуют заглушки unicows. Как таковой, проблемы не возникает. Все, что нам нужно – это доработать маленько наши функции и заодно подменить ::GetProcAddress() на нашу собственную функцию, а там мы сначала поищем в unicows.dll, а если не найдем нужной функции, то отправим вызов в настоящую ::GetProcAddress(). Листинг №3 Подмена ::GetProcAddress()
static FARPROC WINAPI _UnicowsGetProcAddress(IN HMODULE hModule, IN LPCSTR azName);
static FARPROC (WINAPI *_RealGetProcAddress)(IN HMODULE hModule, IN LPCSTR azName);
static LPDWORD GetFunctionAddress(LPCSTR azName)
{
if (0 == ::lstrcmpiA("GetProcAddress", azName))
return LPDWORD(_UnicowsGetProcAddress);
// Unicows
for (DWORD dwName = 0; dwName < g_dwNames; dwName++)
{
if (0 == ::lstrcmpiA(MakePtr(LPSTR, g_hModuleUnicows, g_pdwNames[dwName]), azName))
return MakePtr(LPDWORD, g_hModuleUnicows, g_pdwAddrs[g_pdwOrd[dwName]]);
}
return NULL;
}
static FARPROC WINAPI _UnicowsGetProcAddress(IN HMODULE hModule, IN LPCSTR azName)
{
if (IS_INTRESOURCE(azName))
azName = GetNameFromOrdinal(hModule, WORD(azName));
LPDWORD dwAddr = GetFunctionAddress(azName);
if (dwAddr)
return FARPROC(dwAddr);
return _RealGetProcAddress(hModule, azName);
}
Проблемные APIНекоторые функции не имеют заглушек для уникодной версии в Windows9x/Me. Такими функциям, например, являются ::GetAltTabInfo() и ::RealGetWindowClass(). USER32.DLL из WindowsNT/2k/XP экспортирует по три функции для каждой из них, например, GetAltTabInfo, GetAltTabInfoA и GetAltTabInfoW. В USER32.DLL из Windows98 есть только GetAltTabInfo. USER32.DLL из Windows95 не имеет такой функции вообще. Интересно, что в WinUser.h определены именно GetAltTabInfoA и GetAltTabInfoW, таким образом, даже если ваше приложение скомпилировано без поддержки уникода, Windows9x все равно не сможет его загрузить. Тем не менее, эти функции есть в unicows.dll, и, если воспользоваться стандартным способом подключения unicows, приложение будет работать. Для альтернативного способа нам придется воспользоваться явным (GetProcAddress) или отложенным (delayload) связыванием. Оба пути приведут нас, в конце концов, в unicows.dll, где имеются уникодные версии этих функций. С Module32First/Module32Next, Process32First/Process32Next из KERNEL32.DLL похожая ситуация. В WindowsNT/2k/XP есть Module32First и Module32FirstW, в Widows98 только Module32First. В Unicows.dll Module32FirstW также отсутствует. Для этих функций, впрочем, можно легко отказаться от уникода. Для этого можно просто отменить макрос UNICODE перед включением TlHelp32.h, а затем включить его обратно.
#ifdef UNICODE
#undef UNICODE
#include <Tlhelp32.h>
#define UNICODE
#else
#include <Tlhelp32.h>
#endif
ЗаключениеНа прощание хочу обратить Ваше внимание на тот факт, что unicows не обеспечивает настоящей поддержки уникода под Windows9x/Me, Вам не удастся вывести одновременно японские и русские буквы, как в WindowsNT/2k/XP, но позволяет не компилировать и отлаживать две версии одной программы: с уникодом и без. В любом случае, если вам нужны одновременно и китайские и арабские буквы, обойтись без WindowsNT/2k/XP не получится. Ссылки по темеMicrosoft Layer for Unicode on Windows 95/98/Me Systems Q259403 (загрузка Atl.dll с сайта Майкрософт) Unicows.dll version 1.0 Atl.dll version 3.0
Как реализовать диалог с фоновым изображением? Автор: Сергей ПимановЧтобы реализовать диалог с фоновым изображением, необходимо проделать следующие шаги. Сначала добавляем член-переменные в класс диалога. Они понадобятся нам в дальнейшем. CDC m_dcTemp; CBitmap m_cBmp; UINT m_iBmpHeight; UINT m_iBmpWidth; Битмап, содержащий фоновое изображение, можно загружать как из файла, так и из ресурсов. Во втором случае необходимо добавить в ресурсы соответствующую картинку (например, с идентификатором IDB_BITMAP1). Далее загружаем картинку в обработчике OnInitDialog(). Для случая с внешним файлом пишем: m_hBmp=(HBITMAP) LoadImage(0,"picture.bmp",IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_DEFAULTSIZE); m_cBmp.Attach(hBmp); В случае с картинкой из ресурсов: m_cBmp.LoadBitmap(IDB_BITMAP1); Затем для обоих случаев: BITMAP Bmp; CClientDC dc(this); m_dcTemp.CreateCompatibleDC(&dc); m_dcTemp.SelectObject(m_cBmp); m_cBmp.GetBitmap(&Bmp); m_iBmpHeight=Bmp.bmHeight; m_iBmpWidth=Bmp.bmWidth; Теперь создаём обработчик сообщения WM_ERASEBKGND, в котором наша картинка будет отрисовываться. Чтобы добавить этот обработчик с помощью ClassWizard, необходимо предварительно переключиться на вкладку Class Info и установить для класса диалога Message filter: Window. BOOL CTestDlg::OnEraseBkgnd(CDC* pDC)
{
RECT rect;
GetClientRect(&rect);
int CX=rect.right/m_iBmpWidth;
int CY=rect.bottom/m_iBmpHeight;
int Y=0;
for (int i=0;i<=CY;i++)
{
int X=0;
for(int j=0;j<=CX;j++)
{
pDC->BitBlt(X,Y,X+m_iBmpWidth,Y+m_iBmpHeight,&m_dcTemp,0,0,SRCCOPY);
X+=m_iBmpWidth;
}
Y+=m_iBmpHeight;
}
return TRUE;
}
Если битмап меньше окна, то он будет размножен (черепица). Возможно реализовать и другие варианты отображения картинки. Кроме черепицы, ещё два из них (центрирование и "растягивание" картинки по размеру окна) реализовано в демонстрационной программе BkDlg. Вот и всё. Когда битмап больше не нужен, его следует удалить, но об этом за нас позаботится деструктор класса CBitmap.
Это все на сегодня. Пока! Алекс Jenter jenter@rsdn.ru |