Введение в программирование под Windows.
Автор: Антон В. Звягинцев
Как ни хотелось, а избежать этой темы не удалось.
Немного терминологии. Называть эту статью введением в Win32 было бы неверно — консольное приложение под Windows такое же полноправное Win32 приложение. Что такое консольное приложение? Ах да - да ты ведь регулярно сталкиваешься с такими программами - например мой любимый файл-менеджер FAR является полноценным консольным Win32 приложением. Точка входа для консольных приложений та же, что и в дос программах на Си - это функция main(). Однозначно разделять оконные приложения и консольные тоже, как мне кажется, было бы не правильно - программа, использующая как точку входа в программу WinMain(), как это ни странно, может и не создавать окно, а даже организовать свою консоль (!) вызовом AllocConsole(), а консольное приложение без проблем может создать окно. Возникает лишь один резонный вопрос: зачем это нужно? ;) Консольные приложения хороши своей самодостаточностью — они идеально подходят для утилит командной строки. О достоинствах оконных приложений я говорить не буду - чуть ниже мы перейдем напрямую к оконным приложениям под Windows.
Архитектурно, эти обе категории различаются стабом (stub) который прилинковывается к твоему откомпилированному коду - в данном контексте под стабом подразумевается некий программный код, выполняемый до (и по выходу) передачи управления в main() или WinMain() (в зависимости от типа приложения). Стабы различны для каждых фирм и часто для разных версий одного и того же продукта - что вполне логично. Например, стаб Борланда не совпадает со стабом от MS, хотя обычно они выполняют одни и те же цели - инициализацию каких-либо внутренних обработчиков и т.п. Исходные тексты стаба (кстати говоря, обычно открыты и исходные тексты рантайм функций - например, вполне реально найти исходный текст знакомых тебе fopen() или printf()) открыты. Например, при инсталяции Visual C++ ты можешь установить и исходные тексты стаба и рантайма (по умолчанию в \CRT\SRC).
Ну что - перейдем к созданию оконных приложений? Как я уже и сказал точка входа в оконные приложения WinMain(). Да - чуть не забыл - пример, и исходные тексты лежат ЗДЕСЬ. Приложение максимально упрощено - не жди от него чего-либо необычного. Наша цель понять общие идеи.
Итак, WinMain(). Давай разберемся с его определением. Ну, то что функция возвращает int, это понятно (кстати она возвращает его всегда - переопределить возвращаемое значение скажем на void не выйдет - компилятор выдаст ошибку) - но вот что такое WINAPI? Опытный читатель уже успел (если сталкивался с подобным) перерыть заголовочные *.h файлы Windows ("хедеры") в поисках макроопределения WINAPI. Определение находится в windef.h. Реально то, что подставляется вместо WINAPI, зависит от используемой платформы (ОС) и версии компилятора. Так что же это такое? Это СОГЛАШЕНИЯ о вызове функции. Что это такое? Дело в том, что любая программа высокого уровня (конечно же речь идет о компиляторах, а не интерпретаторах) транслируется при компиляции в маш-коды понятные процессору. Сейчас тебе понадобится минимальные знания процессоров Intel архитектуры IA-32 (достаточно даже более ранней). В ассемблере (язык в котором одна команда сопоставима одной комманде машинных кодов - в общем-то, те же машинные коды, но в понятной для человека форме) нет вызова функции с параметрами, как и возврата результата - зато есть стек, регистры, безусловный переход JMP и вызов подпрограммы инструкцией CALL. Какой код сгенерирует компилятор - зависит от него самого - я не буду затрагивать эту обширную тему - скажу лишь, что существует некоторое количество соглашений, благодаря которым компилятор генерирует код для вызова функций - вот некоторые из них (Кстати говоря - программа, написанная на ассемблере вручную не обязательно придерживается каких-то соглашений. А зачем? Программист легко может использовать необходимые ему регистры - потому, что он пишет свои функции сам. Другое дело, когда он использует какие-то стандартные библиотеки - теперь он ограничен спецификацией используемого им API):
Для полного понимания приведу пример того, как бы скорее всего ассемблировался вызов следующей функции используя соглашения __cdecl:
ParseString("Это просто строка !",0x12);
А теперь ассемблерные текст:
push 0x00000012 ; Первый DWORD в стек второй параметер функции push 0x00040600 ; Адрес придуманный мной - указатель на строку call Какой-то-адрес ; Наша функция - адрес ParseString add esp,08 ; 2 DWORD параметра = на 8 байта нужно сдвинуть ; указатель стека ESP (соглашения cdecl - ; вызывающая процедура правит стек).
Это что касается соглашений - WINAPI - определяется одним из этих типов (не из всех, конечно же, - из пары-тройки) в зависимости, как я уже говорил, от платформы или используемого компилятора. Параметры у WinMain():
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow);
Ты не поверишь, осталось самое простое - зарегистрировать класс приложения, создать окно и запустить цикл обработки сообщений. Ты уже знаешь что Windows "событие-ориентированная" ОС (в оригинале это звучит более лаконично - Event-driven). Это означает, что каждому приложению (а точнее его окну) посылаются некоторые коды-сообщения на которые приложение может реагировать. Ну, например, приложению посылается сообщение WM_CREATE после создания окна, но до его отображения на экране. Или WM_KEYDOWN когда нажата клавиша(ы) и окно имеет фокус. Однако это не значит, что все сообщения ты должен обрабатывать - нисколько. Теперь давай вернемся к регистрации класса приложения:
// более подробную информацию о полях структуры ты сможешь найти в документации // структура описывающая класс WNDCLASSEX wc; // размер самой структуры - для совместимости в случае будущих изменений wc.cbSize = sizeof(wc); // стили класса - в 90% случаев ты будешь пользоваться этими двумя флажками wc.style = CS_HREDRAW | CS_VREDRAW; // "Оконная процедура" - функция, которая будет получать "сообщения" от Windows wc.lpfnWndProc = WndProc; // Расширенные параметры - я их не рассматриваю wc.cbClsExtra = 0; wc.cbWndExtra = 0; // Сюда мы положим идентификатор нашего приложения // На заметку - в Win16 - если второй параметр // WinMain() был не равен 0 - класс не нужно было регистрировать. // Именно поэтому регистрация класса во многих примерах // выделена в отдельную функцию. wc.hInstance = hInst; // Параллельно загружаю стандартную иконку (идентификатор ресурса IDI_APPLICATION). wc.hIcon = LoadIcon( NULL,IDI_APPLICATION); // Стандартный курсор (идентификатор ресурса IDC_ARROW). wc.hCursor = LoadCursor( NULL,IDC_ARROW); // Фон. Запрашиваю цвет стандартного белого браша. Мало ли // какая у нас цветовая схема. wc.hbrBackground= ( HBRUSH)GetStockObject( WHITE_BRUSH); // Меню - у меня его нет. Да не больно то и хотелось. wc.lpszMenuName = NULL; // Строка - имя класса - поле для фантазии разработчика. Визуально нигде не // отображается. wc.lpszClassName= TITLE_STRING; // Этот параметр - маленькая иконка которая будет отображаться // для приложения в task bar'e. Если он не указан - Windows // уменьшит hIcon до требуемых размеров. Это единственный параметр, // в котором различаются два аналогичных по смыслу вызова // RegisterClass и RegisterClassEx. Соответственно в первом случае // используется структура WNDCLASS а во втором WNDCLASSEX - ну это на любителя. wc.hIconSm = NULL; // пытаюсь зарегистрировать класс. if ( !RegisterClassEx( &wc)) { return FALSE; }
Этот кусочек кода отвечает за регистрацию класса окна - да, именно - скорее это даже класс окна.
В этой статье я откоментировал код для того, чтобы не тратить много времени на описание и так понятных действий. Пара оговорок - идентификатор ресурсов - некоторая константа, описывающая номер ресурса - в моем приложении нет своих ресурсов - если бы они были - можно было бы использовать их - указав при загрузке идентификатор экземпляра приложения, вторым - идентификатор ресурса. Продолжаем?
// создаю окно // для создания окна также можно использовать аналогичный по смыслу // вызов CreateWindowEx - у него немного больше возможностей. // первый параметр - это имя класса зарегистрированного RegisterClass или // RegisterClassEx. // второй - имя окна в заголовке - пользуюсь одной и той же строкой в обоих // случаях. // третий параметр - стиль окна - флагов много - я не буду останавливаться // на них - дело за тобой. // следующие 4 параметра это координаты X, Y; длина по X и длина по Y окна. // Параметр CW_USEDEFAULT нужен в случае если я хочу получить эти координаты от // Windows. Этот флаг достаточно указать один раз для каждой пары // параметров - именно поэтому два последних в каждой паре - нули. // восьмой параметр - идентификатор окна-родителя. В моем случае окно одно - // сирота - параметр NULL. // девятый - идентификатор окна ребенка или меню в зависимости от стилей. Хм, ни меню, // ни потомков у нас нет... // угу - идентификатор экземпляра приложения ;) // дополнительный параметр при создании окна - обычно используется // как указатель на дополнительную структуру. Нас сейчас не интересует. // Потому и NULL. g_hWnd = CreateWindow(TITLE_STRING, TITLE_STRING, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,0, CW_USEDEFAULT,0, NULL,NULL,hInst,NULL); // получили ли мы идентификатор ? if ( g_hWnd == NULL) { return FALSE; } // показать рамку и заголовок окна // (если они есть - я их запросил - указав соответствующие стили) ShowWindow( g_hWnd,SW_SHOW); // послать окну сообщение WM_PAINT - фактически просьба перерисовать // содержимое клиентской части окна - той, что доступна пользователю. // Если поставить до этого задержку, можно некоторое время лицезреть // "дырявое" окно ;) UpdateWindow( g_hWnd);
На заметку - именно вызовом CreateWindow(Ex) в Windows создаются ВСЕ стандартные элементы управления - начиная от кнопок, заканчивая ListBox'ами. Для них существуют заранее определенные имена классов, некоторые дополнительные сообщения для их оконных процедур, стили и т.п. Каково? Нам осталось запустить цикл обработки сообщений:
while(GetMessage( &msg,NULL,0,0)) { TranslateMessage( &msg); DispatchMessage( &msg); }
Вызов GetMessage() вернет 0 (то есть "ложь") только когда в получит сообщение WM_QUIT - запрос на завершение работы. Первый параметр - это указатель на структуру, которая будет заполнена данными о сообщении - его номером и некоторой другой полезной информацией.
Второй параметр идентификатор окна (HWND) - передав NULL я прошу обрабатывать сообщения для ВСЕХ окон приложения - их ведь могло бы быть много. Последние два нуля - это фильтр обрабатываемых сообщений - минимальное и максимальное значения сообщения. Я не использую фильтр - поэтому и передаю нули. GetMessage() - блокированная функция - это значит, что пока сообщение, удовлетворяющее фильтру или WM_QUIT не будет получено - выполнение не будет продолжено. Для игровых и некоторых других приложений это неприемлемо - в этом случае лучше использовать другую структуру цикла обработки сообщений - например, такого, который я использовал в примерах по Direct3D. После того как сообщение будет принято, оно будет оттранслированно (TranslateMessage) и преданно в оконную процедуру (DispatchMessage). Где же она, виновница торжества?
// надеюсь ты уже понял что такое WINAPI - и для тебя не составит // труда найти определение CALLBACK ;) // параметры, передаваемые в оконную процедуру: // hwnd - идентификатор окна, которому направленно сообщение // uMsg - собственно сообщение // wParam и lParam - дополнительные данные - в зависимости от сообщения // в них хранится другая полезная информация, зависящая от сообщения. LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { // А не обработать ли сообщения? switch( uMsg) { // А тут мы обрабатываем нажатие клавиш case WM_KEYDOWN: switch( wParam) { // wParam сигнализирует что нажата клавиша Escape ? case VK_ESCAPE: // Отлично - "сезам" закройся. Если пользователь нажал Escape, // я просто ставлю в очередь сообщений WM_CLOSE - сообщение // которое инициирует завершение. Два последних нолика - // это wParam и lParam для моего сообщения. SendMessage( g_hWnd,WM_CLOSE,0,0); // почти все сообщения, обработанные в оконной процедуре (это оговоренно // в документации, описывающей конкретное сообщение) должны // возвращать 0 по выходу из оконной процедуры break; } // ну, тут небольшая неувязка - большого преступления впрочем нет, // если нажата другая клавиша, а я, не обработав ее, не отдаю управление // ядру вызовом DefWindowProc() (см. ниже). Но это только частный случай!!! break; // сообщение WM_DESTORY посылается, когда окно уже закрыто и // убрано с рабочего стола. Учти, если ты не вызовешь функцию // PostQuitMessage() (параметр для нее - это код возврата // ложится в wParam который возвращается из WinMain), окно будет // закрыто, но приложение все еще будет "вертеться" в фоне. // PostQuitMessage() выполняет только одно действие - оно ставит // в очередь сообщений сообщение WM_QUIT, а ты уже знаешь, что // это единственное сообщение, после обработки которого GetMessage() // вернет 0... case WM_DESTROY: PostQuitMessage( 0); // почти все сообщения, обработанные в оконной процедуре (это оговоренно // в документации, описывающей конкретное сообщение) должны // возвращать 0 по выходу из оконной процедуры break; default: // не обработанные сообщения обязательно должны быть отданы ядру // нижеследующим вызовом. Тогда система сможет корректно отработать // например, WM_SIZE(изменение размера окна). return DefWindowProc( hWnd,uMsg,wParam,lParam); } // почти все сообщения, обработанные в оконной процедуре (это оговоренно // в документации, описывающей конкретное сообщение) должны // возвращать 0 по выходу из оконной процедуры return 0; }
Учти, что эту программку я писал, отталкиваясь исключительно от MS Visual C, то есть не пользовался другими компиляторами. Хотя, думаю, проблем не должно возникнуть. Наверное, этого достаточно для начала - теперь дело только за тобой. Удачной «охоты»!
5 июня 2002 (Обновление: 26 авг 2010)
Комментарии [8]