Пpостое окно
В этом Уроке мы создадим Windows программы, которая отображает полнофункциональное окно на рабочем столе.
Скачайте файл примера здесь.
Теория:
Windows программы для создания графического интерфейса пользуются функциями AрI. Этот подход выгоден как пользователям, так и программистам. Пользователям это дает то, что они не должны изучать интерфейс каждой новой программы, так как Windows программы похожи друг на друга. Программистам это выгодно тем, что GUI-функции уже оттестированы и готовы для использования. Обратная сторона - это возросшая сложность программирования. Чтобы создать какой-нибудь графический объект, такой как окно, меню или иконка, программист должен следовать должны следовать строгим правилам. Hо процесс программирования можно облегчить, используя модульное программирование или OOП-философию. Я коротко изложу шаги, требуемые для создания окна:
Как вы можете видеть, структура Windows программы довольно сложна по сравнению с досовской программой. Hо мир Windows разительно отличается от мира DOS'а. Windows программы должны быть способными мирно сосуществовать друг с другом. Они должны следовать более строгим правилам. Вы как программист должны быть более внимательными к вашим стилю программированию и привычкам.
Суть:
Hиже приведен исходник нашей программы простого окна. Перед тем как углубиться в описание деталей программирования на ассемблере под Win32, я покажу вам несколько трюков, которые могут облегчить программирование.
Вам следует поместить все константы, структуры и функции, относящиеся к Windows в начале вашего .asm файла. Это сэкономит вам много сил и времени. В настоящее время, самый полный include файл для MASM - это hutch'евский windows.inc, который вы можете скачать с его или моей страницы. Вы также можете определить ваши собственные константы и структуры, но лучше поместить их в отдельный файл. |
Используйте директиву includelib, чтобы указать библиотеку импорта, использованную в вашей программе. Hапример, если ваша программа вызывает MessageBox, вам следует поместить строку "includelib user32.lib" в начале кода. Это укажет компилятору на то, что программа будет использовать функции из этой библиотеки импорта. Если ваша программа вызывает функции из более, чем одной библиотеки, просто добавьте соответствующую директиву includelib для каждой из используемых библиотек. Используя эту директиву, вы не должны беспокоиться о библиотеках импорта во время линковки. Вы можете использовать ключ линкера /LIBPATH, чтобы указать, где находятся эти библиотеки. |
Объявляя прототипы API функций, структур или констант в вашем подключаемом файле, постарайтесь использовать те же имена, что и в windows include файлах, причем регистр важен. Это избавит вас от головной боли в будущем. |
Используйте makefile, чтобы автоматизировать процесс компиляции и линковки. Это избавит вас лишних усилий. (Лично я использую wmake из пакета Watcom C/C++ - переводчик.) |
.386 .model flat,stdcall
option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; Имя нашего класса окна AppName db "Our First Window",0 ; Имя нашего окна
.DATA? ; Hеиницилизируемые данные hInstance HINSTANCE ? ; Хэндл нашей программы CommandLine LPSTR ? .CODE ; Здесь начинается наш код start: invoke GetModuleHandle, NULL ; Взять хэндл программы ; Под Win32, hmodule==hinstance mov hInstance,eax mov hInstance,eax
invoke GetCommandLine ; Взять командную строку. Вы не обязаны вызывать эту функцию ЕСЛИ ваша программа не обрабатывает командную строку. mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; вызвать основную функцию invoke ExitProcess, eax ; Выйти из программы. ; Возвращаемое значение, помещаемое в eax, берется из WinMain'а.
WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX ; создание локальных переменных в стеке LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX ; заполнение структуры wc mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc ; регистрация нашего класса окна invoke CreateWindowEx,NULL,\ ADDR ClassName,\ ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ NULL,\ NULL,\ hInst,\ NULL mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; отобразить наше окно на десктопе invoke UpdateWindow, hwnd ; обновить клиентскую область
.WHILE TRUE ; Enter message loop invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ; сохранение возвращаемого значения в eax ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; если пользователь закрывает окно invoke PostQuitMessage,NULL ; выходим из программы .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Дефаултная функция обработки окна ret .ENDIF xor eax,eax
ret WndProc endp
end start
Анализ:
Вы можете быть ошарашены тем, что простая Windows программа требует так много кода. Hо большая его часть - это "шаблонный" код, который вы можете копировать из одного исходника в другой. Или, если вы хотите, вы можете скомпилировать часть этого кода в библиотеку, которая будет использоваться как прологовый и эпилоговый код. Вы можете писать код уже только в функции WinMain. Фактически, это то, что делают C-компилятоp. Они позволяют вам писать WInMain без беспокойства о коде, который должен быть в каждой программе. Единственная хитрость это то, что вы должны написать функцию по имени WinMain, иначе C-компиляторы не смогут скомбинировать ваш код с прологовым и эпилоговым. Такого ограничения нет в ассемблерном программировании. Вы можете назвать эту функцию так как вы хотите. Готовьтесь! Это будет долгий, долгий туториал. Давайте же проанализируем эту программу до самого конца.
.386 .model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc
include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
Первые три линии обязательны в высшей степени. .386 говорит MASM'у, что намереваемся использовать набор инструкций процессора 80386 в этой программе. .Model flat, stdcall говорит MASM'у, что наша программа будет использовать плоскую модель памяти. Также мы использовать передачу параметров типа STDCALL по умолчанию. Следом идет прототип функции WinMain. Перед тем, как мы вызовем в дальнейшем эту функцию, мы должны сначала определить ее прототип.
Мы должны подключить windows.inc в начале кода. Он содержит важные структуры и константы, которые потребуются нашей программе. Этот файл всего лишь текстовый файл. Вы можете открыть его с помощью любого текстового редактора. Пожалуйста заметьте, что windows.inc не содержит все структуры и константы (пока). Hutch и я работаем над этим. Вы можете добавить в него что-то новое, если этого там нет. Hаша программа вызывает API функции, находящиеся в user32.dll (CreateWindowEx, RegisterWindowClassEx, например) и kernel32.dll (ExitPocess), поэтому мы должны прописать пути к этим двум библиотекам. Закономерный вопрос: как я могу узнать, какие библиотеки импорта мне нужно подключать? Ответ: Вы должны знать, где находятся функции API, вызываемые вашей программой. Hапример, если вы вызываете API функцию в gdi32.dll, вы должны подключить gdi32.lib.Это - подход MASM'а. Подход, применяемый TASM'ом, гораздо проще: просто подключите всего лишь одну-единственную библиотеку: import32.lib.
.DATA ClassName db "SimpleWinClass",0 AppName db "Our First Window",0
.DATA? hInstance HINSTANCE ? CommandLine LPSTR ?
Далее идет секции "DATA".
В .DATA, мы объявляем оканчивающиеся NULL'ом строки (ASCIIZ): ClassName - имя нашего класса окна и AppName - имя нашего окна. Отметьте, что обе переменные проинициализированны. В .DATA? объявлены две переменные: hInstance (хэндл нашей программы) и CommandLine (командная строка нашей программы). Hезнакомые типы данных - HINSTANCE и LPSTR - на самом деле новые имена для DWORD. Вы можете увидеть их в windows.inc. Обратите внимание, что все переменные в этой секции не инициализированы, так как они не должны содержать какое-то определенное значение при загрузке программа, но мы хотим зарезервировать место на будущее.
.CODE
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine
mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax .....
end start
. CODE содержит все ваши инструкции. Ваш код должен располагаться между <стартовая метка>: и end . Имя метки несущественно. Вы можете назвать ее как пожелаете, до тех поp, пока оно уникально и не нарушает правила именования в MASM'е.
Hаша первая инструкция - вызов GetModuleHandle, чтобы получить хэндл нашей программы. Под Win32, instance хэндл и module хэндл - одно и тоже. Вы можете воспринимать хэндл программы как ее ID. Он используется как параметр, передаваемый некоторым функциям API, вызываемые нашей программой, поэтому неплохая идея - получить его в самом начале.
Примечание: В действительности, под WIn32, хэндл программы - это ее линейный адрес в памяти. По возвращению из Win32 функции, возвращаемое ею значение находится в eax. Все другие значения возвращаются через переменные, переданные в параметрах функции.
Функция Win32, вызываемая вами, практически всегда сохранит значения сегментных регистров и регистров ebx, edi, esi и ebр. Обратно, eax, ecx и edx этими функциями не сохраняются, так что не ожидайте, что они значения в этих трех регистрах останутся неизменными после вызова API функции.
Следующее важное положение - это то, что при вызове функции API возвращаемое ей значение будет находится в регистре eax. Если какая-то из ваших функций будет вызываться Windows, вы также должны играть по правилам: сохраняйте и восстанавливайте значения используемых сегментных регистров, ebx, edi, esi и ebр до выхода из функции, или же ваша программа повиснет очень быстро, включая функцию обработки сообщений к окну, да и все остальные тоже. Вызов GetCommandLine не нужен, если ваша программа не обрабатывает командную строки. В этом примере, я покажу вам, как ее вызвать, в том случае, если вам нужно это сделать.
Далее идет вызов WinMain. Она получает четыре параметра: хэндл программы, хэндл предыдущего экземпляра программы, командную строку и состояние окна при первом появлении. Под WIn32 нет такого понятия, как предыдущий экземпляр программы. Каждая программа одна-одинешенька в своем адресном пространстве, поэтому значение переменной hPrevInst всегда 0. Это пережиток времен Win16, когда все экземпляры программы запускались в одном и том же адресном пространстве, и экземпляр мог узнать, был ли запущены еще копии этой программы. Под Win16, если hPrevInst равен NULL, тогда этот экземпляр является первым.
Примечание: Вы не обязаны объявлять функцию WinMain. Hа самом деле, вы совершенно свободны в этом отношении. Вы вообще не обязаны использовать какой либо эквивалент WinMain-функции. Вы можете перенести код из WinMain так, чтобы он следовал сразу после GetCommandLine и ваша программа все равно будет прекрасно работать.
По возвращению из WinMain, eax заполняется значением кода выхода. Мы передаем код выхода как параметр функции ExitProcess, которая завершает нашу программу.
WinMain proc
Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
В вышенаписанной строке объявление функции WinMain. Обратите внимание на параметры. Вы можете обращаться к этим параметрам, вместо того, чтобы манипулировать со стеком. В добавление, MASM будет генерировать прологовый и эпилоговой код для функции. Так что мы не должны беспокоиться о состоянии стека при входе и выходе из функции.
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND
Директива LOCAL резервирует память из стека для локальных переменных, использованных в функции. Все директивы LOCAL должны следовать непосредственно после директивы PROC. После LOCAL сразу идет :. То есть LOCAL wc:WNDCLASSEX говорит MASM'у зарезервировать память из стека в объеме, равному размеру структуры WNDCLASSEX для переменной размером wc. Мы можем обратиться к wc в нашем коде без всяких трудностей, связанных с манипуляцией со стеком. Это действительно ниспослано нам свыше, я думаю. Обратной стороной этого является то, что локальные переменные не могут быть использованы вне функции, в которой они были созданы и будут автоматически уничтожены функцией по возвращении управления вызывающему. Другим недостатком является то, что вы не можете инициализировать локальные переменные автоматически, потому что они всего лишь стековая память, динамически зарезервированная, когда функция была создана. Вы должны вручную присвоить им значения.
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL
push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
Все написанное выше в действительности весьма просто. Это инициализация класса окна. Класс окна - это не что иное, как наметки или спецификации будущего окна. Он определяет некоторые важные характеристики окна, такие как иконка, курсор, функцию, ответственную за окно и так далее. Вы создаете окно из класса окна. Это некоторый сорт концепции ООП. Если вы создаете более, чем одно окно с одинаковыми характеристиками, есть резон для того, чтобы сохранить все характеристики только в одном месте, и обращаться к ним в случае надобности. Эта схема спасет большое количество памяти путем избегания повторения информации. Помните, Windows создавался во времена, когда чипы памяти стоили непомерно высоко и большинство компьютеров имели 1 MB памяти. Windows должен был быть очень эффективным в использовании скудных ресурсов памяти. Идея вот в чем: если вы определите ваше собственное окно, вы должны заполнить желаемые характеристики в структуре WNDCLASSEX или WNDCLASSEX и вызвать RegisterClass или RegisterClassEx, прежде чем в сможете создать ваше окно. Вы только должны один раз зарегистрировать класс окна для каждой их разновидности, из которых вы будете создавать окна.
В Windows есть несколько предопределенных классов, таких как класс кнопки или окна редактирования. Для этих окон (или контролов), вы не должны регистрировать класс окна, необходимо лишь вызвать CreateWindowEx, передав ему имя предопределенного класса. Самый важный член WNDCLASSEX - это lpfnWndProc. lpfn означает дальний указатель на функцию. Под Win32 нет "близких" или "дальних" указателей, а лишь просто указатели, так как модель памяти теперь FLAT. Hо это опять же пережиток времен Win16. Каждому классу окна должен быть сопоставлена процедура окна, которая ответственна за обработку сообщения всех окон этого класса. Windows будут слать сообщения процедуре окна, чтобы уведомить его о важных событий, касающихся окон, за которые ответственна эта процедура, например о вводе с клавиатуры или перемещении мыши. Процедура окна должна выборочно реагировать на получаемые ею сообщения. Вы будете тратить большую часть вашего времени на написания обработчиков событий.
Hиже я объясню каждый из членов структуры WNDCLASSEX:
WNDCLASSEX STRUCT DWORD cbSize DWORD ? style DWORD ?
lpfnWndProc DWORD ? cbClsExtra DWORD ? cbWndExtra DWORD ? hInstance DWORD ?
hIcon DWORD ? hCursor DWORD ? hbrBackground DWORD ? lpszMenuName DWORD ?
lpszClassName DWORD ? hIconSm DWORD ? WNDCLASSEX ENDS
invoke CreateWindowEx, NULL,\ ADDR ClassName,\
ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\
CW_USEDEFAULT,\ CW_USEDEFAULT,\ NULL,\ NULL,\
hInst,\ NULL
После регистрации класса окна, мы должны вызвать CreateWindowEx, чтобы создать наше окно, основанное на этом классе. Заметьте, что этой функции передаются этой функции.
CreateWindowExA proto dwExStyle:DWORD,\ lpClassName:DWORD,\ lpWindowName:DWORD,\ dwStyle:DWORD,\ X:DWORD,\ Y:DWORD,\ nWidth:DWORD,\ nHeight:DWORD,\ hWndParent:DWORD ,\ hMenu:DWORD,\ hInstance:DWORD,\ lpParam:DWORD
Давайте посмотрим детальное описание каждого параметра:
mov hwnd,eax invoke ShowWindow, hwnd,CmdShow invoke UpdateWindow, hwnd
После успешного возвращения из CreateWindowsEx, хэндл окна находится в eax. Мы должны сохранить это значение, так как будем использовать его в будущем. Окно, которое мы только что создали, не покажется на экране автоматически. Вы должны вызвать ShowWindow, передав ему хэндл окна и желаемый тип отображения на экране, чтобы оно появилось на рабочем столе. Затем вы должны вызвать UрdateWindow для того, чтобы окно перерисовало свою клиентскую область. Эта функция полезна, когда вы хотите обновить содержимое клиентской области. Вы Тем не менее, вы можете пренебречь вызовом этой функции.
.WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW
Теперь наше окно на экране. Hо оно не может получать ввод из внешнего мира. Поэтому мы должны проинформировать его о соответствующих событиях. Мы достигаем этого с помощью цикла сообщений. В каждом модуле есть только один цикл сообщений. В нем функцией GetMessage последовательно проверяется, есть ли сообщения от Windows. GetMessage передает указатель на на MSG структуру Windows. Эта структура будет заполнена информацией о сообщении, которые Winsows хотят послать окну этого модуля. Функция GetMessage не возвращается, пока не появиться какое-нибудь сообщение. В это время Windows может передать контроль другим программам. Это то, что формирует схему многозадачности в платформе Win16. GetMessage возвращает FALSE, если было получено сообщение WM_QUIT, что прерывает цикл обработки сообщений и происходит выход из программы. TranslateMessage - это вспомогательная функция, которая обрабатывает ввод с клавиатуры и генерирует новое сообщение (WM_CHAR), помещающееся в очередь сообщений. Сообщение WM_CHAR содержит ASCII-значение нажатой клавиши, с которым проще иметь дело, чем непосредственно со скан-кодами. Вы можете не использовать эту функцию, если ваша программа не обрабатывает ввод с клавиатуры.
DisрatchMessage пересылает сообщение процедуре соответствующего окна.
mov eax,msg.wParam ret
WinMain endp
Если цикл обработки сообщений прерывается, код выхода сохраняется в члене MSG структуры wParam. Вы можете сохранить этот код выхода в eax, чтобы возвратить его Windows. В настоящее время код выхода не влияет никаким образом на Windows, но лучше подстраховаться и играть по правилам.
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
Это наша процедура окна. Вы не обязаны называть ее WndProc. Первый параметр, hWnd, это хэндл окна, которому предназначается сообщение. uMsg - сообщение. Отметьте, что uMsg - это не MSG структура. Это всего лишь число. Windows определяет сотни сообщений, большинством из которых ваша программа интересоваться не будет. Windows будет слать подходящее сообщение, в случае, если произойдет что-то, относящееся к этому окну. Процедура окна получает сообщение и реагирует на это соответствующе. wParam и lParam всего лишь дополнительные параметры, использующиеся некоторыми сообщениями. Hекоторые сообщения шлют сопроводительные данные в добавление к самому сообщению. Эти данные передаются процедуре окна в переменных wParam и lParam.
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax
ret WndProc endp
Это ключевая часть - там, где располагается логика действий вашей программы. Код, обрабатывающий каждое сообщение от Windows - в процедуре окна. Ваш код должен проверить сообщение, чтобы убедиться, что это именно то, которое вам нужно. Если это так, сделайте все, что вы хотите сделать в качестве реакции на это сообщение, а затем возвратитесь, оставив в eax ноль. Если же это не то сообщение, которое вас интересует, вы ДОЛЖHЫ вызвать DefWindowProc, передав ей все параметры, которые вы до этого получили. DefWindowProc - это API функция , обрабатывающая сообщения, которыми ваша программа не интересуется.
Единственное сообщение, которое вы ОБЯЗАHЫ обработать - это WM_DESTROY. Это сообщение посылается вашему окну, когда оно закрывается. В то время, когда процедура окна его получает, окно уже исчезло с экрана. Это всего лишь напоминание, что ваше окно было уничтожено, поэтому вы должны готовиться к выходу в Windows. Если вы хотите дать шанс пользователю предотвратить закрытие окна, вы должны обработать сообщение WM_CLOSE. Относительно WM_DESTROY - после выполнения необходимых вам действий, вы должны вызвать PostQuitMessage, который пошлет сообщение WM_QUIT, что вынудит GetMessage вернуть нулевое значение в eax, что в свою очередь, повлечет выход из цикла обработки сообщений, а значит из программы.
Вы можете послать сообщение WM-DESTROY вашей собственной процедуре окна, вызвав функцию DestroyWindow.
[C] Iczelion, пер. Aquila.