Введення в програмування під windows
Як не хотілося, а уникнути цієї теми не вдалося.
Трохи термінології. Називати цю статтю введенням в Win32 було б невірно - консольний додаток під Windows таке ж повноправне Win32 додаток. Що таке консольний додаток? Ах да - да ти ж регулярно стикаєшся з такими програмами - наприклад мій улюблений файл-менеджер FAR є повноцінним консольним Win32 додатком. Точка входу для консольних додатків та ж, що і в дос програмах на Сі - це функція main (). Однозначно розділяти віконні додатки і консольні теж, як мені здається, було б не правильно - програма, яка використовує як точку входу в програму WinMain (), як це не дивно, може і не створювати вікно, а навіть організувати свою консоль (!) Викликом AllocConsole (), а консольний додаток без проблем може створити вікно. Виникає лише одне резонне питання: навіщо це потрібно? ;) Консольні додатки гарні своєю самодостатністю - вони ідеально підходять для утиліт командного рядка. Про достоїнства віконних додатків я говорити не буду - трохи нижче ми перейдемо безпосередньо до віконних додатків під Windows.
Ну що - перейдемо до створення віконних додатків? Як я вже і сказав точка входу в віконні додатки WinMain (). Так - мало не забув - приклад, і вихідні тексти лежать ТУТ. Додаток максимально спрощено - не чекай від нього чогось незвичайного. Наша мета зрозуміти загальні ідеї.
Отже, WinMain (). Давай розберемося з його визначенням. Ну, то що функція повертає int, це зрозуміло (до речі вона повертає його завжди - перевизначити значення, що повертається скажімо на void вийде - компілятор видасть помилку) - але ось що таке WINAPI? Досвідчений Новомосковсктель вже встиг (якщо стикався з подібним) перерити заголовні * .h файли Windows ( "хедери") в пошуках макроозначення WINAPI. Визначення знаходиться в windef.h. Реально те, що підставляється замість WINAPI, залежить від використовуваної платформи (ОС) і версії компілятора. Так що ж це таке? Це УГОДИ про виклик функції. Що це таке? Справа в тому, що будь-яка програма високого рівня (звичайно ж мова йде про компіляторах, а не інтерпретатора) транслюється при компіляції в маш-коди зрозумілі процесору. Зараз тобі знадобиться мінімальні знання процесорів Intel архітектури IA-32 (досить навіть більш ранньої). В асемблері (мова в якому одна команда порівнянна однієї комманде машинних кодів - в общем-то, ті ж машинні коди, але в зрозумілій для людини формі) немає виклику функції з параметрами, як і повернення результату - зате є стек, регістри, безумовний перехід JMP і виклик підпрограми інструкцією CALL. Які символи ви бачите згенерує компілятор - залежить від нього самого - я не буду торкатися цієї широку тему - скажу лише, що існує певна кількість угод, завдяки яким компілятор генерує код для виклику функцій - ось деякі з них (До речі кажучи - програма, написана на асемблері вручну не обов'язково дотримується якихось угод. А навіщо? Програміст легко може використовувати необхідні йому регістри - тому, що він пише свої функції сам. Інша справа, коли він використовує якісь стандартні бібліотеки - тепер він обмежений специфікацією використовуваного ним API):
* __stdcall - параметри передаються через стек - справа наліво (в зворотному порядку - перший параметр лягає в стек останнім). Її викликає процедура "повертає" покажчик стека ESP "на місце". Більшість Win32 функцій використовують ці угоди.
* __cdecl - параметри передаються через стек, справа наліво. Зухвала функція (та, з якої був ініційований виклик) "править" стек. Класичний виклик в Сі.
* __pascal - параметри передаються через стек, зліва направо. Стек "править" викликається процедура. Типові угоди про виклики для "Паскаля".thiscall - параметри передаються через стек справа наліво, плюс до цього в регістрі ECX передається покажчик на C ++ this. Зухвала функція "править" стек. Виклик C ++.
Для повного розуміння наведу приклад того, як би швидше за все ассембліровался виклик наступної функції використовуючи угоди __cdecl:
ParseString ( "Це просто рядок!", 0x12);
А тепер асемблерні текст:
Це що стосується угод - WINAPI - визначається одним з цих типів (не з усіх, звичайно ж, - з пари-трійки) в залежності, як я вже говорив, від платформи або використовуваного компілятора. Параметри у WinMain ():
* HInstance - це ідентифікатор екземпляра додатка. Не лякайся типу HINSTANCE. Це напевно, якийсь unsigned int. Кожному з додатком присвоюється унікальний номер, який стаб Новомосковскет викликом GetModuleHandle (), а потім передає його тобі. Якщо ти в додатку прочитаєш цією функцією ідентифікатор додатки - ти побачиш, що він збігається з тим, що тобі передали в WinMain (). Цей ідентифікатор знадобиться нам для дуже багатьох цілей.
* HPrevInstance - це ідентифікатор попереднього екземпляра додатка. Під Win32 він завжди дорівнює NULL. Тут вже тобі вирішувати - працювати далі чи ні - в своєму прикладі фактично я відмовляюся працювати, якщо другий параметр відмінний від NULL. До речі - дізассембліруй Quake1, Quake2 або Quake3 - побачиш те ж саме.
* LpCmdLine - просто покажчик на командний рядок - без поділу на параметри - одна монолітна рядок.
* NCmdShow - це параметр описує прапорці для вікна - то, як воно буде показано на екрані при створенні. Зазвичай цей параметр передають в виклик ShowWindow (). У своєму додатку я його не використовую, а просто передаю в ShowWindow () прапорець SW_SHOW.
Ти не повіриш, залишилося найпростіше - зареєструвати клас додатки, створити вікно і запустити цикл обробки повідомлень. Ти вже знаєш що Windows "подія-орієнтована" ОС (в оригіналі це звучить більш лаконічно - Event-driven). Це означає, що кожному з додатком (а точніше його вікна) надсилаються деякі коди-повідомлення на які додаток може реагувати. Ну, наприклад, з додатком надсилається повідомлення WM_CREATE після створення вікна, але до його відображення на екрані. Або WM_KEYDOWN коли натиснута кнопка (и) і вікно має фокус. Однак це не означає, що всі повідомлення ти повинен обробляти - анітрохи. Тепер давай повернемося до реєстрації класу програми:
Цей шматочок коду відповідає за реєстрацію класу вікна - так, саме - скоріше це навіть клас вікна.
У цій статті я прокоментували код для того, щоб не витрачати багато часу на опис і так зрозумілих дій. Пара застережень - ідентифікатор ресурсів - деяка константа, що описує номер ресурсу - в моєму додатку немає своїх ресурсів - якби вони були - можна було б використовувати їх - вказавши при завантаженні ідентифікатор екземпляра додатка, другим - ідентифікатор ресурсу. Продовжуємо?
На замітку - саме викликом CreateWindow (Ex) в Windows створюються ВСЕ стандартні елементи управління - починаючи від кнопок, закінчуючи ListBox'амі. Для них існують заздалегідь певні імена класів, деякі додаткові повідомлення для їх віконних процедур, стилі і т.п. Яке? Нам залишилося запустити цикл обробки повідомлень:
Виклик GetMessage () поверне 0 (тобто "брехня") тільки коли в отримає повідомлення WM_QUIT - запит на завершення роботи. Перший параметр - це покажчик на структуру, яка буде заповнена даними про сполученні - його номером і деякою іншою корисною інформацією.
Другий параметр ідентифікатор вікна (HWND) - передавши NULL я прошу обробляти повідомлення для ВСІХ вікон програми - їх адже могло б бути багато. Останні два нуля - це фільтр оброблюваних повідомлень - мінімальне і максимальне значення повідомлення. Я не використовую фільтр - тому і передаю нулі. GetMessage () - блокована функція - це значить, що поки повідомлення, яке задовольняє фільтру або WM_QUIT що не отримано - виконання не буде продовжено. Для ігрових і деяких інших додатків це неприйнятно - в цьому випадку краще використовувати іншу структуру циклу обробки повідомлень - наприклад, такого, який я використовував в прикладах по Direct3D. Після того як повідомлення буде прийнято, воно буде оттранслировать (TranslateMessage) і віддано в віконну процедуру (DispatchMessage). Де ж вона, винуватиця торжества?
Врахуй, що цю програму я писав, відштовхуючись виключно від MS Visual C, тобто не користувався іншими компіляторами. Хоча, думаю, проблем не повинно виникнути. Напевно, цього достатньо для початку - тепер справа тільки за тобою. Вдалого полювання"!