Данная статья является введением в программирование на WinAPI. Важно понимать, что она носит скорее информационный характер, чем служит примером кода для реальных приложений. Дело в том, что WinAPI создавался для языка С, и имеет целый ряд недостатков в применении, как-то: невысокая безопасность, большой объем ручного кодирования для решения простейших задач, С-стиль, плохо выглядящий в C++-приложениях. Практически любое средство разработки на языке C++ для Windows включает те или иные высокоуровневые C++-библиотеки (MFC, ATL/WTL для Visual C++, VCL для C++ Builder), значительно упрощающие разработку Windows-приложений, и делающие ее более безопасной.
Предисловие
Эта статья посвящена описанию программирования приложений на «чистом» Win32 API. Она написана в основном для начинающих программистов, пишущих программы на Visual C++ 6 с использованием библиотеки MFC, но я надеюсь, может пригодиться и более опытным людям.
First Blood
После создания нового проекта Win32 Application, в зависимости от выбранных опций, мастер генерирует стартовый код. Из этого кода программисту впоследствии, и придется писать программу. Создавая новый проект Win32 Application, выберите в окне мастера опцию An empty project и добавьте в раздел Source Files новый файл с расширением .cpp.
Язык Си — Как создать и открыть окно с помощью функций WinAPI.
В этом файле добавьте функцию WinMain вида:
#include int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) < MessageBox(NULL, «Простейшая программа!»,»WinAPI App», 0); //сообщение return 0; >
Вот и готова первая программа на WinAPI. Она выводит сообщение, после чего завершает свою работу. Обратите внимание на параметры функции WinMain :
- HINSTANCE hInstance – дескриптор экземпляра приложения. Этот дескриптор содержит адрес начала кода программы в ее адресном пространстве. Дескриптор hInstance чаще всего требуется функциям, работающим с ресурсами программы.
- HINSTANCE hPrevInstance – дескриптор предыдущего экземпляра приложения. Этот дескриптор остался от старых версий Windows — скорее всего, вам он никогда не пригодится.
- LPSTR lpCmdLine – указатель на начало командной строки, введенной при запуске программы.
- int nCmdShow – это значение содержит желаемый вид окна (например, свернутый или развернутый)
Значение, которое возвращается функцией WinMain (тип int ) – код завершения программы. Принято, что если программа завершила свое выполнение без ошибок, возвращается 0.
Функция WinMain – первая функция, которая выполнятся в программе (ее еще называют «точка входа» или «entry point»). С нее все начинается, и ею (желательно) все должно закончиться.
ПРИМЕЧАНИЕ
Функция WinMain – это первая функция, которую вы можете увидеть и заполнить кодом. На самом деле до этой функции выполняется достаточно много кода из библиотеки C++
You have a Message!
Программисты, незнакомые с программированием на WinAPI, спросят: «Что с этим делать?!», — или: «Где создавать CDialog?». В данном случае ответ прост – нигде! В нашем проекте нет класса CDialog или, предположим, CButton – ведь эта статья посвящена тому, как обойтись без них.
На что обратить внимание при изучении WIN API
В Windows при каждом событии, произошедшем в системе, отсылается «сообщение Windows» («windows message»). Эти сообщения уведомляют программу о событиях в системе, а программа в свою очередь, может на них реагировать. Сообщения может отсылать не только Windows, но и сами приложения. Это является одним из способов организации связи между процессами в системе. Конечно, программа может отсылать сообщения и самой себе.
СОВЕТ
Сообщение можно отослать функцией SendMessage или ее асинхронным аналогом PostMessage.
Для приема сообщений в программе должен находиться «цикл сообщений» («message loop») который обычно выглядит так:
//цикл сообщений приложения MSG msg = ; //структура сообщения int iGetOk = 0; //переменная состояния while ((iGetOk = GetMessage( //если GetMessage вернул ошибку — выход if (iGetOk == -1) return 3; TranslateMessage( DispatchMessage( >
Функция GetMessage принимает следующие параметры:
- LPMSG lpMsg – указатель на структуру сообщения, в которую GetMessage вернет результат.
- HWND hWnd – описатель окна, от которого GetMessage примет сообщение ( NULL означает, что GetMessage принимает сообщения от всех окон, принадлежащих потоку).
- UINT wMsgFilterMin – наименьший идентификатор сообщения, которое примет GetMessage.
- UINT wMsgFilterMax – наибольший идентификатор сообщения, которое примет GetMessage (если в значениях параметров wMsgFilterMin и wMsgFilterMax передать 0, функция будет принимать ВСЕ сообщения).
Функция GetMessage не отдает управление программе, пока не придет какое-либо сообщение. Если пришедшее сообщение – WM_QUIT , функция GetMessage вернет 0 . Тогда цикл прервется, и программа завершит свою работу. При любом другом сообщении функция GetMessage возвращает значение больше нуля, и начинатся выполнение тела цикла. При ошибке GetMessage возвращает -1.
СОВЕТ
Сообщение WM_QUIT лучше посылать с помощью специальной функции PostQuitMessage(int iExitCode). Эта функция отошлет сообщение WM_QUIT, а в параметре wParam передаст код завершения программы, указанный в iExitCode.
Функция DispatchMessage должна вызвать «функцию обработки сообщений». В простейшем варианте она выглядит так:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) < // выборка и обработка сообщений switch (message) < case WM_LBUTTONUP: //реакция на сообщение MessageBox(hWnd,»Вы кликнули!»,»событие»,0); break; case WM_DESTROY: //реакция на сообщение PostQuitMessage(0); break; //все необработанные сообщения обработает сама Windows default: return DefWindowProc(hWnd, message, wParam, lParam); // switch (message) > return 0; > // конец функции обработчика сообщений
При вызове этой функции ей передаются следующие параметры:
- HWND hWnd – описатель окна, от которого пришло сообщение.
- UINT message – идентификатор сообщения.
- WPARAM wParam и LPARAM lParam – параметры сообщения.
Функция обработки сообщений не обязательно должна иметь имя WndProc . Таких функций в программе может быть несколько, но их прототипы обязательно должны выглядеть так:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
При вызове этой функции DispatchMessage передает в параметре message идентификатор сообщения. По этому идентификатору производится выборка и выполняется какое-либо действие ( «реакция на сообщение» ).
В Windows существует очень много сообщений! Писать обработчики для всех сообщений – нереальная задача. Чтобы Windows сама обработала бесполезное для вас сообщение, необходимо вызвать функцию DefWindowProc :
LRESULT DefWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Желательно передавать все необработанные сообщения этой функции, а результат ее выполнения возвращать при выходе из WndProc . Это очень важно, так как от обработки некоторых сообщений Windows ждет возврата конкретных результатов или действий.
Одной функцией обработки сообщений могут пользоваться несколько окон, но для одного окна может существовать только одна функция обработки сообщений! Как же система определяет, какой именно функцией обработки сообщения пользоваться для конкретного окна и где она находится?! За это отвечает «класс окна» («window class»).
CLASSные окна
При создании нового окна ему присваивается «Класс окна» (window class). Класс окна задает оконную функцию, используемую по умолчанию. Кроме этого, класс окна задает другие параметры окна, такие, как стиль, меню окна, цвет рабочей области и т.д. Разные классы окон могут указывать на одну и ту же функцию обработки сообщений. Для создания класса его необходимо зарегистрировать.
Итак, регистрация! За нее отвечает функция RegisterClass . В ее параметре необходимо передать указатель на структуру WNDCLASS . Обычно для заполнения структуры и вызова RegisterClass создают отдельную функцию. Но это — дело вкуса.
Вот простейший пример такой функции:
ATOM RegMyWindowClass(HINSTANCE hInst, LPSTR lpzClassName) < WNDCLASS wcWindowClass = ; //адрес ф-ции обработки сообщений wcWindowClass.lpfnWndProc = (WNDPROC)WndProc; //стиль окна wcWindowClass.style = CS_HREDRAW|CS_VREDRAW; //дискриптор экземпляра приложения //название класса wcWindowClass.hInstance = hInst; wcWindowClass.lpszClassName = lpzClassName; //загрузка курсора wcWindowClass.hCursor = LoadCursor(NULL, IDC_ARROW); //загрузка цвета окон wcWindowClass.hbrBackground = (HBRUSH)COLOR_APPWORKSPACE; //регистрация класса return RegisterClass( >
- WNDPROC lpfnWndProc – адрес функции обработки сообщений.
- HINSTANCE hInstance – уже знакомая переменная, описывающая экземпляр.
- LPCTSTR lpszClassName – имя нового класса.
- HICON hCursor – описатель курсора мыши.
- HBRUSH hbrBackground – цвет рабочей области окна.
Функция RegisterClass возвращает уникальный «описатель класса окна» типа ATOM . Если при регистрации класса произошла ошибка, это значение будет равно нулю. Чтобы узнать, что произошло, можно вызвать функцию GetLastError() .
Существует также функция RegisterClassEx. Это аналог функции RegisterClass с возможностью присвоения окнам маленькой иконки. При работе с этой функцией необходимо пользоваться структурой WNDCLASSEX.
ПРИМЕЧАНИЕ
Если вы решились работать с GUI Windows вручную, то пользоваться нужно именно RegisterClassEx, поскольку приложение, не имеющее маленькой иконки, сегодня выглядит в Windows как минимум странно. – прим.ред.
Следите, чтобы имя вашего класса не совпадало с именами системных классов (например: button или edit).
Я описал не всю структуру. Все незаполненные поля, которых нет в примере, сейчас равны нулю. Об их значениях можно узнать из MSDN.
Сообщения от окон, созданных на базе класса, зарегистрированного описанной выше функцией RegMyWindowClass, будут обрабатываться функцией с именем WndProc. Чтобы функция WndProc поняла, от какого именно окна пришло сообщение, ей передается уникальный описатель окна HWND .
Our Windows
На вашем месте у меня возникло бы желание увидеть те самые пресловутые окна, из-за которых столько шума. Окно в Windows создается функцией CreateWindow . Вот ее прототип:
HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD wStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);
Как видите, у функции множество параметров:
- LPCTSTR lpClassName – имя класса для создаваемого окна (это имя использовалось при регистрации класса).
- LPCTSTR lpWindowName – имя окна.
- DWORD dwStyle – стиль окна.
- int x – позиция по горизонтали верхнего левого угла окна.
- int y – позиция по вертикали.
- int nWidth – ширина окна.
- int nHeight – высота окна.
- HWND hWndParent – используется для создания «дочернего окна» («child window»). Сюда передается описатель «родительского окна» («parent window»).
- HMENU hMenu – описатель меню (если hMenu равно нулю, используется меню класса, указанного в lpClassName ).
- HINSTANCE hInstance – экземпляр приложения.
- LPVOID lpParam – указатель на пользовательский параметр окна. Этот указатель со всеми остальными параметрами функции CreateWindow будет занесен в структуру CREATESTRUCT . В сообщениях WM_CREATE или WM_NCCREATE параметр lParam будет содержать указатель на эту структуру.
Функция CreateWindow возвращает уникальный описатель окна HWND . Если функция вернула ноль, значит, во время создания окна произошла ошибка. Какая именно, можно узнать, вызвав функцию GetLastError .
ПРИМЕЧАНИЕ
Существует также функция CreateWindowEx, в которой дополнительно присутствует параметр dwExStyle. С его помощью можно создать окно с дополнительными стилями.
План полета
Итак, сейчас я упрощенно расскажу, что же произойдет, если щелкнуть по окну левой кнопкой мыши.
- Пользователь нажимает левую кнопку мыши в то время когда курсор мыши находится над рабочей областью окна.
- Windows помещает сообщение WM_LBUTTONDOWN в очередь потока.
- Цикл обработки сообщения должен вынуть сообщение с помощью функции GetMessage и передать его на обработку функции DispatchMessage .
- Функция DispatchMessage находит окно, которому предназначено сообщение и помещает сообщение в его очередь.
- Функция окна обрабатывает сообщение WM_LBUTTONDOWN и возвращает результат.
- Тело цикла заканчивается, и управление снова передается функции GetMessage для ожидания новых сообщений.
Итого
WinMain, регистрация класса, цикл сообщений, функция обработки сообщений, создание окна. Как все это связать?! Вот код, который объединяет все написанное выше в одну программу:
#include // объявление функций LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); ATOM RegMyWindowClass(HINSTANCE, LPCTSTR); ////////////////////////////////////////////////////////////////////////// // функция вхождений программы WinMain int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) < // имя будущего класса LPCTSTR lpzClass = TEXT(«My Window Class!»); // регистрация класса if (!RegMyWindowClass(hInstance, lpzClass)) return 1; // вычисление координат центра экрана RECT screen_rect; GetWindowRect(GetDesktopWindow(), // разрешение экрана int x = screen_rect.right / 2 — 150; int y = screen_rect.bottom / 2 — 75; // создание диалогового окна HWND hWnd = CreateWindow(lpzClass, TEXT(«Dialog Window»), WS_OVERLAPPEDWINDOW | WS_VISIBLE, x, y, 300, 150, NULL, NULL, hInstance, NULL); // если окно не создано, описатель будет равен 0 if(!hWnd) return 2; // цикл сообщений приложения MSG msg = ; // структура сообщения int iGetOk = 0; // переменная состояния while ((iGetOk = GetMessage( if (iGetOk == -1) return 3; // если GetMessage вернул ошибку — выход TranslateMessage( DispatchMessage( > return msg.wParam; // возвращаем код завершения программы > ////////////////////////////////////////////////////////////////////////// // функция регистрации класса окон ATOM RegMyWindowClass(HINSTANCE hInst, LPCTSTR lpzClassName) < WNDCLASS wcWindowClass = ; // адрес ф-ции обработки сообщений wcWindowClass.lpfnWndProc = (WNDPROC)WndProc; // стиль окна wcWindowClass.style = CS_HREDRAW|CS_VREDRAW; // дискриптор экземпляра приложения wcWindowClass.hInstance = hInst; // название класса wcWindowClass.lpszClassName = lpzClassName; // загрузка курсора wcWindowClass.hCursor = LoadCursor(NULL, IDC_ARROW); // загрузка цвета окон wcWindowClass.hbrBackground = (HBRUSH)COLOR_APPWORKSPACE; return RegisterClass( // регистрация класса > ////////////////////////////////////////////////////////////////////////// // функция обработки сообщений LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) < // выборка и обработка сообщений switch (message) < case WM_LBUTTONUP: // реакция на сообщение MessageBox(hWnd, TEXT(«Вы кликнули!»), TEXT(«событие»), 0); break; case WM_DESTROY: PostQuitMessage(0); // реакция на сообщение break; default: // все сообщения не обработанные Вами обработает сама Windows return DefWindowProc(hWnd, message, wParam, lParam); > return 0; >
Вот, в принципе, и все! Это полноценное приложение на WinAPI.
Программа регистрирует класс, создает окно этого класса и обслуживает сообщение WM_LBUTTONUP (оно приходит по событию отпускания левой кнопки мыши), показывает окно, и после обработки сообщения снова возвращается в цикл сообщений, находящийся в WinMain .
PS
Признаю, что данная статья и программа опускает очень много деталей! Многие вещи были не раскрыты (например, остальные переменные структуры WNDCLASS ). Все это сделано для того, чтобы максимально упростить статью и уменьшить код программы.
Буду рад получить отзывы и критику.
Эта статья опубликована в журнале RSDN Magazine #4-2005. Информацию о журнале можно найти здесь
Источник: www.rsdn.org
Windows API
Windows API ( Шаблон:Lang-en ) — общее наименование целого набора базовых функций интерфейсов программирования приложений операционных систем семейств Microsoft Windows корпорации «Майкрософт». Является самым прямым способом взаимодействия приложений с Windows. Для создания программ, использующих Windows API, «Майкрософт» выпускает
Общие сведения [ ]
Windows API спроектирован для использования в языке прикладных программ , предназначенных для работы под управлением операционной системы MS Windows. Работа через Windows API — это наиболее близкий к операционной системе способ взаимодействия с ней из прикладных программ. Более низкий Windows Driver Model .
Для облегчения программирования под Windows, в компании Microsoft и сторонними разработчиками было предпринято множество попыток создать библиотеки и среды программирования, частично или полностью скрывающие от программиста особенности Windows API, и предоставляющие ту или иную часть его возможностей в более удобном виде. В частности, сама Microsoft в разное время предлагала библиотеки Active Template Library (ATL)/Windows Template Library (WTL), Microsoft Foundation Classes (MFC), WinForms /WPF, TXLib. Компания Borland (ныне Embarcadero, её преемник в части средств разработки) предлагала Qt , Tk и многие другие. Весомая часть этих библиотек сконцентрирована на облегчении программирования графического интерфейса пользователя.
Для облегчения переноса на другие платформы программ, написанных с опорой на Windows API, сделана библиотека Wine.
Версии [ ]
- Win16 — первая версия WinAPI для 16-разрядных версий Windows. Изначально назывался Windows API, позднее был ретроспективно переименован в Win16 для отличия от Win32. Описан в стандарте ECMA-234 .
- Win32 — 32-разрядный API для современных версий Windows. Самая популярная ныне версия. Базовые функции реализованы в динамически подключаемых библиотеках kernel32.dll и advapi32.dll ; базовые модули графического интерфейса пользователя — в user32.dll и gdi32.dll . Win32 появился вместе с Windows NT и затем был перенесён в несколько ограниченном виде в системы серии Windows 9x. В современных версиях Windows, происходящих от Windows NT, работу Win32 GUI обеспечивают два модуля: csrss.exe (процесс исполнения клиент-сервер), работающий в ядро — ntoskrnl.exe .
- Win32s — подмножество Win32, устанавливаемое на семейство 16-разрядных систем Windows 3.x и реализующее ограниченный набор функций Win32 для этих систем.
- Win64 — 64-разрядная версия Win32, содержащая дополнительные функции Windows на платформах IA-64 .
См. также [ ]
- Microsoft .NET.
- Wine — свободная кроссплатформенная реализация Windows API.
Источник: microsoft.fandom.com
Разбираемся в WinAPI
Эта статья адресована таким же, как и я новичкам в программировании на С++ которые по воле случая или по желанию решили изучать WinAPI.
Хочу сразу предупредить:
Я не претендую на звание гуру по C++ или WinAPI.
Я только учусь и хочу привести здесь несколько примеров и советов которые облегчают мне изучение функций и механизмов WinAPI.
В данной статье я предполагаю что вы уже достаточно ознакомились с С++, что бы уметь создавать классы и перегружать для них различные операторы и что вы уже «прятали» какие-то свои механизмы в класс.
Создание и использование консоли
Для отладки Win32 приложения или просто для того что посмотреть как оно там всё внутри происходит я всегда пользуюсь консолью.
Так как вы создаете GUI приложение, а не консольное, то консоль не подключается. Для того что бы её вызвать в недрах интернета был найден вот этот код
if (AllocConsole())
<
int hCrt = _open_osfhandle((long)GetStdHandle(STD_OUTPUT_HANDLE), 4);
*stdout = *(::_fdopen(hCrt, «w»));
::setvbuf(stdout, NULL, _IONBF, 0);
*stderr = *(::_fdopen(hCrt, «w»));
::setvbuf(stderr, NULL, _IONBF, 0);
std::ios::sync_with_stdio();
>
Для удобства советую обернуть его в функцию. Например:
void CreateConsole()
if (AllocConsole())
<
int hCrt = _open_osfhandle((long)GetStdHandle(STD_OUTPUT_HANDLE), 4);
*stdout = *(::_fdopen(hCrt, «w»));
::setvbuf(stdout, NULL, _IONBF, 0);
*stderr = *(::_fdopen(hCrt, «w»));
::setvbuf(stderr, NULL, _IONBF, 0);
std::ios::sync_with_stdio();
>
Вызванная консоль работает только в режиме вывода и работает он также как и в консольных приложениях. Выводите информацию как и обычно — cout/wcout.
Для работоспособности данного кода необходимо включить в прект следующие файлы:
#include
#include #include
и включить пространство имен std в глобальное пространство имён:
using namespace std;
Конечно же, если вы не хотите этого делать, то просто допишите std:: ко всем сущностям которые в ней находятся.
Наследование объектов для вывода и арифм. операций
При создании и изучении самих «окошек» мне всегда требовалось выводить в консоль какое-нибудь значение.
Например:
Вы получаете размер клиентской области окна с помощью функции GetClientRect куда как параметр передается адрес объекта структуры RECT, что бы заполнить этот объект данными. Если вам нужно узнать размер полученной клиентский области вы просто можете вывести его в уже подключённую консоль
Но делать так каждый раз (особенно если вам часто приходиться делать что-то подобное) очень неудобно.
Здесь нам на помощь приходит наследование.
Создайте класс который открыто наследуется от структуры RECT и перегрузите оператор вывода Например:
Теперь просто выводите обьект с помощью cout/wcout:
И вам в удобном виде будет выводиться всё так, как вам требуется.
Так же вы можете сделать с любыми нужными вам операторами.
Например, если надо сравнивать или присваивать структуры (допустим тот же RECT или POINT) — перегрузите operator==() и operator=() соответственно.
Если хотите реализовать оператор меньше < что бы быстро сравнивать размеры окна и т.д. перегрузите operatorТак вы можете делать, я предполагаю, почти с любыми структурами и самое главное, что все функции которые работают с обычным объектом структуры RECT так же хорошо будут работать и с его наследником.
И еще рекомендую всю эту красоту вынести в отдельный подключаемый файл и использовать при необходимости.
Свой класс
Не знаю как у остальных, но так как я совсем зелёный, я решил для каждой изученной функции или для каждой главы/под главы книги создавать новый проект, что бы всё было по полочкам и можно было в любой момент вернуться и освежить в памяти необходимые моменты.
Так как в WinAPI даже для создания простейшего окна нужно заполнить структуру класса, зарегистрировать её и написать тривиальную оконную процедуру, я после третьего или четвертого проекта вспомнил что я всё таки на С++ пишу.
В итоге я всё спрятал в простенький класс. Хендл окна, его имя, имя класса, адрес оконной процедуры, класс окна (WNDCLASS) всё спрятано в private секцию класса.
Для их получения достаточно описать простенькие методы-Get’еры, к примеру:
HWND GetHWND()
LPCTSTR GetClsName() и т.д.
Заполнение и регистрация оконного класса, создание самого окна и его показ производиться в конструкторе.
Для удобства можно перегрузить конструктор, а заполнение и регистрацию оконного класса спрятать в отдельную private функцию класса и вызывать в каждом из конструкторов. Удобство перегрузки состоит в том, что мне иногда необходимо создать совсем простенькое окно и я вызываю конструктор с двумя параметрами — имя окна и hinstance приложения.
В другой раз мне нужно создать окно с особыми размерами, не с дефолтной процедурой окна и с каким-то другим определённым стилем — я вызываю конструктор с сопутствующими параметрами.
Этот класс у меня определён в отдельно подключаемом файле, который лежит в include папке IDE.
Шаблон такого класса:
class BaseWindow
WNDCLASSEX _wcex;
TCHAR _className[30];
TCHAR _windowName[40];
HWND _hwnd;
bool _WindowCreation();
public:
BaseWindow(LPCTSTR windowName,HINSTANCE hInstance,DWORD style,UINT x,UINT y,UINT height,UINT width);
BaseWIndow(LPCTSTR windowName,HINSTANCE hInstance);
const HWND GetHWND()const
LPCTSTR GetWndName()const
>;
Один раз хорошенько продумав и написав такой класс вы облегчите себе жизнь и будете больше времени уделять обучению и оттачиванию навыков чем написанию одного и того же каждый раз. Тем более, я считаю это очень полезно — самому сделать такой класс и дополнять его по необходимости.
P.S.
Всё описанное справедливо для:
Платформа — Windows 7 32 bit
IDE — Visual Studio 2010
Может у кого-то эти советы будут вызывать смех и иронию, но всё-таки мы все когда-то в чём-то были новичками/стажерами/junior’ами.
Прошу к посту отнестись с понимаем. Конструктивная критика, конечно же приветствуется.
Источник: habr.com