
http://fasm.su/Lessons/03
В
прошлый раз мы познакомились с компилятором
FASM для Windows — рассмотрели основы его
синтаксиса и написали нашу первую
программу. Самые любопытные уже, наверное,
заглянули в папку EXAMPLES и обнаружили там
с дюжину готовых примеров различного
уровня сложности. Если вы еще не сделали
этого — быстренько открывайте
..\FASM\EXAMPLES\ и изучайте — даю вам 5 минут
на это! Время пошло.
Ну
что же, теперь мы можем приступать к
очередной тренировке ввода с клавиатуры
букв, цифр и знаков препинания. Ленивые
могут скопировать исходный код из этой
статьи. Помните анекдот про подставку
для кофе? Когда секретарша звонит
сисадмину и сообщает, что на ее компьютере
сломалась подставка для кофе. Админ, не
раздумывая, набирает другой номер и
говорит: "Петрович, у секрятаря CD-ROM
накрылся, надо заменить". Вот мы сейчас
и оформим программку управления
подставкой для кофе. За основу взят
пример, идущий в комплекте с виндовской
версией фасма.
format
PE GUI 4.0
include 'win32a.inc'
; секции
не обозначены, поэтому fasm автоматически
создаст секцию .flat
; в которой
разместятся и код, и данные, что позволит
уменьшить размер
файла
invokeMessageBoxA,0,_message,_caption,MB_ICONQUESTION+MB_YESNOCANCEL
cmpeax,IDNO
jeclose
cmpeax,IDYES
jneexit
;open:
invokemciSendString,_cd_open,0,0,0
jmpexit
close:
invokemciSendString,_cd_close,0,0,0
exit:
invokeExitProcess,0
_message
db 'Вам нужна подставка для кофе?',0
_caption
db 'Мастер Бытового Обслуживания.',0
_cd_open
db 'set cdaudio door open',0
_cd_close db 'set cdaudio door
closed',0
; импортируемые данные
разместятся в этой же секции:
data
import
library
kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL',\
winmm,'WINMM.DLL'
import
kernel32,\
ExitProcess,'ExitProcess'
import
user32,\
MessageBoxA,'MessageBoxA'
import
winmm,\
mciSendString,'mciSendStringA'
end data
В
от
такое окошко должно у нас получиться.
Нажмете кнопку "Да" — лоток CD-ROM'а
откроется. Нажмете "Нет" — закроется.
А сейчас — традиционное разбирательство
по вопросу "Как Это Работает":
Символ
"точка с запятой" (;) означает, что
в строке или в оставшейся ее части
размещен комментарий. Когда компилятор
встречает этот символ, то игнорирует
текст, идущий после точки с запятой, и
переходит к обработке следующей строки.
Исключением из этого правила является
точка с запятой, заключенная в кавычки.
Старайтесь всегда вносить в код пояснения
— в будущем это поможет вам не запутаться
в собственных программах и упростит
понимание ваших текстов другими людьми.
Как вы могли понять из комментария, в
этом примере секция кода и данных будет
скомпилирована в одну секцию. В нашем
случае это положительно скажется на
размере исполняемого файла, точнее,
отрицательно — короче, размер файла
будет меньше. Каждая отдельная секция
файла для ускорения доступа к ней
операционной системы округляется до
512 байт. Даже если секция будет содержать
лишь пару байт данных или кода, компилятор
все равно допишет недостающее число
нулевых байт. Значит, если у нас совсем
немного кода и данных, мы можем разместить
их в одной универсальной секции для
уменьшения размера получаемого файла.
Зачем вообще нужны эти секции? Ну, типа
для повышения надежности и безопасности
программного обеспечения. Каждая секция
при запуске программы получает свой
набор прав. Обычно секция данных может
быть прочитана и записана, но не может
быть исполнена, а секция кода имеет
разрешение на исполнение, но не может
быть перезаписана. Но это все формальности.
При желании можно найти и способы
изменения исполняемого кода, и исполнения
команд прямо из секции данных. Однако
не будем забегать слишком далеко вперед
и продолжим разбор нового материала.
Что
такое invoke MessageBox, вы знаете из предыдущего
занятия. Однако здесь вы видите
MessageBoxA. Не бойтесь, это ведь та же самая
API-функция. Просто в тот раз мы импортировали
ее командой import USER32,MessageBox,'MessageBoxA', а
теперь — командой import
user32,MessageBoxA,'MessageBoxA'. Название импортируемой
функции, заключенное в кавычки, должно
быть точным: оно будет передано
операционной системе в момент запуска
программы для получения адреса функции.
А вот псевдоним, стоящий через запятую
перед именем функции, может отличаться
от имени — например: import USER32, Box,
'MessageBoxA'. Только в таком случае вы усложните
понимание кода себе и другим людям. Так
что не принимайте это за сигнал к
действию, а просто имейте в виду, что
псевдоним и реальное имя функции могут
иногда различаться. Зачем же я изменил
псевдоним этой функции и заострил на
этом ваше внимание? Дело в том, что, читая
код программ, написанных другими людьми,
вы можете встретить как первый, так и
второй вариант. А в редких случаях и
третий, и еще какой-нибудь четвертый.
Вообще функции Windows, работающие с
текстовыми строками, бывают двух типов:
A (кодировка ANSI) и W (кодировка Юникод).
Мы в основном будем работать с кодировкой
ANSI, но вам следует знать, что у каждой
API-функции, использующей текст ANSI, есть
брат-близнец для кодировки Unicode. Итак,
вызов функции MessageBox выводит окно с
сообщением и приостанавливает работу
программы, ожидая реакцию пользователя.
По завершении функция возвращает
программе код нажатой пользователем
кнопки или возвращает 0, если не хватило
памяти для создания окна с сообщением.
Возвращаемые значения могут быть
следующими:
Псевдоним |
Значение |
Нажатая кнопка |
IDOK |
1 |
OK |
IDCANCEL |
2 |
Отмена (Cancel) |
IDABORT |
3 |
Прервать (Abort) |
IDRETRY |
4 |
Повтор (Retry) |
IDIGNORE |
5 |
Пропустить (Ignore) |
IDYES |
6 |
Да (Yes) |
IDNO |
7 |
Нет (No) |
) Так как же нам узнать, какая кнопка была нажата? Где найти это "возвращаемое значение", и под каким соусом его подавать на стол? Будем разбираться. В процессоре существуют ячейки высокоскоростной памяти, которые физически находятся вблизи его ядра. Эти ячейки называются регистрами. Если вы уже сейчас захотите узнать об этих регистрах более подробно, воспользуйтесь поиском в интернет (ключевые слова: регистры процессора). Однако на данный момент вам может хватить и приведенной здесь информации о регистрах. Основных регистров пользователя всего четыре: EAX(Accumulator), EBX(Base), ECX(Count), EDX(Data). Каждый из них имеет размер 4 байта (32 бита) и может использоваться в вычислениях целиком или частично. Например, можно обратиться к целому регистру EAX, можно работать с его младшей половинкой AX и даже с четвертинками AH и AL. Нельзя напрямую отдельно обратиться к старшей половинке регистра EAX, поэтому у нее нет собственного имени.
EAX (4 байта) |
|
|
|
AX (2 байта) |
|
|
AH(1) |
AL(1) |
Аналогично устроены и регистры EBX, ECX, EDX, а их части называются соответственно BX/BH/BL, CX/CH/CL, DX/DH/DL. Существуют и другие регистры процессора, но мы будем говорить о них по ходу их появления в наших примерах. Большинство функций Windows возвращают результаты своих действий в регистр процессора EAX. Функция MessageBox поступает так же: после того, как пользователь нажмет на кнопку в окне с сообщением, она поместит в регистр EAX числовое значение нажатой кнопки. Теперь нам надо в зависимости от полученного значения выполнить то или иное действие. Для этого мы будем использовать команду сравнения (CMP) и команды условного перехода (JE и JNE). CMP — сокращение от Compare (Сравнить). Синтаксис этой команды: [CMP приемник, источник]. Она сравнивает два числа, вычитая источник из приемника, не изменяя их содержимое. JE — Jump if Equal (Переход, если равно). JNE — Jump if Not Equal (Переход, если не равно). Это команды-антонимы — они противоположны по значению. Синтаксис JE, JNE и других команд перехода такой: [JE метка]. Команды условного перехода используются после команд CMP и SUB (вычитание с сохранением результата в приемник). Переход на метку осуществляется только если соблюдено условие перехода. Если условие не соблюдено, то программа продолжает выполняться в обычном порядке. Команда JMP (Jump) является командой безусловного перехода, то есть переход на указанную метку осуществляется в любом случае. В нашей программе мы сначала сравниваем содержимое eax и IDNO (эквивалент числа 7) и переходим на метку close, если они равны (JE). Иначе — сравниваем eax и IDYES (эквивалент числа 6) и переходим на метку exit, если они не равны (JNE). По этой логике программа выполнит строки invoke mciSendString,_cd_open,0,0,0 jmp exit только при условии, что содержимое eax после функции MessageBox будет равно IDYES. Если eax=IDNO, то выполняются команды после метки close включая команды после метки exit. Если результат не равен ни IDYES, ни IDNO, то выполняется вызов функции завершения работы программы: invoke ExitProcess,0. API-функция mciSendString отправляет командную строку устройству MCI (Media Control Interface). Устройство, которому отправляется команда, должно быть определено в командной строке. У функции четыре параметра: указатель на командную строку, указатель на буфер для ответа, размер буфера для ответа (количество символов), хэндл окна, которому отправляется напоминание при ответе (для этого в командной строке должен присутствовать параметр notify). В нашем случае мы обойдемся без получения ответов от устройства, поэтому вместо последних трех параметров ставим нолики. А вот первый параметр — это целый раздел в интернет-библиотеке мелкомягких (MSDN), поэтому я приведу вам лишь общую информацию о командных строках мультимедиа. Если вы знаете английский, то можете ознакомиться с полной версией описания этих команд по адресу: сайт Командная строка MCI состоит из трех основных частей: команда, устройство, параметры. Эти части должны разделяться пробелами. Команда в нашем конкретном случае — это set. Бывают команды open, play, close и другие. Устройство у нас — cdaudio. Устройством также может являться полное имя файла, псевдоним, установленный параметром alias предшествующей команды open, слово new в команде open при открытии устройства на запись, слово all — для отправки команды всем открытым в программе устройствам. Параметры разделяются пробелами, перечисляются в произвольном порядке и могут вообще отсутствовать в некоторых командах. Теперь мы можем добавить к нашей сегодняшней программе звуковое сопровождение. Для этого необходимо дописать строку invoke mciSendString,_wav_play,0,0,0 прямо перед вызовом функции MessageBox и строку _wav_play db 'play c:\windows\media\tada.wav',0 где-нибудь после вызова функции ExitProcess, но перед импортом данных. Если у вас папка с windows находится в другом месте — укажите свой путь. Можете вообще указать путь к другому wav-файлу или даже попробовать другие форматы: все зависит от кодеков, установленных в вашей windows. На моей системе прокатило даже воспроизведение видео! Подобным образом вы можете озвучить открытие и закрытие лотка дискового привода. Только имейте в виду, что, если программа завершит работу раньше, чем закончится выбранный вами звук, то воспроизведение прервется тоже. Например, если поставить строку с командой воспроизведения прямо перед выходом из программы (ну перед invoke ExitProcess), то вы вообще не услышите никакого звука. Он попросту не успеет начаться, когда ему уже пора будет заканчиваться. Для такого случая предусмотрен параметр wait. Если в командной строке мультимедиа указан этот параметр, то MCI вернет управление программе только после полного исполнения команды. Будьте осторожны: если звук будет слишком длинный, вы рискуете надолго "подвесить" вашу программу в ожидании окончания воспроизведения. Пример строки с использованием параметра wait: _wav_play db 'play c:\windows\media\tada.wav wait',0 Ну, а теперь переделаем нашу программку в полезную утилиту. Программа будет проверять статус лотка CD-ROM'а (открыт/закрыт) и изменять его на противоположный. В таком случае мы сможем вывести на рабочий стол ярлык для нашей программы, выбрать для него какой-нибудь подходящий значок (в свойствах ярлыка) и использовать его почти как кнопку "открыть/закрыть" на самом приводе: format PE GUI 4.0 include 'win32a.inc' invoke mciSendString,_cd_state,_ret,5,0 invoke lstrcmp,_ret,_ret_open cmp eax,0 je close ;open: invoke mciSendString,_cd_open,0,0,0 jmp exit close: invoke mciSendString,_cd_close,0,0,0 exit: invoke ExitProcess,0 _cd_state db 'status cdaudio mode',0 _cd_open db 'set cdaudio door open',0 _cd_close db 'set cdaudio door closed',0 _ret_open db 'open',0 _ret db 5 dup (?) data import library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL',\ winmm,'WINMM.DLL' import kernel32,\ ExitProcess,'ExitProcess',\ GetWindowsDirectory,'GetWindowsDirectory',\ lstrcmp,'lstrcmpA' import user32,\ MessageBoxA,'MessageBoxA' import winmm,\ mciSendString,'mciSendStringA',\ PlaySound,'PlaySoundA' end data В самом начале мы отправляем запрос о состоянии устройства cdaudio, поэтому указываем буфер для ответа и его размер. API-функция lstrcmp используется для посимвольного сравнения двух текстовых строк. Если строки одинаковые, она возвращает значение ноль. _ret_open — это указатель на строку-образец из пяти символов (нуль-терминатор на конце строки тоже считается). _ret — это указатель на пустой буфер из 5 байт. Только к моменту сравнения он уже не будет пустым: после вызова mciSendString в буфер будет помещено 5 символов из ответа о текущем состоянии CD-ROM'а — точнее, 4 буквы и завершающий строку нолик. Для слова open нам вполне хватит четырех букв, если же ответ будет другой и займет больше символов, нас это не волнует: не open — значит, открываем. Поэтому, если строки равны, и в eax находится ноль, мы переходим на метку close. Иначе — открываем лоток и проходим к выходу. db 5 dup (?) резервирует место под 5 неизвестных байт. Можно записать это иначе: rb 5 (reserve 5 bytes). Зарезервированные (неинициализированные) данные не занимают место в файле. Они займут свои места только после запуска в оперативной памяти. Их обычно используют, когда значение заранее не известно. Запись вида db 5 dup (1,2) приведет к созданию пяти копий (duplicate) указанной в скобках последовательности байт. Теперь можно щелкнуть правой кнопкой на получившемся экзешнике и выбрать "Отправить — Рабочий стол (создать ярлык)", в свойствах ярлыка выбрать иконку (сменить значок) и назначить комбинацию клавиш для быстрого вызова — например, Ctrl+Shift+C. При таком раскладе можно будет открыть или закрыть CD-ROM, кликнув на ярлык, или одновременным нажатием выбранной комбинации клавиш. Правда, иногда, MCI долго "думает" прежде чем передать команду CD-ROM'у, но тут уж ничего не попишешь с нашими сегодняшними знаниями. Возможно, когда-нибудь вы напишете свой драйвер, который будет работать напрямую с любым устройством, а может, и целую операционную систему. Но сегодня мы прощаемся. Желаю вам успешно разобраться в пройденном материале, и до новых встреч!
Сегодня мы научимся создавать окно. Окно — это достаточно сложная, но в то же время очень важная штуковина в Windows. Windows (от англ.: окна) потому так и называется, что большинство операций в ней производится с окнами программ. Так удобнее всего работать с множеством приложений. Может быть, когда-нибудь на смену окнам придут кубики или шарики, но на современных компьютерах пользователю удобнее всего работать с окнами. Вывод: для создания удобной в использовании программы нам надо научиться создавать окна. Этим мы сегодня и займемся. Сегодняшний урок будет заметно сложнее предыдущих, но я верю, что вы справитесь. Запускаем компилятор FASMW и вводим следующий код программы: format PE GUI 4.0 entry start include 'win32a.inc' section '.data' data readable writeable _class db 'FASMWIN32',0 _title db 'Пустое Окно',0 _error db 'Ошибка',0
wc WNDCLASS 0,WindowProc,0,0,0,0,0,COLOR_BTNFACE+1,0,_class
msg MSG
section '.code' code readable executable
start:
invoke GetModuleHandle,0
mov [wc.hInstance],eax
invoke LoadIcon,0,IDI_APPLICATION
mov [wc.hIcon],eax
invoke LoadCursor,0,IDC_ARROW
mov [wc.hCursor],eax
invoke RegisterClass,wc
cmp eax,0
je error
invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU,128,128,256,192,0,0,[wc.hInstance],0
cmp eax,0
je error
msg_loop:
invoke GetMessage,msg,0,0,0
cmp eax,0
je end_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop
error:
invoke MessageBox,0,_error,0,MB_ICONERROR+MB_OK
end_loop:
invoke ExitProcess,[msg.wParam]
proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
cmp [wmsg],WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmdestroy:
invoke PostQuitMessage,0
mov eax,0
.finish:
pop edi esi ebx
ret
endp
section '.idata' import data readable writeable
library kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL'
include 'api\kernel32.inc'
include 'api\user32.inc'
Столько кода, и ничего особенного в результате. В MessageBox хотя бы присутствовала кнопка ОК, а тут — пустое окно. Не волнуйтесь: как только мы разберемся с устройством простейшего окна, мы сразу же перейдем к навешиванию на него прочих элементов. Используя MessageBox, мы были ужасно ограничены набором стандартных параметров этой функции. Кроме того, программа не могла продолжать исполняться до тех пор, пока пользователь не прореагирует нажатием кнопки в окошке или его закрытием. В случае использования окна программа может исполняться, параллельно реагируя на действия пользователя. Сразу будет нелегко понять алгоритм работы окна, но, как только вы его поймете, все остальное пойдет намного легче. Приступаем к разбору полетов. В секции данных после объявления трех текстовых строк (класс окна, его заголовок, сообщение об ошибке) объявляются две структуры данных. Первая — WNDCLASS — структура класса окна. Она описана в файле FASM\INCLUDE\EQUATES\USER32.INC (смотрим файл прямо в компиляторе или через блокнот — кому как удобно) и содержит 10 атрибутов класса окна: style — стиль окна; lpfnWndProc — указатель на процедуру обработки сообщений, посланных окну; cbClsExtra — количество дополнительных байт в памяти для данной структуры; cbWndExtra — количество дополнительных байт в памяти для данных, присоединенных к окну; hInstance — идентификатор приложения, создавшего окно; hIcon — идентификатор иконки окна; hCursor — идентификатор курсора окна; hbrBackground — цвет фона окна; lpszMenuName — указатель на имя меню окна; указатель на имя класса. Вторая — MSG — структура сообщения. Описана там же, где и первая. Состоит из шести элементов: hwnd — идентификатор окна — получателя сообщения; message — сообщение; wParam — дополнительная информация о сообщении; lParam — дополнительная информация о сообщении; time — время отправки сообщения; pt — координаты курсора на время отправки сообщения. Последний элемент, кстати, тоже является структурой, которая описана в том же файле. Это наглядный пример вложенной структуры. Не старайтесь все сразу запомнить — просто примите к сведению. После объявления структур wc будет считаться указателем на первый байт структуры данных, созданной в соответствии с шаблоном WNDCLASS, а msg — указателем на MSG. Элементам структуры wc присваиваются соответствующие значения, разделенные запятыми (в основном пока что ноли). Значения элементов структуры msg считаются сейчас неопределенными — хотя на практике они будут иметь нулевые значения, правильно считать их значения неопределенными. В тексте программы можно обращаться к отдельным элементам структуры, например, wc.hCursor или wc.hIcon, хотя в оперативной памяти все данные будут просто идти друг за другом безо всяких пометок. Элементы структур для 32-битных версий windows обычно имеют размер 32 бита — это 4 байта или двойное слово (dword). Поэтому в описании структуры написано dd (data in dwords). Следовательно, мы могли бы заменить, например, wc.hInstance на wc+16, потому что элементы структуры wc в адресном пространстве будут выглядеть примерно так: Элемент Адрес wc.style wc+0 wc.lpfnWndProc wc+4 wc.cbClsExtra wc+8 wc.cbWndExtra wc+12 wc.hInstance wc+16 wc.hIcon wc+20 wc.hCursor wc+24 wc.hbrBackground wc+28 wc.lpszMenuName wc+32 wc.lpszClassName wc+36 М ожно было бы сейчас не вдаваться в эти подробности, но лучше, если вы сразу получите правильное представление о размещении данных в памяти. Теперь попробуем разобраться с исполняемым кодом (section '.code'). Функция GetModuleHandle возвращает в eax идентификатор исполняемого модуля. Она имеет всего один параметр — указатель на строку — имя модуля (exe или dll). Если параметр равен нулю, то функция возвращает идентификатор вызвавшего функцию модуля, то есть нашей программы. Этот идентификатор нужен нам для заполнения элемента wc.hInstance, поэтому следующей командой мы сразу помещаем в этот элемент содержимое eax. LoadIcon загружает указанную иконку из ресурсов исполняемого файла. Первый параметр — идентификатор исполняемого файла или ноль для загрузки стандартной иконки. Второй параметр — строка — имя иконки из ресурсов либо идентификатор стандартной иконки, если первый параметр ноль. В нашем случае иконка стандартная. IDI_APPLICATION — это константа — ее и идентификаторы других стандартных иконок вы легко отыщете все в том же EQUATES\USER32.INC. Возвращаемое значение — идентификатор загруженной иконки, который мы тут же помещаем в wc.hIcon. LoadCursor работает по аналогии. Идентификаторы стандартных курсоров вы найдете чуть выше идентификаторов иконок. Теперь, когда все необходимые данные структуры wc находятся на своих местах, вызывается функция RegisterClass. Единственный параметр этой функции — указатель на структуру, содержащую описание класса окна. Если класс был успешно зарегистрирован, возвращаемое значение будет отлично от нуля. Если по каким-либо причинам не удалось зарегистрировать класс, то в eax вернется ноль. Исходя из этого, мы сравниваем eax и 0 (cmp) и в случае равенства прыгаем на метку ошибки (je error). Если не равно нулю — значит, все в порядке, и мы переходим к созданию окна. Функция CreateWindowEx создает окно. Параметры соответственно: расширенный стиль окна; указатель на зарегистрированное имя класса; указатель на имя окна; стандартный стиль окна; X координата левого верхнего угла окна; Y координата левого верхнего угла окна; ширина окна; высота окна; идентификатор родительского окна или окна-владельца; идентификатор меню или дочернего окна; идентификатор исполняемого модуля, с которым связано окно; указатель на значение, которое передается окну через структуру CREATESTRUCT в параметре lParam сообщения WM_CREATE. Возвращаемое значение — идентификатор созданного окна. В случае ошибки возвращается ноль. Названия расширенных стилей окна вы можете найти в файле EQUATES\USER32.INC в группе Extended Window Styles, названия стандартных стилей находятся чуть выше в группах Window Styles и Common Window Styles. Далее следует цикл msg_loop. Этот цикл будет повторяться до тех пор, пока окно не будет закрыто. Функция GetMessage получает сообщение из очереди сообщений приложения. Если сообщения отсутствуют, функция ожидает их, и цикл приостанавливается до появления нового сообщения. Сообщения посылаются окну операционной системой, когда с окном происходит какое-либо действие — например, когда окно перемещается, изменяется его размер или даже когда курсор мыши просто движется над областью окна. Также окну могут передаваться сообщения от других процессов. Параметры функции следующие: указатель на структуру, в которой разместятся элементы сообщения; идентификатор окна — получателя сообщения: если ноль, то сообщения принимаются для любого окна данного приложения; минимальное значение сообщения; максимальное значение сообщения. Последние два параметра исполняют роль фильтра сообщений. Так как все сообщения являются целочисленными значениями, можно установить фильтрацию типа "от… и до…" Если максимальное и минимальное значения равняются нулю, фильтрация не выполняется, и принимаются все сообщения без исключения. Если функция получает сообщение WM_QUIT (выход), то возвращает ноль. В других случаях eax не будет равен нулю. Следовательно, если eax равен нулю, мы выходим из цикла (je end_loop), иначе цикл продолжается. TranslateMessage переводит комбинации wm_KeyDown/Up в wm_Char или wm_DeadChar, а комбинации wm_SysKeyDown/Up — в wm_SysChar или wm_SysDeadChar — и отправляет переведенное сообщение снова в очередь. Таким образом в процедуру обработки сообщений поступят и виртуальные клавиши и их символьные значения. DispatchMessage передает сообщения процедуре обработки сообщений (WindowProc). В газетной статье нет возможности привести список всех сообщений и их описание, поэтому я рекомендую вам скачать англоязычную справку по API-функциям размером около 22 Мб: сайт . К сожалению, нормального аналогичного справочника на русском языке я вам посоветовать не могу. Так уж сложилось, что не в моде у программистов русский язык. П роцедура обработки сообщений WindowProc вызывается каждый раз при получении окном нового сообщения и передаче его через DispatchMessage. Синтаксис записи этой и других процедур предельно прост: макроинструкция proc, имя процедуры и ее параметры через запятую, которые по умолчанию считаются 32-битными (4 байта каждый). Возврат из процедуры осуществляет команда ret. Каждая процедура должна завершаться инструкцией endp, которая указывает компилятору, где заканчивается код данной процедуры и начинается следующий фрагмент кода. Команда push помещает значение указанного регистра в стек, а команда pop извлекает последнее значение из стека в указанный регистр. Стек — это специально выделенная область памяти для передачи или сохранения данных. Его можно представить, например, как вертикальную штангу тренажера, на которую можно одевать по одному грузу и снимать тоже по одному. Команда push будто указывает, откуда взять груз, а команда pop сообщает, куда поместить груз, снятый со штанги. Такую аналогию я привел для того, чтобы вы уже таки уяснили, что, если мы хотим сохранить содержимое нескольких регистров в стек, а потом вернуть эти значения в регистры, то нам необходимо сохранять в одном порядке, а извлекать — в обратном. Это важно запомнить и не путать: если сохраняем push eax ebx ecx — извлекаем pop ecx ebx eax. А сохраняем мы содержимое этих регистров, чтобы предотвратить потерю данных: функция DefWindowProc спокойно может затереть содержимое этих регистров. Так как окно у нас простейшее, и на нем нет никаких кнопок, кроме крестика для закрытия, единственное сообщение, которое мы должны обработать — это WM_DESTROY, которое будет послано окну перед его удалением. Стало быть, если сообщение равно WM_DESTROY, мы прыгаем на метку .wmdestroy, где будет вызвана функция PostQuitMessage, параметром которой является код завершения. Ноль означает, что программа самостоятельно завершает работу. Эта функция посылает нашему процессу сообщение WM_QUIT, после обработки которого функция GetMessage вернет ноль, и цикл обработки сообщений прервется переходом на end_loop — выход из программы. Если сообщение не WM_DESTROY, то выполняется следующая функция — DefWindowProc. Эта функция обычно вызывается после того, как обработаны все предусмотренные нами сообщения (в нашем случае предусмотрено только одно, но их ведь может быть и сотня), для того, чтобы операционная система своими средствами произвела стандартную обработку сообщения, которое получило наше окно. Параметры функции: идентификатор окна — получателя сообщения; сообщение; дополнительная информация о сообщении; дополнительная информация о сообщении. Что ж, теперь мы можем навесить на наше окно кнопку. Для этого добавим в секцию данных (section '.data') ее класс и имя, а заголовок главного окна немного подправим: _title db 'НеПустое Окно',0 _classb db 'BUTTON',0 _textb db 'КНОПКА',0 В секцию кода добавим обработку сообщения WM_CREATE, которое приходит окну один раз при его создании, чтобы при создании главного окна создавать дочерние окна. Кнопка — это окно стандартного класса BUTTON, поэтому регистрировать этот класс нам не понадобится. Также добавим обработку сообщения WM_COMMAND, которое приходит окну, когда пользователь выбирает пункт меню или совершает действие с другим дочерним элементом окна (в нашем случае — нажатие на кнопку): proc WindowProc hwnd,wmsg,wparam,lparam push ebx esi edi cmp [wmsg],WM_CREATE je .wmcreate cmp [wmsg],WM_COMMAND je .wmcommand cmp [wmsg],WM_DESTROY je .wmdestroy .defwndproc: invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam] jmp .finish .wmcreate: invoke CreateWindowEx,0,_classb,_textb,WS_VISIBLE+WS_CHILD+BS_PUSHBUTTON, 10,10,100,50,[hwnd],1001,[wc.hInstance],NULL jmp .finish .wmcommand: cmp [wparam],1001 jne .finish invoke MessageBox,[hwnd],_textb,_title,0 jmp .finish .wmdestroy: invoke PostQuitMessage,0 mov eax,0 .finish: pop edi esi ebx ret endp Теперь при создании окна выполнятся команды после метки .wmcreate, и на главном окне будет создана кнопка с идентификатором 1001. При нажатии на эту кнопку окно получает сообщение WM_COMMAND, а wparam сообщения будет содержать в старших двух байтах BN_CLICKED (кликнута кнопка), а в младших двух байтах — идентификатор кнопки (1001). Так как константа BN_CLICKED равна нулю (убеждаемся в этом в EQUATES\USER32.INC), можно не учитывать ее и просто сравнить wparam с 1001, чтобы убедиться, что была нажата наша кнопка. Если не равно — значит, не наша или не нажата — jne .finish . Иначе — показываем мессадж-бокс. По аналогии можете самостоятельно добавить еще пару кнопок, только не забывайте изменять координаты их местоположения, иначе они будут накладываться друг на друга, и вы увидите лишь одну из них. Поэкспериментируйте со стилями и другими параметрами. К следующему занятию постарайтесь четко усвоить принципы создания и работы простых окон, чтобы мы смело могли двигаться дальше.
В прошлый раз мы научились создавать окно с кнопкой. Сегодня продолжим тему про окна: будем учиться навешивать на окно другие элементы. Заодно и разберем основы ввода и вывода данных в окнах. В большинстве программ необходимо обеспечивать ввод и вывод данных. То есть то, что вы набрали на клавиатуре, должно каким-то образом попадать в память для последующей обработки, а также данные из памяти необходимо выводить на экран. Windows предоставляет широчайший набор функций для ввода и вывода данных, однако мы рассмотрим один из самых простых и популярных методов: использование поля для редактирования (окно стандартного класса EDIT). Как обычно, подробности будут ниже по тексту, а сейчас — вводим код программы:
format PE GUI 4.0
entry start
Include 'win32a.Inc'
section '.data' data readable writeable
class db 'FASMWIN32',0
title db 'ОКНО',0
classb db 'BUTTON',0
classe db 'EDIT',0
classs db 'STATIC',0
textb1 db 'Копировать',0
textb2 db 'Очистить',0
textg db 'Рамка',0
texts db 'Текст',0
textc db 'Очистить все',0
errtxt db 'Ошибка',0
hwnd dd ?
hwnde dd ?
hwnds dd ?
hwndc dd ?
text rb 100
wc WNDCLASS 0,WindowProc,0,0,0,0,0,COLOR_BTNFACE+1,0,class
msg MSG
section '.code' code readable executable
start:
invoke GetModuleHandle,0
mov [wc.hInstance],eax
invoke LoadIcon,0,IDI_APPLICATION
mov [wc.hIcon],eax
invoke LoadCursor,0,IDC_ARROW
mov [wc.hCursor],eax
invoke RegisterClass,wc
cmp eax,0
je error
invoke CreateWindowEx,0,class,title,WS_VISIBLE+ WS_SYSMENU,128,128,256,192,0,0,[wc.hInstance],0
cmp eax,0
je error
mov [hwnd],eax
msg_loop:
invoke GetMessage,msg,0,0,0
cmp eax,0
je end_loop
invoke IsDialogMessage,[hwnd],msg
cmp eax,0
jne msg_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop
error:
invoke MessageBox,0,errtxt,0,MB_ICONERROR+MB_OK
end_loop:
invoke ExitProcess,[msg.wParam]
proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
cmp [wmsg],WM_CREATE
je .wmcreate
cmp [wmsg],WM_COMMAND
je .wmcommand
cmp [wmsg],WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmcreate:
invoke CreateWindowEx,0,classb,textg,WS_VISIBLE+ WS_CHILD+ BS_GROUPBOX,5,5,240,150,[hwnd],1000,[wc.hInstance],0
invoke CreateWindowEx,0,classb,textb1,WS_VISIBLE+WS_CHILD+ BS_PUSHBUTTON+WS_TABSTOP,20,100,100,40,[hwnd],1001,[wc.hInstance],0
invoke CreateWindowEx,0,classb,textb2,WS_VISIBLE+WS_CHILD+ BS_PUSHBUTTON+WS_TABSTOP,130,100,100,40,[hwnd],1002,[wc.hInstance],0
invoke CreateWindowEx,0,classe,texts,WS_VISIBLE+WS_CHILD+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL,10,25,230,20,[hwnd],1003,[wc.hInstance],0
mov [hwnde],eax
invoke CreateWindowEx,0,classe,0,WS_VISIBLE+WS_CHILD+WS_BORDER+WS_TABSTOP+ES_AUTOHSCROLL+ES_READONLY,10,50,230,20,[hwnd],1004,[wc.hInstance],0
mov [hwnds],eax
invoke CreateWindowEx,0,classb,textc,WS_VISIBLE+WS_CHILD+ BS_AUTOCHECKBOX+WS_TABSTOP,130,75,110,20,[hwnd],1005,[wc.hInstance],0
mov [hwndc],eax
invoke CreateWindowEx,0,classs,textg,WS_VISIBLE+ WS_CHILD,190,145,43,15,[hwnd],1006,[wc.hInstance],0
invoke SetFocus,[hwnde]
jmp .finish
.wmcommand:
cmp [wparam],1001
je .but1
cmp [wparam],1002
je .but2
jmp .finish
.but1:
invoke SendMessage,[hwnde],WM_GETTEXT,100,text
invoke SendMessage,[hwnds],WM_SETTEXT,0,text
jmp .finish
.but2:
invoke SendMessage,[hwnds],WM_SETTEXT,0,0
invoke SendMessage,[hwndc],BM_GETCHECK,0,0
cmp eax,BST_CHECKED
jne .finish
invoke SendMessage,[hwnde],WM_SETTEXT,0,0
invoke SendMessage,[hwndc],BM_SETCHECK,BST_UNCHECKED,0
jmp .finish
.wmdestroy:
invoke PostQuitMessage,0
mov eax,0
.finish:
pop edi esi ebx
ret
endp
section '.idata' import data readable writeable
library kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL'
include 'api\kernel32.inc'
include 'api\user32.inc'
Е сли вы все верно набрали, то при запуске у вас появится окошко, как на рис. 1. Понимаю, что набирать большие тексты вручную и без ошибок достаточно трудно, а посему, как обычно, выкладываю копию исходного текста в интернет. Ссылка в конце статьи. Теперь же будем разбирать новый материал. В секции данных внимательный читатель предыдущих статей не найдет ничего нового, но на всякий случай поясню. Инструкции, содержащие директиву db (данные в байтах), объявляют текстовые строки с завершающим ноликом в конце. Инструкции с dd — двойные слова (двойное слово = 4 байта = 32 бита), которые мы резервируем в данном случае для хранения дескрипторов некоторых окон (hwnd — так обычно сокращают handle of window). Инструкция text rb 100 резервирует 100 байт под переменную для хранения текста. Будет вам мало 100 байт — выделите больше. Ну и две структуры данных: WNDCLASS и MSG, о которых подробно говорили в прошлый раз. Окно создается практически как в программе из прошлой статьи. Только теперь нам необходимо сохранить его дескриптор в переменную для последующего использования: mov [hwnd],eax. В цикл обработки сообщений теперь добавлен вызов функции IsDialogMessage. Эта функция определяет, адресовано ли сообщение указанному блоку диалога, и если да — обрабатывает сообщение. Ее параметры: идентификатор окна или блока диалога и указатель на структуру данных MSG. Если сообщение не было обработано, то возвращаемое значение — ноль. Иначе — сообщение было обработано функцией, и нам не надо его повторно обрабатывать. Поэтому, если после выполнения функции eax не равен нолю, цикл начинается сначала, а если ноль, то обработка сообщения продолжается. Использование этой функции требуется, например, для правильной обработки переключения между элементами, имеющими стиль WS_TABSTOP, по клавише Tab. Уберите ее из цикла (вместе с cmp eax,0 и jne msg_loop), и переключение не будет осуществляться. В прошлый раз после метки .wmcreate у нас создавалось лишь одно дочернее окно — кнопка. Теперь у нас их целых семь: рамка (GroupBox), две кнопки (PushButton), два поля редактирования (EditBox), флажок (CheckBox), надпись (Static). Надпись, кстати, имеет такой же текст, что и рамка, и поставил я ее в нижней части рамки. Не перепутайте их. Чтобы определиться, что есть где, можете убрать из параметра стиля WS_VISIBLE вместе с плюсиком и проверить, что исчезло из окна. WS_VISIBLE указывает, что окно создается видимым для пользователя. WS_CHILD обозначает дочернее окно. WS_BORDER создает окно в рамочке. WS_TABSTOP разрешает переход на данное окно клавишей Tab. В общем, приставка WS_ — это стиль окна (Window Style), BS_ — стиль кнопки (окно класса BUTTON), ES_ — Edit Style и т.д. Причем наше первое дочернее окно — рамка — тоже является окном класса BUTTON со стилем BS_GROUPBOX. Ее основное предназначение — группировка кнопок в логический блок. Заголовок такого окна (при наличии) отображается в левом верхнем углу рамки. ES_AUTOHSCROLL разрешает горизонтальную прокрутку текста, когда он выходит за пределы окна. ES_READONLY запрещает редактирование текста. Я уже говорил о нецелесообразности описания всех стилей в одной статье, поэтому назначение других стилей вам придется узнавать самостоятельно или по ходу рассмотрения их в наших занятиях. Функция CreateWindowEx в случае успешного завершения возвращает в eax дескриптор созданного окна. Дескрипторы некоторых окон нам понадобятся для последующих операций с ними, поэтому мы сохраняем дескрипторы этих окон в переменные hwnde, hwnds и hwndс. Функция SetFocus активирует указанное окно для ввода с клавиатуры. Поэтому при запуске программы курсор текстового ввода уже приветливо мигает нам из поля ввода текста. Ну вот мы и добрались до ядра нашей программы. При нажатии кнопки Копировать происходит переход на метку .but1. Функция SendMessage отправляет сообщение указанному окну. Ее параметры — соответственно дескриптор окна-получателя; собственно сообщение; первый параметр сообщения; второй параметр сообщения. WM_GETTEXT посылается окну, чтобы скопировать текст окна в указанный во втором параметре буфер. Для этого сообщения в первом параметре необходимо указать максимальное количество копируемых символов (включая завершающий строку ноль), а во втором — указатель на переменную-буфер, в которую будет помещена строка. WM_SETTEXT устанавливает новым текстом окна строку из буфера, указанного во втором параметре. Первый параметр в этом сообщении не используется, и потому должен быть нулем. Таким образом, на метке .but1 мы копируем до сотни символов из окна hwnde в переменную text, а затем устанавливаем этот текст в качестве нового текста окна hwnds. Причем первая команда наглядно показывает простой метод ввода данных в оперативную память компьютера, а вторая — вывода данных из оперативной памяти в окно программы. По нажатию кнопки Очистить мы переходим на метку .but2. Первой командой мы устанавливаем "пустой" текст в окне hwnds. Далее мы используем сообщение BM_GETCHECK, предназначенное для проверки состояния флажков (check box) и радиокнопок (radio button). Возвращаемые значения могут быть следующими: BST_CHECKED, BST_UNCHECKED и BST_INDETERMINATE — отмечено, не отмечено и не определено. Параметры не используются. Если состояние не BST_CHECKED — переход на метку .finish, иначе — выполняем следующие команды: устанавливаем "пустой" текст в окне hwnde и снимаем флажок — сообщение BM_SETCHECK — выставить состояние флажка или радиокнопки в соответствии с первым параметром. Второй параметр не используется. Очень удобно ввод данных в программу из какого-либо файла производить перетаскиванием пиктограммы файла на окно программы (Drag and Drop). Для этого прежде всего необходимо разрешить окну получать перетаскиваемые файлы. Это можно сделать, установив соответствующий расширенный стиль окна: invoke CreateWindowEx,WS_EX_ACCEPTFILES,class,title, WS_VISIBLE+WS_SYSMENU,128,128,256,192,0,0,[wc.hInstance],0 В процедуру обработки сообщений добавим обработку сообщения о получении файлов: … … … cmp [wmsg],WM_DROPFILES je .drop … … … .drop: invoke DragQueryFile,[wparam],-1,text,100 invoke wsprintf,text,form,eax invoke SendMessage,[hwnds],WM_SETTEXT,0,text invoke DragQueryFile,[wparam],0,text,100 invoke SendMessage,[hwnde],WM_SETTEXT,0,text jmp .finish … … … Функция DragQueryFile содержится в библиотеке shell32, поэтому в секцию импортируемых данных добавляем импорт shell32: section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL',\ shell32,'SHELL32.DLL',\ user32,'USER32.DLL' include 'api\kernel32.inc' include 'api\shell32.inc' include 'api\user32.inc' Ну и для вывода сообщения о количестве полученных файлов добавим образец формата в секцию данных: form db 'получено файлов: %d',0 Теперь при перетаскивании файла или нескольких файлов в область главного окна окно получает сообщение WM_DROPFILES, и программа переходит на метку .drop. Функция DragQueryFile сообщает в указанный буфер имя полученного файла. Ее параметры следующие: идентификатор структуры, содержащей полученные файлы, сообщенный программе в первом параметре WM_DROPFILES; порядковый номер полученного файла; буфер для ответа; размер буфера. Следует обратить внимание, что, если указать в качестве порядкового номера -1, то в регистр eax будет возвращено количество полученых файлов. Отсчет порядковых номеров начинается с нуля, то есть ноль — это первый полученный файл. Если по верно указанному порядковому номеру в буфер возвращается ноль — значит, в буфере недостаточно места для размещения имени файла, и в eax возвращается необходимый размер в символах без учета завершающего строку нуля. Иначе в eax возвращается число скопированных символов без учета завершающего нуля. Функция wsprintf форматирует блоки данных в соответствии с указанным форматом и помещает отформатированную строку в буфер ответа. Параметры: указатель на буфер для ответа; указатель на образец формата; один или несколько блоков данных через запятую, которые будут отформатированы и помещены в буфер ответа. При успешном выполнении в eax возвращается количество символов, помещенных в буфер ответа. Простейший образец формата — строка, содержащая символ процента и символ — тип формата. %d обозначает формат десятичного числа с учетом знака (эквивалент %i), %u — целое число без учета знака, %x и %X — шестнадцатеричное число соответственно в нижнем и верхнем регистре, %s — строка символов (рис. 2). Таким образом, если перетащить файл или файлы на главное окно нашей программы, в окно hwnds будет помещена строка, сообщающая количество полученных файлов, а в окне hwnde отобразится имя первого из полученных файлов. Теперь вы имеете общее представление о том, как можно открыть тот или иной файл в программе, просто перетащив его на окно этой программы. Многое еще не ясно, но мы обязательно поговорим об этом в следующих статьях.
Итак, основная задача на сегодня — меню. Второстепенные задачи будем привинчивать к основному коду. Меню — это достаточно важный компонент окна. Просмотрев пункты меню какой-нибудь новой или малоизвестной программы, пользователь должен иметь возможность получить общее представление о назначении программы и ее функционале. Чтобы меню было интуитивно понятно пользователю, программисту необходимо придерживаться негласного стандарта расположения элементов меню. Это значит, что сначала идут пункты Файл, Правка, Вид, а в конце — Справка. Меню является одним из типов ресурсов. Бывают такие ресурсы, как иконки, картинки bmp, курсоры и т.д. Традиционно ресурсы описываются в отдельном файле с расширением .rc и компилируются в файл .res компилятором ресурсов. Компилятор FASM нарушает традиции предков и позволяет размещать описание ресурсов прямо в исходном коде — в отдельной секции ресурсов. Для правильного отображения русских символов из ресурсов необходимо подключать макрос 'encoding\WIN1251.INC' (Code Page 1251 — это стандартная 8-битная кодировка для русских версий windows). На мой взгляд, очень удобно описывать ресурсы прямо в тексте программы, однако бывают ситуации, когда гораздо проще и быстрее подключить уже готовый res-файл к тексту программы — у вас есть полное на это право и возможность. Для этого используется директива вида: section '.rsrc' resource from 'resfile.res' data readable, где .rsrc — это название секции, а resfile.res — скомпилированный файл ресурсов. Теперь извольте ознакомиться с примером, в котором ресурсы описаны прямо в тексте программы:
format PE GUI 4.0
entry start
include 'win32a.inc'
include 'encoding\WIN1251.INC'
section '.data' data readable writeable
class db 'FASMWIN32',0
title db 'ОКНО',0
hwnd dd ?
wc WNDCLASS 0,WindowProc,0,0,0,0,0,COLOR_BTNFACE+1,0,class
msg MSG
section '.code' code readable executable
start:
invoke GetModuleHandle,0
mov [wc.hInstance],eax
invoke LoadIcon,0,IDI_APPLICATION
mov [wc.hIcon],eax
invoke LoadCursor,0,IDC_ARROW
mov [wc.hCursor],eax
invoke RegisterClass,wc
cmp eax,0
je error
invoke LoadMenu,[wc.hInstance],1
invoke CreateWindowEx,0,class,title,WS_VISIBLE+WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,\
0,eax,[wc.hInstance],0
cmp eax,0
je error
mov [hwnd],eax
msg_loop:
invoke GetMessage,msg,0,0,0
cmp eax,0
je end_loop
invoke IsDialogMessage,[hwnd],msg
cmp eax,0
jne msg_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop
error:
invoke MessageBox,0,0,0,0
end_loop:
invoke ExitProcess,[msg.wParam]
proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
cmp [wmsg],WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmdestroy:
invoke PostQuitMessage,0
mov eax,0
.finish:
pop edi esi ebx
ret
endp
section '.idata' import data readable writeable
library kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL'
include 'api\kernel32.inc'
include 'api\user32.inc'
section '.rsrc' resource data readable
directory RT_MENU,menus
resource menus,\
1,LANG_RUSSIAN+SUBLANG_DEFAULT,main_menu
menu main_menu
menuitem 'Меню 1',10,MFR_POPUP
menuitem 'Пункт 1-1',11,MFR_POPUP
menuitem 'Пункт 1-1-1',11,MFR_END
menuitem 'Пункт 1-2',12,MFT_STRING,MFS_GRAYED
menuseparator
menuitem 'Пункт 1-3',13,MFR_END,MFS_DEFAULT
menuitem 'Меню 2',20,MFR_POPUP
menuitem 'Пункт 2-1',21,MFR_END,MFS_CHECKED
menuitem 'Меню 3',30,MFR_END
О кно у вас должно получиться большего размера, чем на рисунке. Просто для экономии места я его уменьшил перед фотографированием. Координаты расположения и размеры окна в данном примере установлены в CW_USEDEFAULT. Это значит, что система установит значения по умолчанию для координат и размеров окна. Константа CW_USEDEFAULT может применяться только к окнам верхнего уровня (overlapped windows). Если применить ее к дочернему (child) или всплывающему (popup) окну, система установит значения в ноль. Функция LoadMenu загружает указанное меню из ресурсов исполняемого файла. Параметры: идентификатор исполняемого модуля; имя или идентификатор меню. Если меню успешно загружено, то функция возвращает его дескриптор. При ошибке возвращается ноль. Теперь, чтобы привязать меню к нашему окну, необходимо указать полученный дескриптор меню в третьем с конца параметре CreateWindowEx (идентификатор меню или дочернего окна). Так как функция CreateWindowEx идет сразу же за LoadMenu, нам нет необходимости создавать отдельную переменную для хранения дескриптора меню. Мы просто вписываем eax в качестве параметра функции, потому что на этом этапе в eax должен содержаться дескриптор загруженного меню. Все остальное должно быть вам знакомо из прошлых занятий. До секции ресурсов. А вот про нее, родимую, мы поговорим подробнее. За счет использования макросов ресурсы описываются немного иначе, чем в других компиляторах. В самом начале секции ресурсов должна быть макроинструкция directory, которая определяет типы содержащихся в секции ресурсов. После нее парами следуют значения, разделенные запятыми: первое в каждой паре — идентификатор типа ресурса, а второе — имя поддиректории, содержащей ресурсы указанного типа. У нас пока что один тип ресурсов — меню, поэтому и пара лишь одна. Поддиректории размещаются ниже в этой же секции. Они объявляются макроинструкцией resource. За макроинструкцией следует имя поддиректории (соответствующее имени, указанному в макросе directory), затем тройками идут параметры ресурсов — первый параметр является идентификатором ресурса (выбирается программистом, используется для доступа к ресурсу из программы), второй параметр определяет язык, а третий — имя ресурса. Если ресурс не имеет языковой принадлежности, следует использовать константу LANG_NEUTRAL. Для объявления различных типов ресурсов существуют специальные макроинструкции, которые должны помещаться в описании каждого ресурса. Например, картинки bmp (битовые поля от англ. bitmap), принадлежащие к типу RT_BITMAP, объявляются макросом bitmap. Его первый параметр — имя ресурса (соответствующее имени, указанному в макросе resource), а второй — строка, содержащая путь к файлу картинки, заключенная в кавычки. Меню, относящиеся к типу RT_MENU, объявляются макросом menu, за которым следуют описания пунктов меню. Сам по себе макрос menu имеет лишь один параметр — имя ресурса (соответствующее имени, указанному в макросе resource). А вот макрос menuitem, описывающий пункт меню, может иметь до пяти параметров, первые два из которых обязательные, а остальные три опциональные: первый — строка, содержащая в кавычках текст пункта меню; второй — уникальный идентификатор пункта, который будет передаваться в сообщении окну, если пункт будет выбран пользователем; третий параметр (уже пошли необязательные параметры) — это один из двух возможных флагов MFR — MFR_POPUP (всплывающее меню) и MFR_END (последний пункт меню), каждое всплывающее меню должно завершаться, а также в конце всего меню должен быть завершающий параметр; четвертый параметр — флаг состояния пункта меню — например, MFS_CHECKED или MFS_DISABLED; пятый параметр — флаг типа меню MFT. Список этих и других разрешенных параметров меню вы можете найти в файле FASM\INCLUDE\EQUATES\USER32.INC в секции Menu flags. Их назначение обычно интуитивно понятно, так что, надеюсь, разберетесь самостоятельно. Добавлю только, что макроинструкция menuseparator создает разделитель и может иметь всего один параметр — MFR_END. Раз уж мы коснулись темы ресурсов, то обязательно рассмотрите самостоятельно пример MINIPAD из папки EXAMPLES, чтобы понять, как описываются иконки и версия файла в ресурсах. В описании версии VOS__WINDOWS32 означает 32-битную версию windows, VFT_APP — тип файла "приложение"(application) или VFT_DLL — "динамическая библиотека". Если же вам неохота каждый раз возиться с синтаксисом ресурсов, можете воспользоваться редактором ресурсов и подключать отдельный файл .res способом, указанным в начале статьи. Выбрать подходящий редактор ресурсов вам поможет страничка: сайт . Теперь нам необходимо добавить в код программы обработчик сообщений от меню — без обработки от нашего меню будет мало проку. Добавим в секцию данных пару строчек: mb111 db 'Пункт 1-1-1',0 mb13 db ' Пункт 1-3',0 hmenu dd ? menuinfo MENUITEMINFO sizeof.MENUITEMINFO,MIIM_STATE Сразу после вызова функции LoadMenu сохраним дескриптор меню в переменную: … … … invoke LoadMenu,[wc.hInstance],1 mov [hmenu],eax … … … Когда пользователь выберет пункт меню, окну-владельцу будет послано сообщение WM_COMMAND с идентификатором пункта меню в младшем слове первого параметра (wparam) и нулем в старшем слове. Следовательно, можно считать, что весь wparam содержит идентификатор пункта меню. Хотя это и не самый оптимальный алгоритм, зато вполне наглядный — будем сравнивать wparam с нашими идентификаторами: proc WindowProc hwnd,wmsg,wparam,lparam push ebx esi edi cmp [wmsg],WM_COMMAND je .wmcommand cmp [wmsg],WM_DESTROY je .wmdestroy .defwndproc: invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam] jmp .finish .wmcommand: cmp [wparam],111 je .111 cmp [wparam],13 je .13 cmp [wparam],21 je .21 jmp .finish .111: invoke MessageBox,0,mb111,title,MB_OK jmp .finish .13: invoke MessageBox,0,mb13,title,MB_OK jmp .finish .21: invoke GetMenuItemInfo,[hmenu],21,0,menuinfo xor [menuinfo.fState],MFS_CHECKED invoke SetMenuItemInfo,[hmenu],21,0,menuinfo jmp .finish .wmdestroy: invoke PostQuitMessage,0 mov eax,0 .finish: pop edi esi ebx ret endp Структура MENUITEMINFO необходима нам здесь лишь для переключения состояния пункта меню 2-1. В первый ее элемент мы сразу помещаем размер всей структуры (sizeof.MENUITEMINFO), а во второй — маску запрашиваемых и устанавливаемых элементов структуры. MIIM_STATE означает, что в функциях GetMenuItemInfo и SetMenuItemInfo будет использоваться элемент структуры fState, в котором содержатся флаги состояния пункта меню (MFS_). Функция GetMenuItemInfo возвращает информацию о заданном пункте меню. Параметры: дескриптор меню; идентификатор или позиция пункта меню в зависимости от третьего параметра; если ноль, то во втором параметре содержится идентификатор пункта меню, иначе во втором параметре — его позиция; указатель на структуру MENUITEMINFO, размер структуры и маска должны быть предварительно заполнены. После вызова GetMenuItemInfo элемент fState должен содержать флаги состояния элемента меню. Обычно флаг какого-либо состояния соответствует определенному биту. Например, MFS_CHECKED = MF_CHECKED = 8. Число 8 в двоичном эквиваленте — это 1000b (кто не в курсе, ищите в интернете информацию о системах счисления). То есть четвертый по счету бит установлен в единицу и означает MFS_CHECKED. Команда XOR выполняет логическую операцию исключающего ИЛИ над битами двух операндов. Если соответствующие биты операндов равны, то бит в первом операнде сбрасывается в ноль, если различны — устанавливается в единицу. По сути, биты в первом операнде переключаются в другое положение, если соответствующие биты второго операнда равны единицам, и остаются в прежнем состоянии, когда соответствующие биты второго операнда равны нулю. В нашем случае второй операнд — MFS_CHECKED, он же 1000b в двоичном эквиваленте. Стало быть, флаг будет переключен из нуля в единицу либо из единицы в ноль. Теперь функция SetMenuItemInfo устанавливает состояние пункта меню в соответствии с заданными элементами структуры. Параметры функции аналогичны параметрам GetMenuItemInfo. При ошибке обе функции возвращают в eax ноль. Я не стану слишком подробно рассказывать о структуре MENUITEMINFO и использующих эту структуру функциях, потому что подобной информации весьма много в интернете, и, несмотря на то, что в большинстве случаев описание идет в синтаксисе С++ и Delphi, вы всегда теперь сможете провести аналогию с нашим примером.
Для тех, кто решительно настроен покорять эти горизонты в мире программирования под Windows, я продолжаю свой рассказ про FASM. Сегодня мы закрываем тему ресурсов и приступаем к созданию собственного текстового редактора. Именно поэтому в прошлой статье я просил вас рассмотреть самостоятельно ресурсы в примере MINIPAD из папки EXAMPLES. Если у вас с этим возникли затруднения — попробуйте разобраться со второго захода, теперь уже с моими пояснениями:
format PE GUI 4.0
entry start