Cамоучитель по Assembler

         

Часть II. Более подробное описание программирования в среде Windows

Глава 1. Примеры простейших программ

В данной главе мы серьезно начинаем работать с сообщением WM_PAINT. В главе 1.3 мы уже рассматривали это сообщение, но не применяли его. Причиной было то, что в окне у нас были лишь управляющие элементы, но не было текстовой информации и графики. Теперь мы исправляем положение. Кроме сообщения WM_PAINT, речь в этой главе пойдет о множестве проблем, возникающих при программировании в Windows.

I

Если вы только начинаете программировать под Windows, то в программе на Рис. 2.1.1 найдете много нового. Поэтому приступим к подробному разбору программы.

    В данной программе мы определяем цвет окна и текста через комбинацию трех цветов: красного, зеленого и синего. Цвет определяется одним 32-битным числом. В этом числе первый байт - интенсивность красного, второй байт - интенсивность зеленого, третий байт - интенсивность синего цвета. Последний байт равен нулю. Механизм получения этого числа продемонстрирован в определении константы RGBW. Цвет окна задается посредством определения кисти через функцию CreateSolidBrush.

    Поскольку при перерисовке окна системой посылается сообщение WM_PAINT, именно при получении этого сообщения и следует перерисовывать содержимое этого окна. В данном случае мы выводим всего лишь одну строку текста. Для того чтобы осуществить вывод информации в окно, необходимо сначала получить контекст окна (контекст устройства - Device Context). Для нас это — просто некоторое число, посредством которого осуществляется связь между приложением и окном. Обычно контекст устройства определяется посредством функции GetDC. При получении сообщения WM_PAINT контекст устройства получается посредством функции BeginPaint. Аргументом для этой функции является указатель на специальную структуру, которая у нас называется PAINTSTR и поля которой, впрочем, мы пока не используем. Текст, как Вы уже, надеюсь, поняли из текста программы, выводится посредством функции OutText. Предварительно, посредством функций SetBkColor и SetTextColor, мы определяем цвет фона и цвет букв. Цвет фона, соответственно, совпадает с цветом окна. Несколько слов о системе координат. Центр системы координат находится в левом верхнем углу, ось Y направлена вниз, ось Х - вправо. Впрочем, это общепринятый вариант для графических экранов. Еще один момент также связан с выводом текста в окно. Одним из параметров функции OutText является количество символов выводимой строки. И здесь начинается самое интересное. Определить длину строки (за минусом нулевого элемента) можно по-разному. Например, можно использовать операторы макроассемблера SIZEOF или LENGTHOF. Но вот беда, в Турбо Ассемблере этих операторов нет. Можно, конечно, решить эту проблему, поставив метку в конце строки или используя старые директивы LENGTH и SIZE. Но, как Вы, наверное, уже поняли, для того чтобы легко переходить от MASM32 к TASM32, следует как можно меньше использовать макросредства. Кроме того, раз уже мы употребляем определение строк, как это принято в Си, — естественно и определить функции для работы со строковыми переменными (см. замечание в конце главы). В данном примере мы определили функцию, которая возвращает длину строки. Не смущайтесь, что функция помещает результат в регистр EBX. Нам просто так удобнее. У функции, кроме того, есть одно очень важное преимущество перед макросредствами - она получает длину при выполнении программы, а не во время ее трансляции.

Теперь, чтобы добиться транслируемости программы на Турбо Ассемблере, нужно проделать те же манипуляции, которые мы производили раньше: убрать суффиксы @N и подключить библиотеку import32.lib.

; файл text1.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение приходит при перерисовке окна
WM_PAINT equ 0FH
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0 equ 300
DY0 equ 200
; компоненты цветов
RED equ 50
GREEN equ 50
BLUE equ 255
RGBW equ (RED or (GREEN shl 8)) or (BLUE shl 16)
RGBT equ 255 ; красный
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS equ 32515
; режим показа окна — нормальный
SW_SHOWNORMAL equ 1
; прототипы внешних процедур
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
EXTERN BeginPaint@8:NEAR
EXTERN EndPaint@8:NEAR
EXTERN TextOutA@20:NEAR
EXTERN GetStockObject@4:NEAR
EXTERN CreateSolidBrush@4:NEAR
EXTERN SetBkColor@8:NEAR
EXTERN SetTextColor@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ? ; идентификатор окна,
; получающего сообщение
MSMESSAGE DD ? ; идентификатор сообщения
MSWPARAM DD ? ; доп. информация о сообщении
MSLPARAM DD ? ; доп. информация о сообщении
MSTIME DD ? ; время посылки сообщения
MSPT DD ? ; положение курсора во время
; посылки сообщения
MSGSTRUCT ENDS
WNDCLASS STRUC
CLSSTYLE DD ? ; стиль окна
CLSLPFNWNDPROC DD ? ; указатель на процедуру окна
CLSCBCLSEXTRA DD ? ; информация о доп. байтах для
; данной структуры
CLSCBWNDEXTRA DD ? ; информация о доп. байтах для окна
CLSHINSTANCE DD ? ; дескриптор приложения
CLSHICON DD ? ; идентификатор иконы окна
CLSHCURSOR DD ? ; идентификатор курсора окна
CLSHBRBACKGROUND DD ? ; идентификатор кисти окна
MENNAME DD ? ; имя-идентификатор меню
CLSNAME DD ? ; специфицирует имя класса окон
WNDCLASS ENDS
PAINTSTR STRUC
hdc DWORD 0
fErase DWORD 0
left DWORD 0
top DWORD 0
right DWORD 0
bottom DWORD 0
fRes DWORD 0
fIncUp DWORD 0
Reserv DB 32 dup(0)
PAINTSTR ENDS
; файл text1.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------------------
include text1.inc
; подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\gdi32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
PNT PAINTSTR <?>
HINST DD 0
TITLENAME DB 'Текст в окне',0
NAM DB 'CLASS32',0
XT DWORD 30
YT DWORD 30
TEXT DB 'Текст в окне красный',0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
MOV [WC.CLSSTYLE] , stylcl
; процедура обработки сообщений
MOV [WC.CLSLPFNWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCBCLSEXTRA],0
MOV [WC.CLSCBWNDEXTRA],0
MOV EAX, [HINST]
MOV [WC.CLSHINSTANCE],EAX
; иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
;----------курсор окна
PUSH IDC_CROSS
PUSH 0
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR],EAX
PUSH RGBW ; цвет кисти
CALL CreateSolidBrush@4 ; создать кисть
MOV [WC.CLSHBRBACKGROUND],EAX
MOV DWORD PTR [WC.MENNAME],0
MOV DWORD PTR [WC.CLSNAME], OFFSET NAM
PUSH OFFSET WC
CALL RegisterClassA@4
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH DY0 ; DY0 - высота окна
PUSH DX0 ; DX0 - ширина окна
PUSH 100 ; координата Y
PUSH 100 ; координата X
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET TITLENAME ; имя окна
PUSH OFFSET NAM ; имя класса
PUSH 0
CALL CreateWindowExA@48
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX ; дескриптор окна
;------------------------------------------------------------
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8 ; показать созданное окно
; ————————————————————
PUSH [NEWHWND]
CALL UpdateWindow@4 ; перерисовать видимую часть окна
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP AX, 0
JE END_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP: ; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess@4
_ERR:
JMP END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
CMP DWORD PTR [EBP+0CH], WM_DESTROY
JE WMDESTROY
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
CMP DWORD PTR [EBP+0CH], WM_PAINT
JE WMPAINT
JMP DEFWNDPROC
WMPAINT:
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL BeginPaint@8
PUSH EAX ; сохранить контекст (дескриптор)
;---------------- цвет фона = цвет окна
PUSH RGBW
PUSH EAX
CALL SetBkColor@8
;---------------- контекст
POP EAX
PUSH EAX
;---------------- цвет текста (красный)
PUSH RGBT
PUSH EAX
CALL SetTextColor@8
;---------------- контекст
POP EAX
;---------------- вывести текст
PUSH OFFSET TEXT
CALL LENSTR
PUSH EBX ; длина строки
PUSH OFFSET TEXT ; адрес строки
PUSH YT ; Y
PUSH XT ; X
PUSH EAX ; контекст окна
CALL TextOutA@20
;---------------- закрыть
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL EndPaint@8
MOV EAX, 0
JMP FINISH
WMCREATE:
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA@16
JMP FINISH
WMDESTROY:
PUSH 0
CALL PostQuitMessage@4 ; WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
;----------- функция --------------------------
; длина строки
; [EBP+08H] - указатель на строку
LENSTR PROC
PUSH EBP
MOV EBP, ESP
PUSH ESI
MOV ESI, DWORD PTR [EBP+8]
XOR EBX, EBX
LBL1:
CMP BYTE PTR [ESI], 0
JZ LBL2
INC EBX
INC ESI
JMP LBL1
LBL2:
POP ESI
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Puc. 2.1.1. Пример простейшей программы с текстом.

Еще один пример с выводом текста в окно. Теперь мы усложняем свою задачу. Зададимся целью, чтобы текстовая строка все время, чтобы ни случилось с окном, была бы в его середине. Для этого необходимо знать длину строки в пикселях и размеры окна. Длина строки в пикселях определяется с помощью функции GetTextExtentPoint32, а размеры окна - с помощью функции GetWindowRect. При этом нам понадобятся структуры типа SIZET и RECT. Надеюсь, читатель понимает, как определить положение строки, если известна ее длина и размеры окна, добавлю только, что необходимо учесть высоту заголовка окна.

; файл text2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение приходит при перерисовке окна
WM_PAINT equ 0FH
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0 equ 300
DY0 equ 200
; компоненты цветов
RED equ 80
GREEN equ 80
BLUE equ 255
RGBW equ (RED or (GREEN shl 8)) or (BLUE shl 16)
RGBT equ 00FF00H ; зеленый
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL equ 1
; прототипы внешних процедур
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
EXTERN BeginPaint@8:NEAR
EXTERN EndPaint@8:NEAR
EXTERN TextOutA@20:NEAR
EXTERN GetStockObject@4:NEAR
EXTERN CreateSolidBrush@4:NEAR
EXTERN SetBkColor@8:NEAR
EXTERN SetTextColor@8:NEAR
EXTERN GetTextExtentPoint32A@16:NEAR
EXTERN GetWindowRect@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ? ; идентификатор окна, получающего сообщение
MSMESSAGE DD ? ; идентификатор сообщения
MSWPARAM DD ? ; доп. информация о сообщении
MSLPARAM DD ? ; доп. информация о сообщении
MSTIME DD ? ; время посылки сообщения
MSPT DD ? ; положение курсора, во время
; посылки сообщения
MSGSTRUCT ENDS
WNDCLASS STRUC
CLSSTYLE DD ? ; стиль окна
CLSLPFNWNDPROC DD ? ; указатель на процедуру окна
CLSCBCLSEXTRA DD ? ; информация о доп. байтах для данной структуры
CLSCBWNDEXTRA DD ? ; информация о доп. байтах для окна
CLSHINSTANCE DD ? ; дескриптор приложения
CLSHICON DD ? ; идентификатор иконы окна
CLSHCURSOR DD ? ; идентификатор курсора окна
CLSHBRBACKGROUND DD ? ; идентификатор кисти окна
MENNAME DD ? ; имя-идентификатор меню
CLSNAME DD ? ; специфицирует имя класса окон
WNDCLASS ENDS
PAINTSTR STRUC
hdc DWORD 0
fErase DWORD 0
left DWORD 0
top DWORD 0
right DWORD 0
bottom DWORD 0
fRes DWORD 0
fIncUp DWORD 0
Reserv DB 32 dup (0)
PAINTSTR ENDS
SIZET STRUC
X1 DWORD ?
Y1 DWORD ?
SIZET ENDS
RECT STRUC
L DWORD ? ; X - левого верхнего угла
T DWORD ? ; Y - левого верхнего угла
R DWORD ? ; X - правого нижнего угла
B DWORD ? ; Y - правого нижнего угла
RECT ENDS
; файл text2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------------------
include text2.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\gdi32.lib
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
PNT PAINTSTR <?>
SZT SIZET <?>
RCT RECT <?>
HINST DD 0
TITLENAME DB 'Текст в окне',0
NAM DB 'CLASS32',0
XT DWORD ?
YT DWORD ?
TEXT DB 'Текст в окне зеленый',0
CONT DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
MOV [WC.CLSSTYLE],stylcl
; процедура обработки сообщений
MOV [WC.CLSLPFNWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCBCLSEXTRA], 0
MOV [WC.CLSCBWNDEXTRA], 0
MOV EAX, [HINST]
MOV [WC.CLSHINSTANCE], EAX
; ---------- иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
;---------- курсор окна
PUSH IDC_CROSS
PUSH 0
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR], EAX
PUSH RGBW ; цвет кисти
CALL CreateSolidBrush@4 ; создать кисть
MOV [WC.CLSHBRBACKGROUND], EAX
MOV DWORD PTR [WC.MENNAME],0
MOV DWORD PTR [WC.CLSNAME], OFFSET NAM
PUSH OFFSET WC
CALL RegisterClassA@4
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH DY0 ; DY0 - высота окна
PUSH DX0 ; DX0 - ширина окна
PUSH 100 ; координата Y
PUSH 100 ; координата X
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET TITLENAME ; имя окна
PUSH OFFSET NAM ; имя класса
PUSH 0
CALL CreateWindowExA@48
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX ; дескриптор окна
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8 ; показать созданное окно
PUSH [NEWHWND]
CALL UpdateWindow@4 ; перерисовать видимую часть окна
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP AX, 0
JE END_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess@4
_ERR:
JMP END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0CH] MES
; [EBP+8] HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
CMP DWORD PTR [EBP+0CH],WM_DESTROY
JE WMDESTROY
CMP DWORD PTR [EBP+0CH],WM_CREATE
JE WMCREATE
CMP DWORD PTR [EBP+0CH],WM_PAINT
JE WMPAINT
JMP DEFWNDPROC
WMPAINT:
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL BeginPaint@8
MOV CONT,EAX ; сохранить контекст (дескриптор)
;---------------- цвет фона = цвет окна
PUSH RGBW
PUSH EAX
CALL SetBkColor@8
;---------------- цвет текста (красный)
PUSH RGBT
PUSH CONT
CALL SetTextColor@8
;- вычислить длину текста в пикселях текста
PUSH OFFSET TEXT
CALL LENSTR
PUSH EBX ; сохраним длину строки
PUSH OFFSET SZT
PUSH EBX
PUSH OFFSET TEXT
PUSH CONT
CALL GetTextExtentPoint32A@16
;---------------- размер окна
PUSH OFFSET RCT
PUSH DWORD PTR [EBP+8]
CALL GetWindowRect@8
;---------------- здесь вычисления координат
MOV EAX, RCT.R
SUB EAX, RCT.L
SUB EAX, SZT.X1
SHR EAX, 1 ; текст посередине
MOV XT, EAX
MOV EAX, RCT.B
SUB EAX, RCT.T
SHR EAX, 1
SUB EAX, 25 ; учтем заголовочную часть окна
MOV YT,EAX
; ---------------- вывести текст
; длина строки уже в стеке
PUSH OFFSET TEXT
PUSH YT
PUSH XT
PUSH CONT
CALL TextOutA@20
;---------------- закрыть контекст
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL EndPaint@8
MOV EAX, 0
JMP FINISH
WMCREATE:
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA@16
JMP FINISH
WMDESTROY:
PUSH 0
CALL PostQuitMessage@4 ; WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
; длина строки
; указатель на строку в - [EBP+08H]
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH ESI
MOV ESI, DWORD PTR [EBP+8]
XOR EBX, EBX
LBL1:
CMP BYTE PTR [ESI],0
JZ LBL2
INC EBX
INC ESI
JMP LBL1
LBL2:
POP ESI
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.1.2. Текстовая строка все время в середине окна.

Не могу не воспользоваться случаем и не восхититься теми возможностями, которые открывает перед программистом ассемблер. Можете передавать параметры через стек, а можете и через регистры. Хотите - сохраняйте регистры в начале процедуры, а хотите - не сохраняйте. Ассемблерный код можно совершенствовать и еще раз совершенствовать. Кстати, для любителей цепочечных (строковых) команд ниже привожу другую процедуру определения длины строки, основанную на команде микропроцессора SCAS, позволяющей осуществлять поиск нужного элемента (байта - SCASB, слова - SCASW, двойного слова - SCASD) в строке.

; длина строки - [EBP+08Н]
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX, EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX, EDI
DEC EBX ; теперь здесь длина строки
POP EAX
POP EBP
RET 4
LENSTR ENDP

II

Рассмотрим теперь вопрос о том, как выводить текстовую информацию с различными типами шрифтов. Удобнее всего задать параметры шрифта при помощи функции CreateFontIndirect, параметром которой является указатель на структуру LOGFONT. Хотя название функции и начинается со слова Create, речь идет не о создании, а скорее изменении существующего шрифта согласно заданным параметрам. Существует и другая функция CreateFont, которая, на мой взгляд, менее удобна при использовании на ассемблере — поработайте с ней сами, если хотите. Выбор нужного шрифта осуществляется функцией SelectObject. Начнем с того, что разберем поля этой структуры.

LOGFONT STRUC
LfHeight DWORD ?
LfWidth DWORD ?
LfEscapement DWORD ?
LfOrientation DWORD ?
LfWeight DWORD ?
Lfitalic DB ?
LfUnderline DB ?
LfStrikeOut DB ?
LfCharSet DB ?
LfOutPrecision DB ?
LfClipPrecision DB ?
LfQuality DB ?
LfPitchAndFamily DB ?
LfFaceName DB 32 DUP(0)
LOGFONT ENDS

LfHeight - определяет высоту шрифта в логических единицах; если 0, то высота берется по умолчанию.
LfWidth - определяет ширину шрифта в логических единицах; если 0, то ширина по умолчанию.
LfEscapement - угол наклона текста в десятых долях градуса по отношению к горизонтальной оси в направлении против часовой стрелки.
LfOrientation - тоже, что и предыдущий параметр, но по отношению к отдельному символу (игнорируется в Windows 9х).
LfWeight - задает жирность шрифта (0-900).
Lfitalic - если 1, то курсив.
LfUnderline — если 1, то символы подчеркнуты.
LfStrikeOut — если 1, то символы перечеркнуты.
LfCharSet - задает множество символов шрифта, обычно определяется константой ANSI_CHARSET (=0).
LfOutPrecision - флаг точности шрифта; определяет, насколько точно созданный шрифт отвечает заданным параметрам. Возможные значения:

OUT_DEFAULT_PRECIS = 0
OUT_STRING_PRECIS = 1
OUT_CHARACTER_PRECIS = 2
OUT_STROKE_PRECIS = 3
OUT_TT_PRECIS = 4
OUT_DEVICE__PRECIS = 5
OUT_RASTER_PRECIS = 6
OUT_TT_ONLY_PRECIS = 7
OUT_OUTLINE_PRECIS = 8
OUT_SCREEN_OUTLINE_PRECIS = 9

LfClipPrecision - флаг точности прилегания шрифта; определяет, как будут отсекаться части шрифта, не попадающие в видимую область. Возможные значения:

CLIP_DEFAULT_PRECIS = 0
CLIP_CHARACTER_PRECIS = 1
CLIP_STROKE_PRECIS = 2
CLIP_MASK = 0FH
CLIP_LH_ANGLES = (1 SHL 4)
CLIP_TT_ALWAYS = (2 SHL 4)
CLIP_EMBEDDED = (8 SHL 4)

LfQuality - флаг качества шрифта; определяет соответствие логического шрифта и шрифта, допустимого данным устройством. Возможные значения:

DEFAULT_QUALITY = 0
DRAFT_QUALITY = 1
PROOF_QUALITY = 2

LfPitchAndFamily - определяет тип и семейство шрифта. Возможные значения определяются комбинацией (ИЛИ) двух групп констант:

DEFAULT_PITCH = 0
FIXED_PITCH = 1
VARIABLE_PITCH = 2

и
FF_DONTCARE = 0
FF_ROMAN = (1 SHL 4)
FF_SWISS = (2 SHL 4)
FF_MODERN = (3 SHL 4)
FF_SCRIPT = (4 SHL 4)
FF_DECORATIVE = (5 SHL 4)

LfFaceName - содержит название шрифта. Длина имени не может превосходить 32 символа.

Обратимся к примеру задания своего шрифта (результат работы программы - на Рис. 2.1.5). Однако поскольку большая часть программы будет совпадать с аналогичной частью предыдущих программ, я приведу здесь только необходимые фрагменты. Рассмотрим сначала фрагмент, выполняющийся при получении сообщения WM_PAINT (Рис.2.1.3).

WMPAINT:
;------- определить контекст
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL BeginPaint@8
MOV CONT,EAX ; сохранить контекст (дескриптор)
;------- цвет фона = цвет окна
PUSH RGBW
PUSH EAX
CALL SetBkColor@8
;------- цвет текста (красный)
PUSH RGBT
PUSH CONT
CALL SetTextColor@8
;------ здесь определение координат
MOV XT, 120
MOV YT, 140
;------ задать (создать) шрифт
MOV lg.LfHeight,12 ; высота фонта
MOV lg.LfWidth, 9 ; ширина фонта
MOV lg.LfEscapement,900 ; ориентация
MOV lg.LfOrientation, 0 ; вертикальная
MOV lg.LfWeight,400 ; толщина линий шрифта
MOV lg.LfItalic, 0 ; курсив
MOV lg.LfUnderline, 0 ; подчеркивание
MOV lg.LfStrikeOut, 0 ; перечеркивание
MOV lg.LfCharSet, 0 ; набор шрифтов
MOV lg.LfOutPrecision, 0
MOV lg.LfClipPrecision, 0
MOV lg.LfQuality,2
MOV lg.LfPitchAndFamily,0
PUSH OFFSET lg
; задать название шрифта
PUSH OFFSET NFONT
PUSH OFFSET lg.LfFaceName
CALL COPYSTR
CALL CreateFontIndirectA@4
;------ выбрать созданный объект
PUSH EAX
PUSH CONT
CALL SelectObject@8
PUSH EAX
;------ вычислить длину текста в пикселях текста
PUSH OFFSET TEXT
CALL LENSTR
;---------- вывести текст ----------------
PUSH EBX
PUSH OFFSET TEXT
PUSH YT
PUSH XT
PUSH CONT
CALL TextOutA@20
; удалить объект "FONT"
; идентификатор уже в стеке
CALL DeleteObject@4
;---------------- закрыть контекст
PUSH OFFSET PNT
PUSH DWORD PTR [EBP+08H]
CALL EndPaint@8
MOV EAX, 0
JMP FINISH

Рис. 2.1.3. Фрагмент программы, выводящей текст с заданным шрифтом (см. Рис. 2.1.5).

Как видно из фрагмента, создание шрифта производится по следующей схеме: надо задать шрифт при помощи функции CreateFontIndirect, выбрать шрифт функцией SelectObject, вывести текст заданным шрифтом, удалить созданный шрифт (объект). Поле LfFaceName структуры LOGFONT должно содержать название шрифта. Если такого шрифта нет, выводится шрифт по умолчанию. Название шрифта у нас задано в строке NFONT, и мы копируем его в поле LfFaceName при помощи функции COPYSTR, текст которой приводится на Рис. 2.1.4.

; процедура копирования одной строки в другую
; строка, куда копировать [EBP+08H]
; строка, что копировать [EBP+0CH]
COPYSTR PROC
PUSH EBP
MOV EBP,ESP
MOV ESI, DWORD PTR [EBP+0CH]
MOV EDI, DWORD PTR [EBP+08H]
L1:
MOV AL,BYTE PTR [ESI]
MOV BYTE PTR [EDI],AL
CMP AL,0
JE L2
INC ESI
INC EDI
JMP L1
L2:
POP EBP
RET 8
COPYSTR ENDP

Рис. 2.1.4. Процедура копирования одной строки в другую.

Данная процедура не учитывает длину строки, в которую производится копирование. Лучше всего это сделать, включив еще один параметр - максимальное количество символов, которое может быть скопировано. Попробуйте это сделать.

Рис. 2.1.5. Вывод текста под углом 90 градусов.

В заключении раздела мы рассмотрим один очень важный вопрос. При разборе предыдущих примеров этот вопрос, скорее всего, у Вас не возникал, и вот почему. Весь вывод информации происходил в программе по получению сообщения WM_PAINT. В реальных программах вывод информации в окно может происходить по различным событиям и из различных процедур. Кроме того, если информации в окне много, то непосредственный вывод при помощи функции TextOut достаточно медленный. Чтобы воспроизводить содержимое окна, необходимо где-то запомнить это содержимое. Возникает проблема сохранения информации (и не только текстовой), находящейся в окне.

Если кто-то программировал для операционной системы MS DOS, то там подобная проблема также возникает. Решается она следующим образом: используется фоновая видеостраница, на которую выводится вся информация. Затем фоновая страница копируется на видимую страницу. При этом создается впечатление, что информация появляется на экране мгновенно. В качестве фоновой страницы используется как область ОЗУ, так и область видеопамяти.

Аналогично в операционной системе Windows образуется виртуальное окно, и весь вывод информации производится туда. Затем по приходе сообщения WM_PAINT содержимое виртуального окна копируется на реальное окно. В целом общая схема такова:

1. При создании окна:

    Создается совместимый контекст устройства.
    Функция CreateCompatibleDC. Полученный контекст следует запомнить.
    Создается карта бит, совместимая с данным контекстом.
    Функция CreateCompatibleBitmap.
    Выбирается кисть цветом, совпадающим с цветом основного окна.
    Создается битовый шаблон путем выполнения растровой операции с использованием выбранной кисти. Функция PatBlt.

2. Вся информация выводится в виртуальное окно и дается команда перерисовки окна. Функция InvalidateRect.

3. При получении сообщения WM_PAINT содержимое виртуального окна копируется на реальное окно. Функция BitBlt. Изложенная теория будет применена на практике в следующем разделе.

III

Этот раздел главы посвящается графике. Впрочем, основы графики в Windows в принципе достаточно тривиальны, поэтому мы рассмотрим один простой пример - на вывод графических образов. Но в начале я изложу некоторые опорные моменты:

1. Система координат для вывода графических образов такая же, как и для ввода текстовой информации. Координаты измеряются в логических единицах, которые по умолчанию совпадают с пикселями. При желании эту пропорцию можно изменить.

2. Цвет рисования образуется тремя способами. При использовании функции SetPixel задается цвет данной точки. Для линий необходимо задать цвет пера. Для задания цвета замкнутых графических объектов следует задать цвет кисти.

3. Перо создается при помощи функции CreatePen, кисть - при помощи CreateSolidBrush (мы ее уже использовали). Для создания разноцветной картинки можно заранее создать несколько кистей и перьев, а затем в нужный момент выбирать при помощи функции SelectObject (мы также уже использовали эту функцию).

4. Для рисования можно использовать следующие функции API:

    SetPixel - установить заданный цвет пикселя. LineTo - провести линию от текущей точки до точки с заданными координатами, которая в свою очередь становится текущей. MoveToEx - сменить текущую точку. Arc - рисование дуги. Rectangle - нарисовать прямоугольник. RoundRect - нарисовать прямоугольник с округленными углами. Ellipse, Pie - рисование эллипсов и секторов эллипсов.

5. Если при рисовании замкнутой фигуры был установлен цвет кисти, отличный от цвета основного фона, то замкнутая фигура окрашивается этим цветом.

6. Для установки соотношения между логическими единицами и пикселями используется функция SetMapMode.

7. Можно установить область вывода при помощи функции SetViewportExtEx. С помощью функции SetViewportOrgEx можно задать начало области ввода.

После всего сказанного пора продемонстрировать программу. Программа достаточно проста, но в ней заложены основы работы с графикой. По щелчку левой кнопки мыши сначала появляется горизонтальная линия, по второму щелчку - наклонная линия, по третьему щелчку - заполненный прямоугольник. Программа показана на Рис. 2.1.6, результат ее работы - на Рис. 2.1.7.

; файл graph1.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN equ 201h
; сообщение приходит при перерисовке окна
WM_PAINT equ 0FH
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0 equ 600
DY0 equ 400
; компоненты цветов
RGBW equ (50 or (50 shl 8)) or (255 shl 16) ; цвет окна
RGBR equ 150 ; цвет региона
RGBL equ 0 ; цвет линии
RGBP equ 255 or (100 shl 8) ; цвет точки
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL equ 1
; прототипы внешних процедур
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
EXTERN BeginPaint@8:NEAR
EXTERN EndPaint@8:NEAR
EXTERN GetStockObject@4:NEAR
EXTERN CreateSolidBrush@4:NEAR
EXTERN GetSystemMetrics@4:NEAR
EXTERN GetDC@4:NEAR
EXTERN CreateCompatibleDC@4:NEAR
EXTERN SelectObject@8:NEAR
EXTERN CreateCompatibleBitmap@12:NEAR
EXTERN PatBlt@24:NEAR
EXTERN BitBlt@36:NEAR
EXTERN ReleaseDC@8:NEAR
EXTERN DeleteObject@4:NEAR
EXTERN InvalidateRect@12:NEAR
EXTERN GetStockObject@4:NEAR
EXTERN DeleteDC@4:NEAR
EXTERN CreatePen@12:NEAR
EXTERN SetPixel@16:NEAR
EXTERN LineTo@12:NEAR
EXTERN MoveToEx@16:NEAR
EXTERN Rectangle@20:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ? ; идентификатор окна, получающего сообщение
MSMESSAGE DD ? ; идентификатор сообщения
MSWPARAM DD ? ; доп. информация о сообщении
MSLPARAM DD ? ; доп. информация о сообщении
MSTIME DD ? ; время посылки сообщения
MSPT DD ? ; положение курсора во время
; посылки сообщения
MSGSTRUCT ENDS
WNDCLASS STRUC
CLSSTYLE DD ? ; стиль окна
CLSLPFNWNDPROC DD ? ; указатель на процедуру окна
CLSCBCLSEXTRA DD ? ; информация о доп. байтах для
; данной структуры

CLSCBWNDEXTRA DD ? ; информация о доп. байтах для окна CLSHINSTANCE DD ? ; дескриптор приложения CLSHICON DD ? ; идентификатор иконы окна CLSHCURSOR DD ? ; идентификатор курсора окна CLSHBRBACKGROUND DD ? ; идентификатор кисти окна MENNAME DD ? ; имя-идентификатор меню CLSNAME DD ? ; специфицирует имя класса окон WNDCLASS ENDS PAINTSTR STRUC hdc DD 0 fErase DD 0 left DD 0 top DD 0 right DD 0 bottom DD 0 fRes DD 0 fIncUp DD 0 Reserv DB 32 dup(0) PAINTSTR ENDS RECT STRUC L DD ? ; Х-левого верхнего угла T DD ? ; Y-левого верхнего угла R DD ? ; Х- правого нижнего угла B DD ? ; Y- правого нижнего угла RECT ENDS ; файл graph.asm .386P ; плоская модель .MODEL FLAT, stdcall ;------------------------------------------------------------------ include graph1.inc ; подключения библиотек includelib c:\masm32\iib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ;------------------------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' NEWHWND DWORD 0 MSG MSGSTRUCT <?> WC WNDCLASS <?> PNT PAINTSTR <?> HINST DWORD 0 TITLENAME BYTE 'Графика в окне',0 NAM BYTE 'CLASS32',0 XT DWORD 30 YT DWORD 30 XM DWORD ? YM DWORD ? HDC DWORD ? MEMDC DWORD ? HPEN DWORD ? HBRUSH DWORD ? P DWORD 0 ; признак вывода XP DWORD ? YP DWORD ? _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX REG_CLASS: ; заполнить структуру окна ; стиль MOV [WC.CLSSTYLE],stylcl ; процедура обработки сообщений MOV [WC.CLSLPFNWNDPROC], OFFSET WNDPROC MOV [WC.CLSCBCLSEXTRA],0 MOV [WC.CLSCBWNDEXTRA], 0 MOV EAX, [HINST] MOV [WC.CLSHINSTANCE], EAX ; ----------иконка окна PUSH IDI_APPLICATION PUSH 0 CALL LoadIconA@8 MOV [WC.CLSHICON], EAX ; ----------курсор окна PUSH IDC_CROSS PUSH 0 CALL LoadCursorA@8 MOV [WC.CLSHCURSOR], EAX ;----------- PUSH RGBW ; цвет кисти CALL CreateSolidBrush@4 ; создать кисть MOV [WC.CLSHBRBACKGROUND], EAX MOV DWORD PTR [WC.MENNAME],0 MOV DWORD PTR [WC.CLSNAME], OFFSET NAM PUSH OFFSET WC CALL RegisterClassA@4 ; создать окно зарегистрированного класса PUSH 0 PUSH [HINST] PUSH 0 PUSH 0 PUSH DY0 ; DYO - высота окна PUSH DX0 ; DXO - ширина окна PUSH 100 ; координата Y PUSH 100 ; координата X PUSH WS_OVERLAPPEDWINDOW PUSH OFFSET TITLENAME ; имя окна PUSH OFFSET NAM ; имя класса PUSH 0 CALL CreateWindowExA@48 ; проверка на ошибку CMP EAX, 0 JZ _ERR MOV [NEWHWND], EAX ; дескриптор окна ;------------------------------------------------------------ PUSH SW_SHOWNORMAL PUSH [NEWHWND] CALL ShowWindow@8 ; показать созданное окно ;------------------------------------------------------------ PUSH [NEWHWND] CALL UpdateWindow@4 ; перерисовать видимую часть окна ; петля обработки сообщений MSG_LOOP: PUSH 0 PUSH 0 PUSH 0 PUSH OFFSET MSG CALL GetMessageA@16 CMP AX, 0 JE END_LOOP PUSH OFFSET MSG CALL TranslateMessage@4 PUSH OFFSET MSG CALL DispatchMessageA@4 JMP MSG_LOOP END_LOOP: ; выход из программы (закрыть процесс) PUSH [MSG.MSWPARAM] CALL ExitProcess@4 _ERR: JMP END_LOOP ;------------------------------------------------------------ ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10H] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI CMP DWORD PTR [EBP+0CH],WM_DESTROY JE WMDESTROY CMP DWORD PTR [EBP+0CH],WM_CREATE JE WMCREATE CMP DWORD PTR [EBP+0CH],WM_PAINT JE WMPAINT CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN JE LBUTTON JMP DEFWNDPROC LBUTTON: CMP P,0 JNE F1 ; линия точками (горизонтальная) MOV YP,50 ; Y MOV XP,10 ; X MOV ECX,200 LL: PUSH ECX PUSH RGBP PUSH YP PUSH XP PUSH MEMDC CALL SetPixel@16 INC XP POP ECX LOOP LL INC P JMP F3 F1: CMP P,1 JNE F2 ; вначале установим текущие координаты на конец ; предыдущей линии PUSH 0 PUSH YP PUSH XP PUSH MEMDC CALL MoveToEx@16 ; линия пером PUSH 300 PUSH 550 PUSH MEMDC CALL LineTo@12 INC P JMP F3 F2: CMP P,2 JNE FIN ; замкнутая фигура - прямоугольник ; вначале выбрать кисть для заполнения области PUSH HBRUSH PUSH MEMDC CALL SelectObject@8 ; теперь рисуем заполненный прямоугольник если не выбирать ; кисть, то будет нарисован незаполненный прямоугольник PUSH 350 PUSH 400 PUSH 200 PUSH 200 PUSH MEMDC CALL Rectangle@20 INC P F3: ; дать команду перерисовать окно PUSH 0 PUSH OFFSET RECT PUSH DWORD PTR [EBP+08H] CALL InvalidateRect@12 FIN: MOV EAX, 0 JMP FINISH WMPAINT: PUSH OFFSET PNT PUSH DWORD PTR [EBP+08H] CALL BeginPaint@8 MOV HDC,EAX ; сохранить контекст (дескриптор) ; скопировать виртуальное окно на реальное PUSH 0CC0020h ; SRCCOPY=изображение как есть PUSH 0 ; у-источника PUSH 0 ; х-источника PUSH MEMDC ; контекст источника PUSH YM ; высота - куда PUSH XM ; ширина - куда PUSH 0 ; у - куда PUSH 0 ; х-куда PUSH HDC ; контекст - куда CALL BitBlt@36 ;---------------- закрыть контекст окна PUSH OFFSET PNT PUSH DWORD PTR [EBP+08H] CALL EndPaint@8 MOV EAX, 0 JMP FINISH WMCREATE: ; размеры экрана PUSH 0 ; X CALL GetSystemMetrics@4 MOV XM, EAX PUSH 1 ; Y CALL GetSystemMetrics@4 MOV YM, EAX ; открыть контекст окна PUSH DWORD PTR [EBP+08H] CALL GetDC@4 MOV HDC,EAX ; создать совместимый с данным окном контекст PUSH EAX CALL CreateCompatibleDC@4 MOV MEMDC, EAX ; создать в памяти растровое изображение, совместимое с hdc PUSH YM PUSH XM PUSH HDC CALL CreateCompatibleBitmap@12 ; выбрать растровое изображение в данном контексте PUSH EAX PUSH MEMDC CALL SelectObject@8 ; цвет кисти PUSH RGBW CALL CreateSolidBrush@4 ; создать кисть ; выбрать кисть в данном контексте PUSH EAX PUSH MEMDC CALL SelectObject@8 ; заполнить данную прямоугольную область PUSH 0F00021h ; РАТСОРУ=заполнить данным цветом PUSH YM PUSH XM PUSH 0 PUSH 0 PUSH MEMDC CALL PatBlt@24 ; создать кисть и перо для рисования ; цвет кисти PUSH RGBR CALL CreateSolidBrush@4 ; создать кисть MOV HBRUSH,EAX ; задать перо PUSH RGBR ; цвет PUSH 0 ; толщина=1 PUSH 0 ; сплошная линия CALL CreatePen@12 MOV HPEN, EAX ; удалить контекст PUSH HDC PUSH DWORD PTR [EBP+08H] CALL ReleaseDC@8 MOV EAX, 0 JMP FINISH DEFWNDPROC: PUSH DWORD PTR [EBP+14H] PUSH DWORD PTR [EBP+10H] PUSH DWORD PTR [EBP+0CH] PUSH DWORD PTR [EBP+08H] CALL DefWindowProcA@16 JMP FINISH WMDESTROY: ; удалить перо PUSH HPEN CALL DeleteDC@4 ; удалить кисть PUSH HBRUSH CALL DeleteDC@4 ; удалить виртуальное окно PUSH MEMDC CALL DeleteDC@4 ; выход PUSH 0 CALL PostQuitMessage@4 ; WM_QUIT MOV EAX, 0 FINISH: POP EDI POP ESI POP EBX POP EBP RET 16 WNDPROC ENDP _TEXT ENDS END START

Puc. 2.1.6. Простая программа для демонстрации графики.

Рассказ о графическом выводе будет неполным, если не коснуться вопроса манипулирования растровыми изображениями. Рассмотрим последовательность действий, которые необходимо выполнить для вывода растрового изображения, примеры вывода будут даны в последующих главах.

    Загрузить растровое изображение и запомнить его дескриптор. Получить контекст устройства для области памяти, где будет храниться изображение. Выбрать изображение в данном контексте. Скопировать изображение на экран (BitBlt).

С растровыми изображениями удобно работать при помощи ресурсов, но их можно создавать непосредственно в программе или считывать из файла. Но об этом позднее.

Рис. 2.1.7. Результат работы программы на Рис. 2.1.6.

Замечание. Специалисту в Windows-программировании, возможно, показалось странным, что мы пишем свои собственные строковые функции, вместо того чтобы воспользоваться существующими в Windows соответствующими API-функциями. Да-да, такие функции существуют, дорогой читатель. Причина моего пренебрежения этими функциями проста. Во-первых, я рассчитываю не только на "продвинутых" читателей, но и на людей, обучающихся программированию на ассемблере. Во-вторых, как я уже говорил в предисловии, данная книга - это попытка создания некоторого симбиоза ассемблера и программирования в Windows. Следуя этому принципу, мы не всегда будем решать задачи только средствами API-функций. Однако, понимая всю важность строковых API-функций, в свое время я приведу примеры их использования. Кроме того, в приложении будет дано их полное описание.





[an error occurred while processing this directive]

Консольные программы - что это? О, это для тех, кто любит работать с командной строкой. Самая знаменитая консольная программа - это Far. А на первый взгляд кажется, что работаешь с DOS-программой, не правда ли? Но дело ведь не только в любви к текстовому режиму. Часто нет необходимости и времени для создания графического интерфейса, а программа должна что-то делать, например, обрабатывать большие объемы информации. И вот тут на помощь приходят консольные приложения. Ниже Вы увидите, что консольные приложения очень компактны не только в откомпилированном виде, но и в текстовом варианте. Но главное, консольное приложение имеет такие же возможности обращаться к ресурсам Windows посредством API-функций, как и обычное графическое приложение.

Надо сказать, что в книге автора "Assembler. Учебный курс" [1] излагается несколько экзотический способ трансляции консольных приложений, но связано это было с отсутствием у меня в то время нового инструментария. В данной книге мы используем MASM32 6.14 и TASM32 5.0. Здесь все достаточно просто.

Для MASM:

ml /с /coff cons1.asm
link /subsystem:console cons1.obj

Для TASM32:

TASM32 /ml cons1.asm
tlink32 /ap cons1.obj

Как и раньше, мы предполагаем, что библиотеки будут указываться при помощи директивы includelib. Ниже на Рис. 2.2.1 и Рис. 2.2.2 представлено простое консольное приложение для MASM и TASM соответственно. Для вывода текстовой информации используется функция API WriteConsoleA, параметры которой (слева направо) имеют следующий смысл.

1-й параметр - дескриптор буфера вывода консоли, который может быть получен при помощи функции GetStdHandle. 2-й параметр - указатель на буфер, где находится выводимый текст. 3-й параметр - количество выводимых символов. 4-й параметр - указывает на переменную DWORD, куда будет помещено количество действительно выведенных символов. 5-й параметр - резервный параметр, должен быть равен нулю.

Заметим, что буфер, где находится выводимый текст, не обязательно должен заканчиваться нулем, поскольку для данной функции указывается количество выводимых символов. Договоримся только не путать входные-выходные буфера консоли и буфера, которые мы создаем в программе, в том числе и для обмена с буферами консоли.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
STR1 DB "Консольное приложение",0
LENS DD ? ; количество выведенных символов
RES DD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH OFFSET RES ; резерв
PUSH OFFSET LENS ; выведено символов
PUSH EBX ; длина строки
PUSH OFFSET STR1 ; адрес строки
PUSH EAX ; HANDLE вывода
CALL WriteConsoleA@20
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
;------------------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;------------------------------
POP EAX
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.1. Простое консольное приложение для MASM32.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN GetStdHandle:NEAR
EXTERN WriteConsoleA:NEAR
EXTERN ExitProcess:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
STR1 DB "Консольное приложение",0
LENS DD ?
; количество выведенных символов
RES DD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH OFFSET RES ; резерв
PUSH OFFSET LENS ; выведено символов
PUSH EBX ; длина строки
PUSH OFFSET STR1 ; адрес строки
PUSH EAX ; HANDLE вывода
CALL WriteConsoleA
PUSH 0
CALL ExitProcess
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
; ----------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
; ----------
POP EAX
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.2. Простое консольное приложение для TASM32.

Надо сказать, что, поскольку информация выводится в консольном окне, кодировка всех строковых констант должна быть DOS-овской. В дальнейшем разъяснится вопрос, как осуществить перекодировку программным путем.

Прокомментируем теперь приведенные выше программы. При запуске их из командной строки, например из Far'a, в строку выводится сообщение "Консольное приложение". При запуске программы как Windows-приложения консольное окно появляется лишь на секунду. В чем тут дело? Дело в том, что консольные приложения могут создать свою консоль. В этом случае весь ввод-вывод будет производиться в эту консоль. Если же приложение консоль не создает, то здесь может возникнуть двоякая ситуация: либо наследуется консоль, в которой программа была запущена, либо Windows создает для приложения свою консоль.

II

Рассмотрим несколько простых консольных функций и их применение. Во-первых, работать с чужой консолью не всегда удобно. А для того чтобы создать свою консоль, используется функция AllocConsole. По завершении программы все выделенные консоли автоматически освобождаются. Однако это можно сделать и принудительно, используя функцию FreeConsole. Для того чтобы получить дескриптор консоли, используется уже знакомая Вам функция GetStdHandle, аргументом которой может являться следующая из трех констант:

STD_INPUT_HANDLE equ -10 ; для ввода
STD_OUTPUT_HANDLE equ -11 ; для вывода
STD_ERROR_HANDLE equ -12 ; для сообщения об ошибке

Следует отметить, что один процесс может иметь только одну консоль, поэтому выполнение в начале программы FreeConsole обязательно. При запуске программы в "чужой" консоли она наследует эту консоль, поэтому, пока мы не выполним функцию FreeConsole, новой консоли не создать - чужой консоли эта функция закрыть не может.

Для чтения из буфера консоли используется функция ReadConsole. Значения параметров этой функции (слева-направо)28 следующие:
1-й, дескриптор входного буфера.
2-й, адрес буфера, куда будет помещена вводимая информация.
3-й, длина этого буфера.
4-й, количество фактически прочитанных символов.
5-й, зарезервировано.

Установить позицию курсора в консоли можно при помощи функции SetConsoleCursorPosition со следующими параметрами:
1-й, дескриптор входного буфера консоли. 2-й, структура COORD:

COORD STRUC
Х WORD ?
Y WORD ?
COORD ENDS

Хочу лишний раз подчеркнуть, что вторым параметром является не указатель на структуру (что обычно бывает), а именно структура. На самом деле для ассемблера это просто двойное слово (DWORD), у которого младшее слово - координата X, а старшее слово — координата Y.

Установить цвет выводимых букв можно с помощью функции SetConsoleTextAttribute. Первым параметром этой функции является дескриптор выходного буфера консоли, а вторым - цвет букв и фона. Цвет получается путем комбинации (сумма или операция "ИЛИ") двух или более из представленных ниже констант. Причем возможна "смесь" не только цвета и интенсивности, но и цветов (см. программа ниже).

FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность

Для определения заголовка окна консоли используется функция SetConsoleTitle, единственным параметром которой является адрес строки с нулем на конце. Здесь следует оговорить следующее: если для вывода в само окно консоли требовалась DOS-кодировка, то для установки заголовка требуется Windows-кодировка. Чтобы покончить с этой проблемой раз и навсегда, посмотрим, как это можно решить средствами Windows.

Существует специальная функция CharToOem. Первым параметром этой функции является указатель на строку, которую следует перекодировать, а вторым параметром - на строку, куда следует поместить результат. Причем поместить результат можно и в строку, которую перекодируем. Вот и все, проблема перекодировки решена. В дальнейшем, в консольных приложениях, мы будем использовать данную функцию без особых оговорок.

Мы рассмотрели несколько консольных функций, всего их около пятидесяти. Нет нужды говорить обо всех этих функциях. О некоторых из них я еще скажу, но читатель, я думаю, по приведенным в книге примерам и обсуждениям сможет сам использовать в своих программах другие консольные функции. Замечу только, что для большинства консольных функций характерно то, что при правильном их завершении возвращается ненулевое значение. В случае ошибки в EAX помещается ноль.

Ну что же, пора приступать к разбору следующих примеров.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; атрибуты цветов
FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность
COL1 = 2h+8h ; цвет выводимого текста
COL2 = 1h+2h+8h ; цвет выводимого текста 2
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN SetConsoleScreenBufferSize@8:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
COOR STRUC
X WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
STR1 DB "Введите строку: ",13,10,0
STR2 DB "Простой пример работы консоли",0
BUF DB 200 dup (?)
LENS DWORD ? ; количество выведенных символов
CRD COOR <?>
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; перекодируем строку
PUSH OFFSET STR1
PUSH OFFSET STR1
CALL CharToOemA@8
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1, EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL, EAX
; установить новый размер окна консоли
MOV CRD.X, 100
MOV CRD.Y, 25
PUSH CRD
PUSH EAX
CALL SetConsoleScreenBufferSize@8
; задать заголовок окна консоли
PUSH OFFSET STR2
CALL SetConsoleTitleA@4
; установить позицию курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; задать цветовые атрибуты выводимого текста
PUSH COL1
PUSH HANDL
CALL SetConsoleTextAttribute@8
; вывести строку
PUSH OFFSET STR1
CALL LENSTR ; в EBX длина строки
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR1
PUSH HANDL
CALL WriteConsoleA@20
; ждать ввод строки
PUSH 0
PUSH OFFSET LENS
PUSH 200
PUSH OFFSET BUF
PUSH HANDL1
CALL ReadConsoleA@20
; вывести полученную строку
; вначале задать цветовые атрибуты выводимого текста
PUSH COL2
PUSH HANDL
CALL SetConsoleTextAttribute@8
;------------------------------------------------------------
PUSH 0
PUSH OFFSET LENS
PUSH [LENS] ; длина вводимой строки
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
; небольшая задержка
MOV ECX,01FFFFFFFH
L1:
LOOP L1
; закрыть консоль
CALL FreeConsole@0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
;--------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX, EDI
MOV ECX, 100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX, EDI
DEC EBX
;--------------
POP EAX
LEAVE
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.3. Пример создания собственной консоли.

В программе на Рис. 2.2.3, кроме уже описанных функций, появились еще две SetConsoleCursorPosition - установить позицию курсора, и здесь все довольно ясно. Функция SetConsoleScreenBufferSize менее понятна. Она устанавливает размер буфера окна консоли. Этот размер не может уменьшить уже существующий буфер (существующее окно), а может только его увеличить.

Заметим, кстати, что в функции LENSTR мы теперь используем пару команд ENTER-LEAVE (см. Гл. 1.2) вместо обычных сочетаний. Честно говоря, никаких особых преимуществ такое использование не дает. Просто пора расширять свой командный запас.


28 Вообще, как Вы понимаете, для ассемблера практически все параметры имеют тип DWORD. По смыслу же они - или адреса, или значения. Поэтому проще перечислять их, с указанием смысла, чем записывать функцию в Си-нотации.


III

Данный раздел будет поевящен обработке команд мыши и клавиатуры в консольном приложении. Возможность такой обработки делает консольные приложения весьма гибкими, расширяя круг задач, которые можно решить в этом режиме.

Прежде, однако, мы рассмотрим одну весьма необычную, но чрезвычайно полезную API-функцию. Эта функция wsprintfA. Я подчеркиваю, что это именно API-функция, которая предоставляется системой приложению. Эта функция является неким аналогом библиотечной Си-функции - sprintf. Первым параметром функции является указатель на буфер, куда помещается результат форматирования. Второй - указатель на форматную строку, например: "Числа: %lu, %lu". Далее идут указатели на параметры (либо сами параметры, если это числа, см. ниже), число которых определено только содержимым форматной строки. А теперь - самое главное. Поскольку количество параметров не определено, то стек придется освобождать нам. Пример использования этой функции будет дан ниже. Заметим также, что прототипом этой функции для библиотеки import32.lib (TASM32) будет не wsprintfA, a _wsprintfA (!). Наконец отметим, что если функция выполнена успешно, то в EAX будет возвращена длина скопированной строки.

В основе получения информации о клавиатуре и мыши в консольном режиме является функция ReadConsoleInput. Параметры этой функции:
1-й, дескриптор входного буфера консоли. 2-й, указатель на структуру (или массив структур), в которой содержится информация о событиях, происшедших с консолью. Ниже мы подробно рассмотрим эту структуру. 3-й, количество получаемых информационных записей (структур). 4-й, указатель на двойное слово, содержащее количество реально полученных записей.

А теперь подробно разберемся со структурой, в которой содержится информация о консольном событии. Прежде всего замечу, что в Си эта структура записывается с помощью типа данных union (о типах данных см. Гл. 6 данной части). На мой взгляд, частое использование этого слова притупляет понимание того, что же за этим стоит. И при описании этой структуры мы обойдемся без STRUCT и UNION. Замечу также, что в начале этого блока данных идет двойное слово, младшее слово которого определяет тип события. В зависимости от значения этого слова последующие байты (максимум 18) будут трактоваться так или иначе. Те, кто уже знаком с различными структурами, используемыми в Си и Макроассемблере, теперь должны понять, почему UNION здесь весьма подходит.

Но вернемся к типу события. Всего системой зарезервировано пять типов событий:

KEY_EVENT equ 1h ; клавиатурное событие
MOUSE_EVENT equ 2h ; событие с мышью
WINDOW_BUFFER_SIZE_EVENT equ 4h ; изменился размер окна
MENU_EVENT equ 8h ; зарезервировано
FOCUS_EVENT equ 10h; зарезервировано

А теперь разберем значение других байт структуры в зависимости от происшедшего события.

Событие KEY_EVENT

СмещениеДлинаЗначение
+44При нажатии клавиши значение поля больше нуля.
+82Количество повторов при удержании клавиши.
+102Виртуальный код клавиши.
+122Скан-код клавиши.
+142Для функции ReadConsoleInputA-младший байт равен ASCII-коду клавиши. Для функции ReadConsoleInputW слово содержит код клавиши в двухбайтной кодировке (Unicode).
+164Содержится состояния управляющих клавиш. Может являться суммой следующих констант:
RIGHT_ALT_PRESSED equ 1h
LEFT_ALT_PRESSED equ 2h
RIGHT_CTRL_PRESSED equ 4h
LEFT_CTRL_PRESSED equ 8h
SHIFT_PRESSED equ 10h
NUMLOCK_ON equ 20h
SCROLLLOCK_ON equ 40h
CAPSLOCK_ON equ 80h
ENHANCED_KEY equ 100h
Смысл констант очевиден.

Событие MOUSE_EVENT

СмещениеДлинаЗначение
+44Младшее слово - Х-координата курсора мыши,
старшее слово - Y-координата мыши.
+8 4Описывает состояние кнопок мыши. Первый бит - левая кнопка, второй бит - правая кнопка, третий бит - средняя кнопка. Бит установлен - кнопка нажата.
+12 4Состояние управляющих клавиш. Аналогично предыдущей таблице.
+164Может содержать следующие значения:
MOUSE_MOV equ 1h; было движение мыши
DOUBLE_CL equ 2h; был двойной щелчок

Событие WINDOW_BUFFER_SIZE_EVENT

По смещению +4 находится двойное слово, содержащее новый размер консольного окна. Младшее слово - это размер по X, старшее слово - размер по Y. Да, когда речь идет о консольном окне, все размеры и координаты даются в "символьных" единицах.

Что касается последних двух событий, то там также значимым является двойное слово по смещению +4, Ниже на Рис. 2.2.4 дана простая программа обработки консольных событий.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; тип события
KEY_EV equ 1h
MOUSE_EV equ 2h
; константы - состояния клавиатуры
RIGHT_ALT_PRESSED equ 1h
LEFT_ALT_PRESSED equ 2h
RIGHT_CTRL_PRESSED equ 4h
LEFT_CTRL_PRESSED equ 8h
SHIFT_PRESSED equ 10h
NUMLOCK_ON equ 20h
SCROLLLOCK_ON equ 40h
CAPSLOCK_ON equ 80h
ENHANCED_KEY equ 100h
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleInputA@16:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; структура для определения событий
COOR STRUC
Х WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
TITL DB "Обработка событий мыши",0
BUF DB 200 dup (?)
LENS DWORD ? ; количество выведенных символов
C0 DWORD ?
FORM DB "Координаты: %u %u "
CRD COOR <?>
STR1 DB "Для выхода нажмите ESC",0
MOUS_KEY WORD 9 dup (?)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; задать заголовок окна консоли
PUSH OFFSET TITL
CALL SetConsoleTitleA@4
;**********************************
; перекодировка строки
PUSH OFFSET STR1
PUSH OFFSET STR1
CALL CharToOemA@8
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR1
PUSH HANDL
CALL WriteConsoleA@20
; цикл ожиданий: движение мыши или двойной щелчок
L00:
; координаты курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; прочитать одну запись о событии
PUSH OFFSET C0
PUSH 1
PUSH OFFSET MOUS_KEY
PUSH HANDL1
CALL ReadConsoleInputA@16
; проверим, не с мышью ли что?
CMP WORD PTR MOUS_KEY, MOUSE_EV
JNE L001
; здесь преобразуем координаты мыши в строку
MOV AX, WORD PTR MOUS_KEY+6 ; Y-мышь
; копирование с обнулением старших битов
MOVZX EAX,AX
PUSH EAX
MOV AX, WORD PTR MOUS_KEY+4 ; Х-мышь
; копирование с обнулением старших битов
MOVZX EAX,AX
PUSH EAX
PUSH OFFSET FORM
PUSH OFFSET BUF
CALL wsprintfA
; восстановить стек
ADD ESP,16
; перекодировать строку для вывода
PUSH OFFSET BUF
PUSH OFFSET BUF
CALL CharToOemA@8
; длина строки
PUSH OFFSET BUF
CALL LENSTR
; вывести на экран координаты курсора
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
JMP L00 ; к началу цикла
L001:
; нет ли события от клавиатуры?
CMP WORD PTR MOUS_KEY,KEY_EV
JNE L00
; есть, какое?
CMP BYTE PTR MOUS_KEY+14,27
JNE L00
;********************************
; закрыть консоль
CALL FreeConsole@0
PUSH 0
CALL ExitProcess@4
RET
; процедура определения длины строки
; строка - [EBP+08Н]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
CLD
MOV EDI, DWORD PTR [EBP+08Н]
MOV EBX, EDI
MOV ECX, 100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX, EDI
DEC EBX
POP EAX
LEAVE
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.4. Пример обработки событий от мыши и клавиатуры для консольного приложения.

После того как вы познакомились с программой на Рис. 2.2.4, давайте ее подробнее обсудим.

Начнем с функции wsprintfA. Как я уже заметил, функция необычная.

    Она имеет переменное число параметров. Первые два параметра обязательны. Вначале идет указатель на буфер, куда будет скопирована результирующая строка. Вторым идет указатель на форматную строку. Форматная строка может содержать текст, а также формат выводимых параметров. Поля, содержащие информацию о параметре, начинаются с символа "%". Формат этих полей в точности соответствует формату полей, используемых в стандартных Си-функциях printf, sprintf и др. Исключением является отсутствие в формате для функции wsprintf вещественных чисел. Нет нужды излагать этот формат, заметим только, что каждое поле в форматной строке соответствует параметру (начиная с третьего). В нашем случае форматная строка была равна: "Координаты: %u %u". Это означало, что далее в стек будет отправлено два числовых параметра типа WORD. Конечно, в стек мы отправили два двойных слова, позаботившись лишь о том, чтобы старшие слова были обнулены. Для такой операции очень удобна команда микропроцессора MOVZX, которая копирует второй операнд в первый так, чтобы биты старшего слова были заполнены нулями. Если бы параметры были двойными словами, то вместо поля %u мы бы поставили %lu. В случае, если поле форматной строки определяет строку-параметр, например "%S", в стек следует отправлять указатель на строку (что естественно).29 Поскольку функция "не знает", сколько параметров может быть в нее отправлено, разработчики не стали усложнять текст этой функции, и оставили нам проблему освобождения стека30. Это производится командой ADD ESP,N. Здесь N - это количество освобождаемых байтов.

Обратимся теперь к функции ReadConsoleInputA. К уже сказанному о ней добавлю только, что если буфер событий пуст, то функция будет ждать, пока "что-то" не случится с консольным окном, и только тогда возвратит управление. Кроме того, мы можем указать, чтобы функция возвращала не одну, а несколько записей о происшедших с консолью событиях. В этом случае в буфер будет помещена не одна, а несколько информационных записей. Но мы на этом останавливаться не будем.

По обыкновению отмечу, как откомпилировать данную программу в TASM32. Как обычно, удаляем все значки @N, указываем библиотеку import32.lib и наконец wsprintfA меняем на _wsprintfA.


29 В этой связи не могу не отметить, что встречал в литературе по ассемблеру (!) утверждение, что все помещаемые в стек для этой функции параметры являются указателями. Как видим, вообще говоря, это не верно.

30 Компилятор Си, естественно, делает это за нас.


IV

В последнем разделе главы мы рассмотрим довольно редко освещаемый в литературе вопрос - таймеры в консольном приложении. Надо сказать, что мы несколько опережаем события и рассматриваем таймер в консольном приложении раньше, чем в приложении GUI (Graphic Universal Interface - так называются обычные оконные приложения).

Основным способом создания таймера является использование функции SetTimer. Позднее мы будем подробно о ней говорить. Таймер может быть установлен в двух режимах. Первый режим - это когда последний параметр равен нулю. В этом случае на текущее окно (его функцию) через равные промежутки времени, определяемые третьим параметром, будет приходить сообщение WM_TIMER. Во втором режиме последний параметр указывает на функцию, которая будет вызываться опять через равные промежутки времени. Однако для консольного приложения эта функция не подходит, так как сообщение WM_TIMER пересылается окну функцией DispatchMessage, которая используется в петле обработки сообщений. Но использование этой функции для консольных приложений проблематично.

Для консольных приложений следует использовать функцию timeSetEvent. Вот параметры этой функции:
1-й параметр - время задержки таймера, для нас это время совпадает со временем между двумя вызовами таймера. 2-й параметр - точность работы таймера (приоритет посылки сообщения). 3-й параметр - адрес вызываемой процедуры. 4-й параметр - параметр, посылаемый в процедуру. 5-й параметр - тип вызова - одиночный или периодический.

Если функция завершилась удачно, то в EAX возвращается идентификатор таймера.

Сама вызываемая процедура получает также 5 параметров:
1-й параметр - идентификатор таймера. 2-й параметр - не используется. 3-й параметр - параметр Dat (см. timeSetEvent). 4 и 5-й параметры - не используются.

Для удаления таймера используется функция timeKillEvent, параметром которой является идентификатор таймера.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
TIME_PERIODIC equ 1 ; тип вызова таймера
; атрибуты цветов
FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность
COL1 = 2h+8h ; цвет выводимого текста
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN timeSetEvent@20:NEAR
EXTERN timeKillEvent@4:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\winmm.lib
;------------------------------------------------------------
COOR STRUC
X WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
STR2 DB "Пример таймера в консольном приложении",0
STR3 DB 100 dup (0)
FORM DB "Число вызовов таймера: %lu",0
BUF DB 200 dup (?)
NUM DWORD 0
LENS DWORD ? ; количество выведенных символов
CRD COOR <?>
ID DWORD ? ; идентификатор таймера
HWND DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; задать заголовок окна консоли
PUSH OFFSET STR2
CALL SetConsoleTitleA@4
; задать цветовые атрибуты выводимого текста
PUSH COL1
PUSH HANDL
CALL SetConsoleTextAttribute@8
; установить таймер
PUSH TIME_PERIODIC ; периодический вызов
PUSH 0
PUSH OFFSET TIME ; вызываемая таймером процедура
PUSH 0 ; точность вызова таймера
PUSH 1000 ; вызов через одну секунду
CALL timeSetEvent@20
MOV ID, EAX
; ждать ввод строки
PUSH 0
PUSH OFFSET LENS
PUSH 200
PUSH OFFSET BUF
PUSH HANDL1
CALL ReadConsoleA@20
; закрыть таймер
PUSH ID
CALL timeKillEvent@4
; закрыть консоль
CALL FreeConsole@0
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
;--------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;--------------------
POP EAX
LEAVE
RET 4
LENSTR ENDP
; процедура вызывается таймером
TIME PROC
PUSHA ; сохранить все регистры
; установить позицию курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; заполнить строку STR3
PUSH NUM
PUSH OFFSET FORM
PUSH OFFSET STR3
CALL wsprintfA
ADD ESP,12 ; восстановить стек
; перекодировать строку STR3
PUSH OFFSET STR3
PUSH OFFSET STR3
CALL CharToOemA@8
; вывести строку с номером вызова таймера
PUSH OFFSET STR3
CALL LENSTR
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR3
PUSH HANDL
CALL WriteConsoleA@20
INC NUM
POPA
RET 20 ; выход с освобождением стека
TIME ENDP
_TEXT ENDS
END START

Рис. 2.2.5. Таймер в консольном режиме.

Программа на Рис. 2.2.5 будет выводить в окно значение счетчика, которое будет каждую секунду увеличиваться на единицу.

Я начал данную главу с рассуждения о командной строке, но до сих пор не объявил, как работать с командной строкой. О, здесь все очень просто. Есть API-функция GetCommandLine, которая возвращает указатель на командную строку. Эта функция одинаково работает как для консольных приложений, так и для приложений GUI. Ниже представлена программа, печатающая параметры командной строки. Надеюсь, вы понимаете, что первым параметром является полное имя программы.

; программа вывода параметров командной строки
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
BUF DB 100 dup (0)
LENS DWORD ? ; количество выведенных символов
NUM DWORD ?
CNT DWORD ?
HANDL DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; получить количество параметров
CALL NUMPAR
MOV NUM,EAX
MOV CNT,0
;-------------------------------------
; вывести параметры командной строки
LL1:
MOV EDI,CNT
CMP NUM,EDI
JE LL2
; номер параметра
INC EDI
MOV CNT, EDI
; получить параметр номером EDI
LEA EBX,BUF
CALL GETPAR
; получить длину параметра
PUSH OFFSET BUF
CALL LENSTR
; в конце - перевод строки
MOV BYTE PTR [BUF+EBX],13
MOV BYTE PTR [BUF+EBX+1],10
MOV BYTE PTR [BUF+EBX+2],0
ADD EBX,2
; вывод строки
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
JMP LL1
LL2:
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
;--------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;--------------------
POP EAX
POP EBP
RET 4
LENSTR ENDP
; определить количество параметров (->EAX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX, 0
JMP L2
L3:
OR EDX, 1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
; получить параметр
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI, EAX ; указатель на строку
XOR ECX, ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI] ,32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX, 0
JMP L2
L3:
OR EDX, 1
L2:
CMP ECX, EDI
JNE L5
MOV AL, BYTE PTR [ESI]
MOV BYTE PTR [EBX],AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX],0
RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.2.6. Пример работы с параметрами командной строки.

Рекомендую читателю разобраться в алгоритме работы процедур NUMPAR и GETPAR.

Следует отметить, что для трансляции программы на Рис. 2.2.6 в TASM, кроме обычных, уже известных Вам изменений, для совпадающих меток следует в начале имени поставить "@@" - признак локальности, а в начале программы поставить директиву LOCALS. Транслятор MASM метки, стоящие в процедуре, считает локальными автоматически. Подробнее о локальных метках будет сказано в Главах 2.5 и 2.6.



Глава 3. Понятие ресурса. Редакторы и трансляторы ресурсов

В операционную систему Windows введено понятие ресурса. Ресурс представляет собой некий визуальный элемент с заданными свойствами, хранящийся в исполняемом файле отдельно от кода и данных, который может отображаться специальными функциями.

Использование ресурсов дает две вполне определенные выгоды:

    Ресурсы загружаются в память лишь при обращении к ним, т.е. реализуется экономия памяти. Свойства ресурсов поддерживаются системой автоматически, не требуя от программиста написания дополнительного кода.

Описание ресурсов хранится отдельно от программы в текстовом файле (*.rc) и компилируется (*.res) специальным транслятором ресурсов. В исполняемый файл ресурсы включаются компоновщиком. Транслятором ресурсов в пакете MASM32 является RC.EXE, в пакете TASM32 - BRCC32.EXE.

I

В данном разделе мы займемся изучением языка ресурсов. Зная его, можно вполне обойтись без специального редактора ресурсов. В настоящее время существует большое количество редакторов ресурсов. Видимо, поэтому книги по программированию уделяют мало внимания языку описания ресурсов. Мы, напротив, не будем касаться этих программ31, а как можно подробнее разберем структуру и язык ресурсов. Отметим также, что изложенный стандарт одинаково будет восприниматься обоими компиляторами ресурсов. Исключения будут обговорены отдельно.

Начнем с перечисления наиболее употребляемых ресурсов.

    Иконки. Курсоры. Битовая картинка. Строка. Диалоговое окно. Меню. Акселераторы.

Вот наиболее распространенные ресурсы. Надо только иметь в виду, что такой ресурс, как диалоговое окно, может содержать в себе управляющие элементы, которые также должны быть описаны, но в рамках описания окна. Но об этом поговорим несколько позже.

1. Иконки. Могут быть описаны в самом файле ресурсов, либо храниться в отдельном файле *.ico. Рассмотрим последний случай. Вот файл ресурсов resu.rc:

#define IDI_ICON1 1
IDI_ICON1 ICON "Cdrom01.ico"

Как видите, файл содержит всего две значимых строки. Одна строка определяет идентификатор иконки, вторая - ассоциирует идентификатор с файлом "Cdrom01.ico". Оператор define является Си-оператором препроцессора. Как вы увидите в дальнейшем, язык ресурсов очень напоминает язык Си. Откомпилируем текстовый файл resu.rc: RC resu.rc. На диске появляется объектный файл resu.res. При компоновке укажем этот файл в командной строке:

LINK /subsystem:windows resu.obj resu.res

У читателя возникает вопрос: как использовать данный ресурс в программе? Здесь все просто: предположим, что мы хотим установить новую иконку для окна. Вот фрагмент программы, который устанавливает стандартную иконку для главного окна.

PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX

А вот фрагмент программы для установки иконки, указанной в файле ресурсов:

PUSH 1 ; идентификатор иконки (см. файл resu.rc)
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX

Компилятор ресурсов brcc32.exe (из пакета TASM32) допускает включение иконки в текст проекта. В этом случае проект будет иметь следующий вид (Рис. 2.3.1):

#define IDI_ICON1 1
IDI_ICON1 ICON
{
'00 00 01 00 02 00 20 20 10 00 00 00 00 00 E8 02'
'00 00 26 00 00 00 10 10 10 00 00 00 00 00 28 01'
'00 00 0E 03 00 00 28 00 00 00 20 00 00 00 40 00'
'00 00 01 00 04 00 00 00 00 00 80 02 00 00 00 00'
'00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00'
'00 00 00 00 BF 00 00 BF 00 00 00 BF BF 00 BF 00'
'00 00 BF 00 BF 00 BF BF 00 00 C0 C0 C0 00 80 80'
'80 00 00 00 FF 00 00 FF 00 00 00 FF FF 00 FF 00'
'00 00 FF 00 FF 00 FF FF 00 00 FF FF FF 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 77 78 33 AA 00 00 00 00 00 00 00 00'
'00 00 07 7F 77 78 33 AA 77 80 00 00 00 00 00 00'
'00 0F F7 F7 77 78 33 AA 77 C8 60 00 00 00 00 00'
'00 FF FF 7F 77 78 33 AA 78 C6 66 00 00 00 00 00'
'0F FF FF F7 77 78 38 A7 7C 86 66 60 00 00 00 00'
'77 FF FF 7F 77 78 37 A7 8C 66 66 77 00 00 00 07'
'87 7F FF F7 F7 78 37 A7 C8 66 67 77 70 00 00 08'
'78 77 FF FF 77 78 3A A8 C6 66 77 77 E0 00 00 87'
'87 87 7F FF F7 78 3A AC 86 67 77 EE EE 00 00 78'
'78 78 77 FF 7F 78 3A 8C 66 77 EE EE BB 00 07 87'
'87 87 87 7F F7 78 3A C8 67 7E EB BB BA A0 08 78'
'78 78 78 77 F8 88 88 C6 7E BB BB AA AA A0 07 87'
'87 87 87 87 88 00 00 88 BB BA AA A3 33 30 08 78'
'78 78 78 78 80 8F F8 08 33 33 33 DD DD D0 08 88'
'88 88 88 88 80 FF FF 08 5D 5D 5D 5D 5D 50 05 D5'
'D5 D5 D5 D5 80 FF FF 08 88 88 88 88 88 80 0D DD'
'DD 33 33 33 80 8F F8 08 87 87 87 87 87 80 03 33'
'3A AA AB BB 88 00 00 88 78 78 78 78 78 70 0A AA'
'AA BB BB E7 6C 88 88 8F 77 87 87 87 87 80 0A AB'
'BB BE E7 76 8C A3 87 7F F7 78 78 78 78 70 00 BB'
'EE EE 77 66 C8 A3 87 F7 FF 77 87 87 87 00 00 EE'
'EE 77 76 68 CA A3 87 7F FF F7 78 78 78 00 00 0E'
'77 77 66 6C 8A A3 87 77 FF FF 77 87 80 00 00 07'
'77 76 66 8C 7A 73 87 7F 7F FF F7 78 70 00 00 00'
'77 66 66 C8 7A 73 87 77 F7 FF FF 77 00 00 00 00'
'06 66 68 C7 7A 83 87 77 7F FF FF F0 00 00 00 00'
'00 66 6C 87 AA 33 87 77 F7 FF FF 00 00 00 00 00'
'00 06 8C 77 AA 33 87 77 7F 7F F0 00 00 00 00 00'
'00 00 08 77 AA 33 87 77 F7 70 00 00 00 00 00 00'
'00 00 00 00 AA 33 87 77 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF F0'
'0F FF FF 80 01 FF FE 00 00 7F FC 00 00 3F F8 00'
'00 1F F0 00 00 0F E0 00 00 07 C0 00 00 03 C0 00'
'00 03 80 00 00 01 80 00 00 01 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 80 00 00 01 80 00 00 01 C0 00 00 03 C0 00'
'00 03 E0 00 00 07 F0 00 00 0F F8 00 00 1F FC 00'
'00 3F FE 00 00 7F FF 80 01 FF FF F0 0F FF 28 00'
'00 00 10 00 00 00 20 00 00 00 01 00 04 00 00 00'
'00 00 C0 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80'
'00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80'
'00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF'
'00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF'
'00 00 FF FF FF 00 00 00 00 00 00 00 00 00 00 00'
'08 87 3A 80 00 00 00 0F F8 87 32 CC 60 00 00 08'
'F8 87 32 C6 68 00 00 87 8F 87 2C 66 86 00 08 78'
'78 87 2C 68 AA A0 07 87 87 70 08 2A A2 20 08 78'
'78 0F F0 II 15 50 05 51 II 0F F0 87 87 80 02 2A'
'A2 80 08 78 78 70 0A AA 86 C2 78 87 87 80 00 68'
'66 C2 78 F8 78 00 00 86 6C 23 78 8F 88 00 00 06'
'CC 23 78 8F F0 00 00 00 08 A3 78 80 00 00 00 00'
'00 00 00 00 00 00 F8 IF 00 00 E0 07 00 00 C0 03'
'00 00 80 01 00 00 80 01 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 80 01 00 00 80 01 00 00 C0 03 00 00 E0 07'
'00 00 F8 1F 00 00'
}

Рис. 2.3.1. Пример файла ресурсов с включенным туда кодом иконки.

2. Курсоры. Подход здесь полностью идентичен. Привожу ниже файл ресурсов, где определен и курсор, и иконка.

#define IDI_ICON1 1
#define IDI_CUR1 2
IDI_ICON1 ICON "Cdrom01.ico"
IDI_CUR1 CURSOR "4way01.cur"

А вот фрагмент программы, вызывающей иконку и курсор.

;----------иконка окна
PUSH 1 ; идентификатор иконки
PUSH [HINST]
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
; ----------курсор окна
PUSH 2 ; идентификатор курсора
PUSH [HINST]
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR], EAX

Как и для иконки, программа brcc32.exe обрабатывает определение курсора в тексте файла ресурсов.

3. Битовые картинки (*.BMP). Здесь ситуация аналогична двум предыдущим. Вот пример файла ресурсов с битовой картинкой.

#define ВIТ1 1
BIT1 BITMAP "PIR2.BMP"

4. Строки. Чтобы задать строку или несколько строк используется ключевое слово STRINGTABLE. Ниже представлен текст ресурса, задающий две строки. Для загрузки строки в программу используется функция LoadString (см. ниже). Строки, задаваемые в файле ресурсов, могут играть роль констант.

#define STR1 1
#define STR2 2
STRINGTABLE
{
STR1, "Сообщение"
STR2, "Версия 1.01"
}

5. Диалоговые окна. Диалоговые окна являются наиболее сложными элементами ресурсов. В отличие от ресурсов, которые мы до сих пор рассматривали, для диалога не задается идентификатор. Обращение к диалогу происходит по его имени (строке).

#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

Как видим, определение диалога начинается со строки, содержащей ключевое слово DIALOG. В этой же строке далее указывается положение и размер диалогового окна. Далее идут строки, содержащие другие свойства окна. Наконец идут фигурные скобки. В данном случае они пусты. Это означает, что на окне нет никаких управляющих элементов. Тип окна, а также других элементов определяется константами, которые мы поместили в начале файла. Эти константы стандартны, и для языка Си хранятся в файле RESOURCE.H. Мы, как и раньше, все константы будем определять непосредственно в файле ресурсов. Обращаю ваше внимание, что константы определяются согласно нотации языка Си.

Прежде чем разбирать пример на Рис. 2.3.2, рассмотрим особенности работы с диалоговыми окнами. Диалоговое окно очень похоже на обычное окно. Так же как обычное окно, оно имеет свою процедуру. Процедура диалогового окна имеет те же параметры, что и процедура обычного окна. Сообщений, которые приходят на процедуру диалогового окна, гораздо меньше. Но те, которые у диалогового окна имеются, в основном совпадают с аналогичными сообщениями для обычного окна. Только вместо сообщения WM_CREATE приходит сообщение WM_INITDIALOG. Процедура диалогового окна может возвращать либо нулевое, либо ненулевое значение. Ненулевое значение должно возвращаться в том случае, если процедура обрабатывает (берет на себя обработку) данное сообщение, и ноль - если предоставляет обработку системе.

Отличия в поведении диалогового окна от обычного окна легко объяснить. Действительно, если Вы создаете обычное окно, то все его свойства определяются тремя факторами: свойствами класса, свойствами, определяемыми при создании окна, реакцией процедуры окна на определенные сообщения. При создании диалогового окна все свойства заданы в ресурсах. Часть этих свойств задается, когда при вызове функции создания диалогового окна (DialogBox, DialogBoxParam и др.) неявно вызывается функция CreateWindow. Остальная же часть свойств определяется поведением внутренней функции, которую создает система при создании диалогового окна. Если с диалоговым окном что-то происходит, то сообщение сначала приходит на внутреннюю процедуру, а затем вызывается процедура диалогового окна, которую мы создаем в программе. Если процедура возвращает 0, то внутренняя процедура продолжает обработку данного сообщения, если же возвращается ненулевое значение, внутренняя процедура не обрабатывает сообщение. Вот, вкратце, как работают механизмы, регулирующие работу диалогового окна. Рассмотрите теперь программу на Рис. 2.3.2, ниже будет дано ее разъяснение.

// файл dial.rc
// определение констант
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
// идентификаторы
#define STR1 1
#define STR2 2
#define IDI_ICON1 3
// определили иконку
IDI_ICON1 ICON "ico1.ico"
// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}
// определение строк
STRINGTABLE
{
STR1, "Сообщение"
STR2, "Версия программы 1.00"
}

; файл dial.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_SETICON equ 80h
; прототипы внешних процедур
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN LoadStringA@16:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN SendMessageA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
;файл dial.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;--------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
BUF1 DB 40 dup(0)
BUF2 DB 40 dup(0)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST],EAX
;------
; загрузить строку
PUSH 40
PUSH OFFSET BUF1
PUSH 1
PUSH [HINST]
CALL LoadStringA@16
; загрузить строку
PUSH 40
PUSH OFFSET BUF2
PUSH 2
PUSH [HINST]
CALL LoadStringA@16
;------------------------------------------------------------
PUSH 0 ; MB_OK
PUSH OFFSET BUF1
PUSH OFFSET BUF2
PUSH 0
CALL MessageBoxA@16
; создать диалоговое окно
PUSH 0
PUSH OFFSET WNDPROC ; процедура окна
PUSH 0
PUSH OFFSET PA ; название ресурса (DIAL1)
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
KOL:
PUSH 0
CALL ExitProcess@4
;--------------------------
; процедура диалогового окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CН] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
;-----
CMP DWORD PTR [EBP+ОСН], WM_CLOSE
JNE L1
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
JMP FINISH
L1:
CMP DWORD PTR [EBP+ОСН], WM_INITDIALOG
JNE FINISH
; загрузить иконку
PUSH 3 ; идентификатор иконки
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
; установить иконку
PUSH EAX
PUSH 0 ; тип иконки (маленькая)
PUSH WM_SETICON
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
MOV EAX, 0
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.3.2. Демонстрация использования простых ресурсов.

Рассмотрим теперь, как работает эта программа.

    Файл ресурсов должен быть Вам понятен, так как все используемые там ресурсы были подробно рассмотрены ранее. Замечу только, что файл ресурсов содержит сразу несколько элементов. При этом все ресурсы, кроме диалогового окна, должны иметь идентификатор. Для диалогового окна определяющим является его название, в нашем случае это DIAL1. Перед тем как вызвать диалоговое окно, демонстрируется то, как нужно работать с таким ресурсом, как строка. Как видите, это достаточно просто. При помощи функции LoadString строка загружается в буфер, после чего с ней можно работать, как с обычной строкой. Вызов диалогового окна достаточно очевиден, так что перейдем сразу к процедуре диалогового окна. Начнем с сообщения WM_INITDIALOG. Это сообщение, как и сообщение WM_CREATE для обычного окна, приходит один раз при создании окна. Это весьма удобно для проведения какой-то начальной инициализации. Мы используем это для определения иконки диалогового окна. В начале загружаем иконку, а далее посылаем сообщение установить иконку для данного окна (WM_SETICON). Вторым сообщением, которое мы обрабатываем, является WM_CLOSE. Это сообщение приходит, когда происходит щелчок мышью по крестику в правом верхнем углу экрана. По получении этого сообщения выполняется функция EndDialog, что приводит к удалению диалогового окна из памяти, выходу из функции DialogBoxParamA и в конечном итоге - к выходу из программы.

Выше было сказано, что процедура диалогового окна должна возвращать ненулевое значение, если она берет на себя обработку данного сообщения. Как видно из данного примера, в принципе в этом не всегда имеется необходимость. В дальнейшем мы акцентируем внимание на тех случаях, когда в этом есть необходимость.


31 Лично я предпочитаю использовать редактор ресурсов из пакета Borland C++ 5.00, либо простой текстовый редактор.


II

6. Меню. Меню также может быть задано в файле ресурсов. Как и диалоговое окно, в программе оно определяется по имени (строке). Меню можно задать и в обычном окне, и в диалоговом окне. Для обычного окна при регистрации класса следует просто заменить строку

MOV DWORD PTR [WC.CLMENNAME],0

на

MOV DWORD PTR [WC.CLMENNAME], OFFSET MENS

Здесь MENS - имя, под которым меню располагается в файле ресурсов. Меню на диалоговое окно устанавливается другим способом, который, разумеется, подходит и для обычного окна. В начале меню загружается при помощи функции LoadMenu, а затем устанавливается функцией SetMenu.

А теперь обо всем подробнее. Рассмотрим структуру файла ресурсов, содержащего определение меню. Ниже представлен текст файла, содержащего определение меню.

Далее представлена программа, демонстрирующая меню на диалоговом окне.

MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
POPUP "Подмен&ю"
{
MENUITEM "Десятый пунк&т",6
}
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4
}
MENUITEM "Вы&ход",5
}

Внимательно рассмотрите текст меню. Как видите, пункты меню имеют идентификаторы, по которым в программе можно определить, какой пункт меню выбран. Можно заметить, что выпадающее меню может содержать еще и подменю.

//файл menu.rc
//определение констант
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_POPUP 0x80000000L
#define WS_CAPTION 0x00C00000L
MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4
POPUP "Еще подмен&ю"
{
MENUITEM "Десятый пунк&т", 6
}
}
MENUITEM "Вы&ход", 5
}
// идентификаторы
#define IDI_ICON1 100
; определили иконку
IDI_ICON1 ICON "ico1.ico"
//определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}
; файл menu.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_SETICON equ 80h
WM_COMMAND equ 111h
; прототипы внешних процедур
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN LoadStringA@16:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN LoadMenuA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN SetMenu@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
; файл menu.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include menu. inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
PMENU DB "MENUP",0
STR1 DB "Выход из программы",0
STR2 DB "Сообщение",0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
;---------------------------------
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
KOL:
PUSH 0
CALL ExitProcess@4
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0CH] MES
; [EBP+8] HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
;-----------------
CMP DWORD PTR [EBP+0CH],WM_CLOSE
JNE L1
; закрыть диалоговое окно
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
JMP FINISH
L1:
CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
JNE L2
; загрузить иконку
PUSH 100 ; идентификатор иконки
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
; установить иконку
PUSH EAX
PUSH 0 ; тип иконки (маленькая)
PUSH WM_SETICON
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; загрузить меню
PUSH OFFSET PMENU
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
JMP FINISH
L2:
; проверяем, не случилось ли чего с управляющими элементами
; на диалоговом окне, в нашем случае имеется единственный
; управляющий элемент - это меню
CMP DWORD PTR [EBP+0CH], WM_COMMAND
JNE FINISH
; здесь определяем идентификатор, в данном случае
; это идентификатор пункта меню
CMP WORD PTR [EBP+10Н],5
JNE FINISH
; сообщение
PUSH 0 ; МВ_ОК
PUSH OFFSET STR2
PUSH OFFSET STR1
PUSH 0
CALL MessageBoxA@16
; закрыть диалоговое окно
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
FINISH:
MOV EAX, 0
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.3.3. Пример программы с меню.

Дадим небольшой комментарий к программе на Рис. 2.3.3. Прежде всего обращаю ваше внимание на довольно прозрачную аналогию между диалоговым окном и меню. Действительно, и в том и в другом случае ресурс определяется не по идентификатору, а по имени. Далее, и диалоговое окно, и меню содержат в себе элементы, определяющиеся по их идентификаторам, которые мы задали в файле ресурсов и которые помещаются в младшее слово параметра WPARAM. В программе на Рис. 2.3.3 мы программно загружаем меню. Можно поступить и по другому: указать меню в опциях определения диалогового окна следующим образом.

//определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
MENU MENUP
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

Этого достаточно, чтобы меню загрузилось и отобразилось автоматически.

Хочу напомнить читателю одну вещь. В главе 1.3 приводился пример кнопки, которая создавалась как дочернее окно. То, что нажата имение эта кнопка, мы определяли по содержимому LPARAM, который содержал дескриптор кнопки.

Как видите, идентифицировать элемент, расположенный на диалоговом окне, можно и по дескриптору, и по идентификатору ресурса.

Вернемся к меню. Пункты меню могут содержать дополнительные параметры, которые определяют дополнительные свойства этих пунктов.

Вот эти свойства, понимаемые компилятором ресурсов:
CHECKED - пункт отмечен "птичкой".
GRAYED - элемент недоступен (имеет серый цвет).
HELP - элемент может быть связан с помощью. Редакторы ресурсов дополнительно создают ресурс - строку. При этом идентификатор строки совпадает с идентификатором пункта меню.
MENUBARBREAK - для горизонтального пункта это означает, что начиная с него горизонтальные пункты располагаются в новой строке. Для вертикального пункта - то, что начиная с него пункты расположены в новом столбце. При этом проводится разделительная линия.
MENUBREAK - аналогично предыдущему, но разделительная линия не проводится.
INACTIVE - пункт не срабатывает.
SEPARATOR - создает в меню разделитель. При этом идентификатор не ставится.

Заканчивая рассуждение о меню, замечу, что у Windows есть обширнейший набор функций, с помощью которых можно менять свойства меню (удалять и добавлять пункты меню, менять их свойства). В следующей главе приводятся примеры некоторых из этой группы.

III

7. Акселераторы. На первый взгляд этот вопрос достаточно прост, но, как станет ясно, он потянет за собой множество других. Акселератор позволяет выбирать пункт меню просто сочетанием клавиш.

Это очень удобно и быстро. Таблица акселераторов является ресурсом, имя которого должно совпадать с именем того меню (ресурса), пункты которого она определяет.

Вот пример такой таблицы. Определяется один акселератор на пункт меню MENUP, имеющий идентификатор 4.

MENUP ACCELERATORS
{
VK_F5, 4, VIRTKEY
}

А вот общий вид таблицы акселераторов.

Имя ACCELERATORS
{
Клавиша 1, Идентификатор пункта меню (1) [,тип] [,параметр]
Клавиша 2, Идентификатор пункта меню (2) [,тип] [,параметр]
Клавиша 3, Идентификатор пункта меню (3) [,тип] [,параметр]
...
Клавиша N, Идентификатор пункта меню (N) [,тип] [,параметр]
}

Рассмотрим представленную схему. Клавиша - это либо символ в кавычках, либо код ASCII символа, либо виртуальная клавиша. Если вначале стоит код символа, то тип задается как ASCII. Если используется виртуальная клавиша, то тип определяется как VIRTUAL. Все названия (макроимена) виртуальных клавиш можно найти в include- файлах (windows.h). Мы, как обычно, будем определять все макроимена непосредственно в программе.

Параметр может принимать одно из следующих значений: NOINVERT, ALT, CONTROL, SHIFT. Значение NOINVERT означает, что не подсвечивается выбранный при помощи акселератора пункт меню. Значения ALT, SHIFT, CONTROL означают, что, кроме клавиши, определенной в акселераторе, должна быть нажата одна из управляющих клавиш. Кроме этого, если клавиша определяется в кавычках, то нажатие при этом клавиши CONTROL определяется знаком "^": "^А".

А теперь поговорим о механизме работы акселераторов. Для того чтобы акселераторы работали, необходимо выполнить два условия:

    Должна быть загружена таблица акселераторов. Для этого используется функция LoadAccelerators. Сообщения, пришедшие от акселератора, следует преобразовать в сообщение WM_COMMAND. Здесь нам пригодится функция TranslateAccelerator.

Остановимся подробнее на втором пункте. Функция TranslateAccelerator преобразует сообщения WM_KEYDOWN и WM_SYSKEYDOWN в сообщения WM_COMMAND и WM_SYSCOMMAND соответственно. При этом в старшем слове параметра WPARAM помещается 1, как отличие для акселератора. В младшем слове, как Вы помните, содержится идентификатор пункта меню. Возникает вопрос: для чего необходимы два сообщения WM_COMMAND и WM_SYSCOMMAND? Здесь все закономерно: сообщение WM_SYSCOMMAND генерируется для пунктов системного меню или меню окна (см. Рис. 2.3.4).

Функция TranslateAccelerator возвращает ненулевое значение, если было произведено преобразование сообщения акселератора, в противном случае возвращается 0. Естественно включить вызов этой функции в кольцо сообщений. Вот этот фрагмент.

MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP EAX, 0
JE END_LOOP
PUSH OFFSET MSG
PUSH [ACC]
PUSH [NEWHWND]
CALL TranslateAcceleratorA@12
CMP EAX ,0
JNE MSG_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:

Рис. 2.3.4. Меню окна.

Фрагмент Вам знаком, но в него вставлена функция TranslateAccelerator. Первым параметром этой функции идет дескриптор приложения, вторым параметром идет дескриптор таблицы акселераторов ([ACC]), получаемый при загрузке таблицы с помощью функции LoadAccelerators. Третий параметр - адрес, где содержится сообщение, полученное функцией GetMessage.

И вот тут начинается самое интересное, потому что Вы можете сказать, что кольцо обработки сообщений используется, когда основное окно создается обычным способом, посредством регистрации класса окна и последующего вызова функции CreateWindow. В данной же главе мы рассматриваем диалоговые окна. Конечно, диалоговые окна могут порождаться обычным окном, и здесь все нормально. Но как быть в простейшем случае одного диалогового окна. Первое, что приходит в голову — это поместить функцию TranslateAccelerator в функцию окна и там, на месте, осуществлять преобразования. Но сообщения от акселератора до этой функции не доходят. И здесь мы подходим к новому материалу: модальные и немодальные окна.

До сих пор мы работали с модальными диалоговыми окнами. Эти окна таковы, что при их вызове программа должна дожидаться, когда окно будет закрыто. Существуют и немодальные диалоговые окна. После их вызова программа продолжает свою работу. При этом немодальное окно позволяет переключаться на другие окна. Каковы особенности создания немодального диалога?

    Немодальный диалог создается при помощи функции CreateDialog. Уничтожается немодальный диалог функцией DestroyWindow. Для того чтобы немодальный диалог появился на экране, нужно либо указать у него свойство WS_VISIBLE, либо после создания диалога выполнить команду ShowWindow.

Ниже (Рис. 2.3.5) представлена программа, демонстрирующая немодальный диалог с меню и обработкой сообщений акселератора.

// файл menu1.rc
// определение констант
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_POPUP 0x80000000L
#define VK_F5 0х74
#define st WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2,HELP
MENUITEM "Что-то?",8
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4
MENUITEM SEPARATOR
POPUP "Еще подмен&ю"
{
MENUITEM "Десятый пунк&т",6
}
}
MENUITEM "Вы&ход",5
}
// идентификаторы
#define IDI_ICON1 100
// определили иконку
IDI_ICON1 ICON "ico1.ico"
// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | st
CAPTION "Пример немодального диалогового окна"
FONT 8, "Arial"
{
}
MENUP ACCELERATORS
{
VK_F5, 4, VIRTKEY, ALT
}
; файл menu1.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_SETICON equ 80h
WM_COMMAND equ 111h
; прототипы внешних процедур
EXTERN ShowWindow@8:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN LoadMenuA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN SetMenu@8:NEAR
EXTERN LoadAcceleratorsA@8:NEAR
EXTERN TranslateAcceleratorA@12:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN CreateDialogParamA@20:NEAR
EXTERN DestroyWindow@4:NEAR
EXTERN TranslateMessage@4:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
;файл menu1.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include menu1.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
PMENU DB "MENUP",0
STR1 DB "Выход из программы",0
STR2 DB "Сообщение",0
STR3 DB "Выбран четвертый", О
АСС DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
; загрузить акселераторы
PUSH OFFSET PMENU
PUSH [HINST]
CALL LoadAcceleratorsA@8
MOV АСС, EAX ; запомнить дескриптор таблицы
; создать немодальный диалог
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL CreateDialogParamA@20
; визуализировать немодальный диалог
MOV NEWHWND, EAX
PUSH 1 ; SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8 ; показать созданное окно
; кольцо обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@l6
CMP EAX, 0
JE END_LOOP
; транслировать сообщение акселератора
PUSH OFFSET MSG
PUSH [АСС]
PUSH [NEWHWND]
CALL TranslateAcceleratorA@12
CMP EAX,0
JNE MSG_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:
PUSH 0
CALL ExitProcess@4
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0CH] MES
; [EBP+8] HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
;-------------------------
CMP DWORD PTR [EBP+0CH],WM_CLOSE
JNE L1
; закрыть диалоговое окно
JMP L5
L1:
CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
JNE L3
; загрузить иконку
PUSH 100 ; идентификатор иконки
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
; установить иконку
PUSH EAX
PUSH 0 ; тип иконки (маленькая)
PUSH WM_SETICON
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; загрузить меню
PUSH OFFSET PMENU
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
;-----------------------------
MOV EAX, 1 ; возвратить не нулевое значение
JMP FIN
; проверяем, не случилось ли чего с управляющими
; элементами на диалоговом окне
L3:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JE L6
JMP FINISH
; здесь определяем идентификатор, в данном случае
; это идентификатор пункта меню сообщение
L6:
CMP WORD PTR [EBP+10H], 4
JNE L4
PUSH 0 ; MB_OK
PUSH OFFSET STR2
PUSH OFFSET STR3
PUSH 0
CALL MessageBoxA@16
JMP FINISH
L4:
CMP WORD PTR [EBP+10H], 5
JNE FINISH
; сообщение
PUSH 0 ; MB_OK
PUSH OFFSET STR2
PUSH OFFSET STR1
PUSH 0
CALL MessageBoxA@16
; закрыть диалоговое немодальное окно
L5:
PUSH DWORD PTR [EBP+08H]
CALL DestroyWindow@4
; послать сообщение для выхода из кольца
; обработки сообщений
PUSH 0
CALL PostQuitMessage@4 ; сообщение WM_QUIT
FINISH:
MOV EAX, 0
FIN:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.3.5. Пример диалогового немодального окна сменю и обработкой сообщений акселераторов.

Несколько комментариев по поводу программы на Рис. 2.3.5.

    Немодальный диалог задается в файле ресурсов так же, как и модальный. Поскольку в свойствах окна нами не было указано свойство WS_VISIBLE, в программе, для того чтобы окно было видимым, мы используем функцию ShowWindow. Выход из программы в нашем случае предполагает не только удаление из памяти диалогового окна, что достигается посредством функции DestroyWindow, но и выход из цикла обработки сообщений. Последнее осуществляется через вызов функции PostQuitMessage.

В заключение отмечу, что остались неосвещенными также следующие виды ресурсов:

1. Ресурс, содержащий неструктурированные данные.

имя RCDATA
BEGIN
raw-data
. . .
END

2. Ресурс VERSIONINFO.

ID VERSIONINFO
BEGIN
block-statement
. . .
END

Оба этих ресурса используются значительно реже остальных, и мы на них останавливаться не будем.

Трансляция при помощи пакета TASM32

При работе с ресурсами в пакете TASM32 следует учитывать некоторые особенности. И дело здесь не только в том, что компиляторы ресурсов могут иметь свои конструкции языка, которые не понимает другой компилятор. Об этом мы уже говорили и больше касаться этого не будем. Есть и другое отличие. Пусть программа называется DIAL.ASM, а файл ресурсов DIAL.RC. Тогда полная трансляция в пакете TASM32 будет выглядеть следующим образом.

TASM32 /ml DIAL.ASM
BRCC32 DIAL.RC
TLINK32 DIAL.OBJ,,,,, DIAL.RES

В результате получится программа DIAL.EXE. Если программа представляет на экран диалоговое окно (именно диалоговое, а не обычное), то, скорее всего (возможно и нет). Вы обнаружите, что стиль его соответствует стилю окон Windows 3.1, чего не было в случае трансляции в MASM32. Проблема разрешится, если добавить в стиль окна константу DS_3DLOOK, равную 0x0004L. В файле помощи можно найти утверждение, что стиль DS_3DLOOK должен автоматически устанавливаться у диалоговых окон. Возможно, суть здесь заключается в особенности работы TLINK32.EXE. Других существенных отличий при работе с ресурсами в пакетах MASM32 и TASM32 я не усматриваю.



Глава 4. Примеры программ использующих ресурсы

Вопрос использования ресурсов при программировании в Windows весьма важен, поэтому я посвящаю ему еще одну главу. Здесь будет приведено три более сложных примера на использование ресурсов и подробное их разъяснение.

I

Читатель, наверное, обращал внимание, что во многих программах меню может динамически меняться во время работы: исчезают и добавляются некоторые пункты, одно меню встраивается в другое. Пример простейших манипуляций с меню приведен на Рис. 2.4.1.

Программа открывает окно с кнопкой и меню. При нажатии кнопки текущее меню заменяется другим. Если нажать еще раз, то меню исчезает. Следующее нажатие приводит к появлению первого меню и так далее, по кругу. Кроме того, в первом меню имеется пункт, который приводит к такому же результату, что и нажатие кнопки. Наконец, для этого пункта установлена акселераторная клавиша - F5. При передвижении по меню название пунктов меню и заголовков выпадающих (POPUP) подменю отображается в заголовке окна. Вот, вкратце, как работает программа. Механизмы работы программы будут подробно разобраны ниже.

// файл menu2.rc
// виртуальная клавиша F5
#define VK_F5 0х74
// ************** MENUP **************
MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый\tF5",4
MENUITEM SEPARATOR
POPUP "Еще подмен&ю"
{
MENUITEM "Дополнительный пу&нкт",6
}
}
MENUITEM "Вы&ход",5
}
//**************** MENUC ********************
MENUC MENU
{
POPUP "Набор первый"
{
MENUITEM "Белый",101
MENUITEM "Серый",102
MENUITEM "Черный",103
}
POPUP "Набор второй"
{
MENUITEM "Красный",104
MENUITEM "Синий",105
MENUITEM "Зеленый",106
}
}
// таблица акселераторов
// определен один акселератор для вызова
// пункта из меню MENUP
MENUP ACCELERATORS
{
VK_F5, 4, VIRTKEY, NOINVERT
}
; файл menu2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_COMMAND equ 111h
WM_MENUSELECT equ 11Fh
WM_SETTEXT equ 0Ch
MIIM_TYPE equ 10h
MF_STRING equ 0h
MF_POPUP equ 10h
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
STYLE equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
BS_DEFPUSHBUTTON equ 1h
WS_VISIBLE equ 10000000h
WS_CHILD equ 40000000h
STYLBTN equ WS_CHILD+BS_DEFPUSHBUTTON+WS_VISIBLE
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_ARROW equ 32512
; режим показа окна - нормальный
SW_SHOWNORMAL equ 1
SW_HIDE equ 0
SW_SHOWMINIMIZED equ 2
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetMenuItemInfoA@16:NEAR
EXTERN LoadMenuA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
EXTERN TranslateAcceleratorA@12:NEAR
EXTERN LoadAcceleratorsA@8:NEAR
EXTERN GetMenu@4:NEAR
EXTERN DestroyMenu@4:NEAR
EXTERN SetMenu@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
;----структура класса окон
WNDCLASS STRUC
CLSSTYLE DD ?
CLWNDPROC DD ?
CLSCBCLSEX DD ?
CLSCBWNDEX DD ?
CLSHINST DD ?
CLSHICON DD ?
CLSHCURSOR DD ?
CLBKGROUND DD ?
CLMENNAME DD ?
CLNAME DD ?
WNDCLASS ENDS
MENINFO STRUCT
cbSize DD ?
fMask DD ?
fType DD ?
fState DD ?
wID DD ?
hSubMenu DD ?
hbmpChecked DD ?
hbmpUnchecked DD ?
dwItemData DD ?
dwTypeData DD ?
cch DD ?
MENINFO ENDS
; файл menu2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include menu2.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
SPACE DB 30 dup(32),0
MENI MENINFO <0>
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
HINST DD 0 ; дескриптор приложения
CLASSNAME DB 'CLASS32',0
CPBUT DB 'Кнопка',0 ; выход
CLSBUTN DB 'BUTTON',0
HWNDBTN DD 0
CAP DB 'Сообщение',0
MES DB 'Конец работы программы',0
MEN DB 'MENUP',0
MENC DB 'MENUC',0
ACC DD ?
HMENU DD ?
PRIZN DD ?
BUFER DB 100 DUP(0)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; инициализировать счетчик
MOV PRIZN, 2
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
MOV [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
MOV [WC.CLWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCBCLSEX], 0
MOV [WC.CLSCBWNDEX], 0
MOV EAX, [HINST]
MOV [WC.CLSHINST], EAX
; ----------иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
; ----------курсор окна
PUSH IDC_ARROW
PUSH 0
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR], EAX
; ——————----
MOV [WC.CLBKGROUND], 17 ; цвет окна
MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN
MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
PUSH OFFSET WC
CALL RegisterClassA@4
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH 400 ; DY - высота окна
PUSH 400 ; DX - ширина окна
PUSH 100 ; Y
PUSH 100 ; X
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET SPACE ; имя окна
PUSH OFFSET CLASSNAME ; имя класса
PUSH 0
CALL CreateWindowExA@48
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX ; дескриптор окна
; определить идентификатор меню
PUSH EAX
CALL GetMenu@4
MOV HMENU, EAX
; загрузить акселераторы
PUSH OFFSET MEN
PUSH [HINST]
CALL LoadAcceleratorsA@8
MOV ACC, EAX
; --———————————————————-
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8 ; показать созданное окно
; -------------------------
PUSH [NEWHWND]
CALL UpdateWindow@4 ; команда перерисовать видимую
; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP EAX, 0
JE END_LOOP
PUSH OFFSET MSG
PUSH [ACC]
PUSH [NEWHWND]
CALL TranslateAcceleratorA@12
CMP EAX, 0
JNE MSG_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess@4
_ERR:
JMP END_LOOP
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
; сообщение WM_DESTROY - при закрытии окна
CMP DWORD PTR [EBP+0CH], WM__DESTROY
JE WMDESTROY
; сообщение WM CREATE - при создании окна
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
; сообщение WM COMMAND - при событиях с элементами на окне
CMP DWORD PTR [EBP+0CH], WM_COMMAND
JE WMCOMMND
; сообщение WM_MENUSELECT - события, связанные с меню
CMP DWORD PTR [EBP+0CH], WM_MENUSELECT
JE WMMENUSELECT
; остальные события возвращаем обратно
JMP DEFWNDPROC
WMMENUSELECT:
; пропускаем первое сообщение при обращении к меню
CMP WORD PTR [EBP+14Н],0
JE FINISH
; проверяем, что активизировано - пункт меню
;или заголовок выпадающего меню
MOV EDX, 0
TEST WORD PTR [EBP+12H],MF_POPUP
SETNE DL
; заполнение структуры для вызова функции
; GetMenuItemInfo
MOVZX EAX,WORD PTR [EBP+10H] ; идентификатор
MOV MENI.cbSize,48
MOV MENI.fMask, MIIM_TYPE
MOV MENI.fType, MF_STRING
MOV EBX, DWORD PTR [EBP+14H]
MOV MENI.hSubMenu, EBX
MOV MENI.dwTypeData, OFFSET BUFER
MOV MENI.cch, 100
; получить информацию о выбранном пункте меню
PUSH OFFSET MENI
PUSH EDX
PUSH EAX
PUSH DWORD PTR [EBP+14H]
CALL GetMenuItemInfoA@16
; проверить результат выполнения функции
CMP EAX, 0
JE FINISH
; вывести название пункта меню
PUSH MENI.dwTypeData
PUSH 0
PUSH WM_SETTEXT
PUSH DWORD PTR [EBP+0BH]
CALL SendMessageA@16
MOV EAX, 0
JMP FINISH
WMCOMMND:
MOV EAX, HWNDBTN
; проверить, не нажата ли кнопка
CMP DWORD PTR [EBP+14Н], EAX
JE YES_BUT
; проверить, не выбран ли пункт меню MENUC - Выход
CMP WORD PTR [EBP+10Н],5
JE WMDESTROY
; проверить, не выбран ли пункт меню с идентификатором 5
CMP WORD PTR [EBP+10Н], 4
JNE L00
JMP YES_BUT
L00:
MOV EAX, 0
JMP FINISH
YES_BUT:
; здесь обработка нажатия кнопки
; вначале стереть надпись в заголовке
PUSH OFFSET SPACE
PUSH 0
PUSH WM_SETTEXT
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; проверить загружено или нет меню
CMP PRIZN, 0
JE L1
CMP PRIZN, 1
JE L2
; загрузить меню MENC
PUSH OFFSET MENC
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
MOV HMENU, EAX
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
; установить признак
MOV PRIZN,0
MOV EAX,0
JMP FINISH
L2:
; загрузить меню MENUP
PUSH OFFSET MEN
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
MOV HMENU, EAX
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
; установить признак
MOV PRIZN, 2
MOV EAX,0
JMP FINISH
L1:
; удалить меню
PUSH HMENU
CALL DestroyMenu@4
; обновить содержимое окна
PUSH SW_SHOWMINIMIZED
PUSH DWORD PTR [EBP+08H]
CALL ShowWindow@8
PUSH SW_SHOWNORMAL
PUSH DWORD PTR [EBP+08H]
CALL ShowWindow@8
MOV PRIZN,1
MOV EAX, 0
JMP FINISH
WMCREATE:
; создать окно-кнопку
PUSH 0
PUSH [HINST]
PUSH 0
PUSH DWORD PTR [EBP+08H]
PUSH 20 ; DY
PUSH 60 ; DX
PUSH 10 ; Y
PUSH 10 ; X
PUSH STYLBTN
; имя окна (надпись на кнопке)
PUSH OFFSET CPBUT
PUSH OFFSET CLSBUTN ; имя класса
PUSH 0
CALL CreateWindowExA@48
MOV HWNDBTN, EAX ; запомнить дескриптор кнопки
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA@16
JMP FINISH
WMDESTROY:
PUSH 0 ; MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES
PUSH DWORD PTR [EBP+08H] ; дескриптор окна
CALL MessageBoxA@16
PUSH 0
CALL PostQuitMessage@4 ; сообщение WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.1. Пример манипуляции с меню.

Программа на Рис. 2.4.1 имеет ряд механизмов, к обсуждению которых я намерен сейчас приступить. Для начала замечу, что в программе используются три ресурса: два меню и таблица акселераторов (см. файл ресурсов).

1. Первое, на что хочу обратить Ваше внимание, - это переменная PRIZN. В ней хранится состояние меню: 2 - загружено меню MENUP, 1 - меню отсутствует, 0 - загружено меню MENUC. Начальное состояние обеспечивается заданием меню при регистрации класса окна:

MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN

2. Второе - это кнопка. Механизм распознавания нажатия кнопки мы уже разбирали, так что больше на этом останавливаться не будем. Одно из событий, которое может произойти при нажатии кнопки - это удаление меню. Удаляется меню при помощи функции DestroyMenu. После удаления необходимо обновить содержимое окна. Это достигается последовательностью двух команд ShowWindow.

3. Еще одно событие, которое происходит при нажатии кнопки, это смена меню. Интересно, что смена меню происходит автоматически, если мы загрузим и установим новое меню.

4. Выбор одного из пунктов меню MENUP также приводит к смене меню. Здесь должно быть все понятно, поскольку обращение идет к тому же участку программы, что и в случае нажатия кнопки.

5. Интересная ситуация возникает с акселератором. Акселераторная клавиша у нас F5. При ее нажатии генерируется такое же сообщение, как при выборе пункта "Четвертый" меню MENUP. Важно то, что такое же сообщение будет генерироваться и тогда, когда загружается меню MENUC и когда меню не будет. А поскольку наша процедура обрабатывает сообщение в любом случае, клавиша F5 будет срабатывать всегда.

6. Рассмотрим теперь то, как производится определение названия выбранного пункта меню. Центральную роль в этом механизме играет сообщение WM_MENUSELECT. Это сообщение приходит всегда, когда выбирается пункт меню. Тут важно отметить, что когда мы активизируем меню, то в начале приходит сообщение WM_MENUSELECT со значением LPARAM, которое определяет идентификатор меню равным нулю. Этим целям служат строки:

CMP WORD PTR [EBP+14H], 0
JE FINISH

7. По получении сообщения WM_MENUSELECT в младшем слове параметра WPARAM может содержаться либо идентификатор пункта меню, либо номер заголовка выпадающего меню. Это ключевой момент. Нам важно это знать, так как строка заголовка выпадающего меню и строка пункта меню получаются по-разному. Определить, что выбрано, можно по старшему слову WPARAM. Мы используем для этого константу MF_POPUP: TEST WORD PTR [EBP+12H], MF_POPUP. Обратите внимание, как удобна и как кстати здесь команда SETNE.

8. Далее, для получения строки-названия используется функция GetMenuItemInfo. Третьим параметром этой функции как раз и может быть либо ноль, либо единица. Если ноль, то второй параметр - это идентификатор пункта меню, если единица, то второй параметр - номер заголовка выпадающего меню. Четвертым параметром является указатель на структуру, которая и будет заполняться в результате выполнения функции. Некоторые поля этой структуры должны быть, однако, заполнены заранее. Обращаю внимание на поле dwTypeData, которое должно содержать указатель на буфер, получающий необходимую нам строку. При этом поле cch должно содержать длину этого буфера. Но для того чтобы поля dwTypeData и cch трактовались функцией именно как указатель на буфер и его длину, поля fMask и fType должны быть правильно заполнены (см. программу). Наконец, поле cbSize должно содержать длину всей структуры.

9. После получения нужной информации, т.е. строки-названия пункта меню, при помощи функции SendMessage мы посылаем сообщение WM_SETTEXT, которое дает команду установить заголовок окна.

II

Итак, продолжим рассматривать ресурсы. Хочется рассказать о весьма интересном приеме, который можно использовать при работе с окнами редактирования. Наверное, Вы работали с визуальными языками типа Visual Basic, Delphi и пр. и обратили внимание, что окна редактирования можно так запрограммировать, а точнее, задать их свойства, что они позволят вводить только вполне определенные символы. В Delphi это свойство называется EditMask. Я думаю. Вам хотелось бы понять, как подобное реализовать только API-средствами. Но обо всем по порядку.

Обычное окно при нажатии клавиши (если в нем находится фокус) получает сообщения WM_KEYDOWN, WM_KEYUP и их квинтэссенцию WM_CHAR. Но в данном случае мы имеем дело не с обычным окном, а с диалоговым. Диалоговое окно таких сообщений не получает. Остается надеяться на сообщения, посылаемые на события, происходящие с самим элементом "окном редактирования". Но, увы, и здесь нас ждут разочарования. Данный элемент получает лишь два сообщения из тех, которые нас хоть как-то могут заинтересовать. Это сообщение EN_UPDATE и сообщение EN_CHANGE. Оба сообщения приходят, когда уже произведено изменение в окне редактирования. Но сообщение EN_UPDATE приходит, когда изменения на экране еще не произведены, а EN_CHANGE - после таких изменений. Нам придется сначала получить содержимое окна редактирования, определить, какой символ туда поступил последним, и если он недопустим, удалить его из строки и послать строку в окно снова. Добавьте сюда еще проблему, связанную с положением курсора и вторичным приходом сообщения EN_UPDATE. Лично я по такому пути бы не пошел.

Есть другой более изящный и короткий путь: использовать понятие горячей клавиши (HOTKEY). Мы ограничимся лишь программными свойствами горячих клавиш, то есть свойствами, которые необходимо знать программисту, чтобы использовать горячие клавиши в своих программах.

Горячая клавиша может быть определена для любой виртуальной клавиши, клавиши, определяемой через макроконстанты с префиксом VK. Для обычных алфавитно-цифровых клавиш значение этих констант просто совпадает с кодами ASCII. Возможны также сочетания с управляющими клавишами Alt, Control, Shift. После того как для данного окна определена горячая клавиша, при ее нажатии на функцию окна приходит сообщение WM_HOTKEY. По параметрам можно определить, какая именно горячая клавиша была нажата. Существенно, что понятие горячей клавиши глобально, т.е. она будет срабатывать, если будут активны другие окна и даже окна других приложений. Это требует от программиста весьма аккуратной работы, так как вы можете заблокировать нормальную работу других приложений. Т.е. необходимо отслеживать, когда данное окно активно, а когда нет. Этому весьма могут помочь сообщения WM_ACTIVATE и WM_ACTIVATEAPP. Первое сообщение всегда приходит на функцию окна тогда, когда окно активизируется или деактивизируется. Первый раз сообщение приходит при создании окна. Вот при получении этого сообщения и есть резон зарегистрировать горячие клавиши. Второе сообщение всегда приходит на функцию окна, когда окно теряет "фокус" - активизируется другое окно. Соответственно, при получении этого сообщения и следует отменить регистрацию этих клавиш.

Для работы с горячими клавишами используют в основном две функции: RegisterHotKey и UnregisterHotKey. Функция RegisterHotKey имеет следующие параметры:
первый - дескриптор окна; второй - идентификатор горячей клавиши; третий - модификатор, определяющий, не нажата ли управляющая клавиша; четвертый - виртуальный код клавиши.

Функция UnregisterHotKey имеет всего два параметра:
первый - дескриптор окна; второй - идентификатор.

Важно то, что если мы определили горячую клавишу, она перестает участвовать в каких-либо событиях, фактически оказывается заблокированной. Единственный метод, с помощью которого можно судить о нажатии этой клавиши - сообщение WM_HOTKEY.

Рассмотрим простой пример диалогового окна, на котором расположены два окна редактирования и кнопка выхода. Поставим перед собой такую цель. Первое окно редактирования должно пропускать только цифры от 0 до 9. Во второе окно можно вводить все символы. Выше рассматривался возможный механизм использования горячих клавиш с сообщениями WM_ACTIVATE и WM_ ACTIVATEAPP. Ясно, что эти события в данном случае нам ничем не помогут. Здесь дело тоньше, надо использовать сообщения, относящиеся к одному окну редактирования. Это сообщения EN_SETFOCUS и EN_KILLFOCUS, передаваемые, естественно, через сообщение WM_COMMAND. Ниже представлена программа, демонстрирующая этот механизм, и комментарий к ней. Сообщение EN_SETFOCUS говорит о том, что окно редактирования приобрело фокус (стало активным), а сообщение EN_KILLFOCUS - что окно редактирования потеряло фокус.

// файл dial1.rc
// определение констант
// стили окна
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT 0x0000L
// стиль всех элементов на окне
#define WS_CHILD 0x40000000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE 0x10000000L
// бордюр вокруг элемента
#define WS_BORDER 0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP 0x00010000L
// прижать строку к левому краю отведенного поля
#define SS_LEFT 0x00000000L
// стиль кнопка
#define BS_PUSHBUTTON 0x00000000L
// центрировать текст на кнопке
#define BS_CENTER 0x00000300L
#define DS_LOCALEDIT 0x20L
// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
// поле редактирования, идентификатор 1
CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
| WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 20, 128, 12
// еще одно поле редактирования, идентификатор 2
CONTROL "", 2, "edit", ES_LEFT | WS_CHILD
| WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 52, 127, 12
// текст, идентификатор 3
CONTROL "Строка 1", 3, "static", SS_LEFT
| WS_CHILD | WS_VISIBLE, 164, 22, 60, 8
// еще текст, идентификатор 4
CONTROL "Строка 2", 4, "static", SS_LEFT
| WS_CHILD | WS_VISIBLE, 163, 54, 60, 8
// кнопка, идентификатор 5
CONTROL "Выход", 5, "button", BS_PUSHBUTTON
| BS_CENTER | WS_CHILD | WS_VISlBLE | WS_TABSTOP,
180, 76, 50, 14
}
;файл dial1.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_COMMAND equ 111h
WM_SETTEXT equ 0Ch
WM_HOTKEY equ 312h
EN_SETFOCUS equ 100h
EN_KILLFOCUS equ 200h
; прототипы внешних процедур
EXTERN UnregisterHotKey@8:NEAR
EXTERN RegisterHotKey@16:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN GetDlgItem@8:NEAR
EXTERN MessageBoxA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DWORD ?
MSMESSAGE DWORD ?
MSWPARAM DWORD ?
MSLPARAM DWORD ?
MSTIME DWORD ?
MSPT DWORD ?
MSGSTRUCT ENDS
;файл dial.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial1.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------
; сегмент данных DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
STR1 DB "Неправильный символ !",0
STR2 DB "Ошибка !",0
; таблица для создания горячих клавиш
TAB DB 32,33,34,35,36,37,38,39,40
DB 41,42,43,44,45,46,47,58,59,60
DB 61,62,63,64,65,66,67,68,69,70
DB 71,72,73,74,75,76,77,78,79,80
DB 81,82,83,84,85,86,87,88,89,90
DB 91,92,93,94,95,96,97,98,99,100
DB 101,102,103,104,105,106,107,108,109,110
DB 111,112,113,114,115,116,117,118,119,120
DB 121,122,123,124,125,126,127,128,129,130
DB 131,132,133,134,135,136,137,138,139,140
DB 141,142,143,144,145,146,147,148,149,150
DB 151,152,153,154,155,156,157,158,159,160
DB 161,162,163,164,165,166,167,168,169,170
DB 171,172,173,174,175,176,177,178,179,180
DB 181,182,183,184,185,186,187,188,189,190
DB 191,192,193,194,195,196,197,198,199,200
DB 201,202,203,204,205,206,207,208,209,210
DB 211,212,213,214,215,216,217,218,219,220
DB 221,222,223,224,225,226,227,228,229,230
DB 231,232,233,234,235,236,237,238,239,240
DB 241,242,243,244,245,246,247,248,249,250
DB 251,252,253,254,255
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
;----------------------------------------
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
KOL:
;----------------------------------------
PUSH 0 CALL
ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
;----------------------------------------
CMP DWORD PTR [EBP+0CH], WM_CLOSE
JNE L1
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
MOV EAX, 1
JMP FIN
L1:
CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
JNE L2
; здесь заполнить окна редактирования, если надо
;
;
MOV EAX, 1
JMP FIN
L2:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JNE L5
; кнопка выхода ?
CMP WORD PTR [EBP+10H], 5
JNE L3
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
MOV EAX, 1
JMP FIN
L3:
CMP WORD PTR [EBP+10H], 1
JNE FINISH
; блок обработки сообщений первого окна редактирования
CMP WORD PTR [EBP+12H], EN_KILLFOCUS
JNE L4
; окно редактирования с идентификатором 1 теряет фокус
MOV EBX, 0
; снимаем все горячие клавиши
L33:
MOVZX EAX,BYTE PTR [ТАВ+EBX]
PUSH EAX
PUSH DWORD PTR [EBP+08Н]
CALL UnregisterHotKey@8
INC EBX
CMP EBX, 214
JNE L33
MOV EAX, 1
JMP FIN
L4:
CMP WORD PTR [EBP+12H],EN_SETFOCUS
JNE FINISH
; окно редактирования с идентификатором 1 получает фокус
MOV EBX, 0
; устанавливаем горячие клавиши
L44:
MOVZX EAX,BYTE PTR [ТАВ+EBX]
PUSH EAX
PUSH 0
PUSH EAX
PUSH DWORD PTR [EBP+08Н]
CALL RegisterHotKey@16
INC EBX
CMP EBX, 214
JNE L44
MOV EAX, 1
JMP FIN
L5:
CMP DWORD PTR [EBP+0CH],WM_HOTKEY
JNE FINISH
; здесь реакция на неправильно введенный символ
PUSH 0 ; МВ_ОК
PUSH OFFSET STR2
PUSH OFFSET STR1
PUSH DWORD PTR [EBP+08Н] ; дескриптор окна
CALL MessageBoxA@16
FINISH:
MOV EAX, 0
FIN:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.4.2. Пример использования горячих клавиш с диалоговым окном.

Комментарий к программе на Рис. 2.4.2.

1. Самое главное: разберитесь с тем, как мы определяем, когда первое окно редактирования теряет, когда приобретает фокус. В начале определяется, что сообщение пришло от окна редактирования с идентификатором 1, а затем - какое сообщение пришло: EN_SETFOCUS или EN_KILLFOCUS. В первом случае мы устанавливаем горячие клавиши, а во втором снимаем горячие клавиши.

2. В области данных задаем таблицу горячих клавиш. Функция RegisterHotKey имеет следующие параметры:
1-й параметр - идентификатор окна. 2-й параметр - идентификатор горячей клавиши. 3-й параметр - флаг нажатия управляющих клавиш. 4-й параметр - виртуальный код клавиши.

В нашем случае виртуальный код клавиши и идентификатор горячей клавиши совпадают. Конечно, здесь есть поле для дальнейшего усовершенствования. Скажем, исключить из обработки клавиши управления курсором. Я думаю, читатель справится с этим самостоятельно.

III

Данный раздел посвящен диалоговому окну с двумя списками. Двойным щелчком по элементу левого списка заполняется правый список. При этом мы учитываем возможность повторного щелчка по одному и тому же элементу. В принципе, в программе нет ничего сложного. Ниже будет дан комментарий к ней. Но мне хотелось бы немного поговорить о таком элементе, как список, остановившись на некоторых важных моментах.

Средства управления списком можно разделить на сообщения и свойства32. Свойства задаются в файле ресурсов. Например, свойство LBS_SORT приводит к тому, что содержимое списка будет автоматически сортироваться при добавлении туда элемента. Очень важным является свойство LBS_WANTKEYBOARDINPUT. При наличии такого свойства приложение получает сообщение WM_VKEYTOITEM, которое посылается приложению, когда нажимается какая-либо клавиша при наличии фокуса на данном списке. Вы можете выбрать самостоятельную обработку - клавиша PgUp, или оставить стандартную обработку. В том случае, если стандартная обработка не нужна, следует возвратить из функции диалогового окна отрицательное значение.


32 Впрочем, это можно сказать о любых элементах на диалоговом окне. Не правда ли, это весьма похоже на методы и свойства в объектном программировании. Но мы то с Вами знаем, что если углубиться еще дальше, то мы обнаружим, что значительная часть свойств опять сведется к обработке сообщений (см. комментарий к программе на Рис. 2.4.3).


// файл diallst.rc
// определение констант
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_VISIBLE 0x10000000L
#define WS_TABSTOP 0x00010000L
#define WS_VSCROLL 0x00200000L
#define WS_THICKFRAME 0x00040000L
#define LBS_NOTIFY 0x0001L
#define LBS_SORT 0x0002L
#define LBS_WANTKEYBOARDINPUT 0x0400L
// идентификаторы
#define LIST1 101
#define LIST2 102
#define IDI_ICON1 3
// определили иконку
IDI_ICON1 ICON "ico1.ico"
// определение диалогового окна
DIAL1 DIALOG 0, 0, 210, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
CONTROL "ListBox1",LIST1, "listbox", WS_VISIBLE |
WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY |
LBS_WANTKEYBOARDINPUT,
16, 16, 70, 75
CONTROL "ListBox2", LIST2, "listbox", WS_VISIBLE |
WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY | LBS_SORT,
116, 16, 70, 75
}
; файл diallst.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_SETICON equ 80h
WM_COMMAND equ 111h
WM_VKEYTOITEM equ 2Eh
LB_ADDSTRING equ 180h
LBN_DBLCLK equ 2
LB_GETCURSEL equ 188h
LB_GETTEXT equ 189h
LB_FINDSTRING equ 18Fh
VK_INSERT equ 2Dh
; прототипы внешних процедур
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN SendDlgItemMessageA@20:NEAR
EXTERN MessageBoxA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
; файл diallst.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
BUFER DB 100 DUP (0)
STR1 DB "Первый",0
STR2 DB "Второй",0
STR3 DB "Третий",0
STR4 DB "Четвертый",0
STR5 DB "Пятый ",0
STR6 DB "Шестой",0
STR7 DB "Седьмой",0
STR8 DB "Восьмой",0
STR9 DB "Девятый",0
STR10 DB "Десятый",0
STR11 DB "Одиннадцатый",0
STR12 DB "Двенадцатый",0
STR13 DB "Тринадцатый",0
STR14 DB "Четырнадцатый",0
STR15 DB "Пятнадцатый",0
INDEX DD OFFSET STR1
DD OFFSET STR2
DD OFFSET STR3
DD OFFSET STR4
DD OFFSET STR5
DD OFFSET STR6
DD OFFSET STR7
DD OFFSET STR8
DD OFFSET STR9
DD OFFSET STR10
DD OFFSET STR11
DD OFFSET STR12
DD OFFSET STR13
DD OFFSET STR14
DD OFFSET STR15
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
;----------------------------------------
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
; сообщение об ошибке
KOL:
;----------------------------------------
PUSH 0
CALL ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP, ESP
PUSH EBX
PUSH ESI
PUSH EDI
;----------------------------------------
CMP DWORD PTR [EBP+0CH],WM_CLOSE
JNE L1
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
JMP FINISH
L1:
CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
JNE L2
; загрузить иконку
PUSH 3 ; идентификатор иконки
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
; установить иконку
PUSH EAX
PUSH 0 ; тип иконки (маленькая)
PUSH WM_SETICON
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; заполнить левый список
MOV ECX, 15
MOV ESI, 0
L01:
PUSH ECX ; сохранить параметр цикла
PUSH INDEX[ESI]
PUSH 0
PUSH LB_ADDSTRING
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
ADD ESI, 4
POP ECX
LOOP L01
JMP FINISH
L2:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JNE L3
; не сообщение ли от левого списка?
CMP WORD PTR [EBP+10Н],101
JNE FINISH
; не было ли двойного щелчка?
CMP WORD PTR [EBP+12H], LBN_DBLCLK
JNE FINISH
; был двойной щелчок, теперь определим элемент
; получить индекс выбранного элемента
L4:
PUSH 0
PUSH 0
PUSH LB_GETCURSEL
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
; скопировать элемент списка в буфер
PUSH OFFSET BUFER
PUSH EAX ; индекс записи
PUSH LB_GETTEXT
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
; определить, нет ли элемента в правом списке
PUSH OFFSET BUFER
PUSH -1 ; искать во всем списке
PUSH LB_FINDSTRING
PUSH 102
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
CMP EAX,-1
JNE FINISH ; элемент нашли
; не нашли, можно добавлять
PUSH OFFSET BUFER
PUSH 0
PUSH LB_ADDSTRING
PUSH 102
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
MOV EAX,-1
JMP FIN
L3:
; здесь проверка, не нажата ли клавиша
CMP DWORD PTR [EBP+0CH],WM_VKEYTOITEM
JNE FINISH
CMP WORD PTR [EBP+10H],VK_INSERT
JE L4
MOV EAX,-1
JMP FIN
FINISH:
MOV EAX, 0
FIN:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.3. Пример программы с двумя списками. Перебросить запись из левого списка в правый можно двойным щелчком мыши или клавишей INSERT.

Комментарий к программе на Рис. 2.4.3.

    В первую очередь обратите внимание на функцию SendDIgItemMessage. Для посылки сообщения элементам диалогового окна эта функция более удобна, чем SendMessage, так как элемент в ней идентифицируется не дескриптором (который еще надо узнать), а номером, определенным в файле ресурсов. Взглянув на файл ресурсов. Вы увидите, что второму (правому) списку присвоено свойство LBS_SORT. Если такое свойство присвоено списку, то при добавлении в него элемента (сообщение LB_ADDSTRING) этот элемент помещается в список так, что список остается упорядоченным. Свойство LBS_SORT стоит системе Windows довольно большой работы. Посредством сообщения WM_COMPAREITEM она определяет нужное положение нового элемента в списке, а затем вставляет его при помощи сообщения LB_INSERTSTRING. Хотелось бы также обратить внимание на цикл заполнения левого списка. Нам приходится хранить регистр ECX в стеке. Вы скажете - дело обыкновенное при организации цикла при помощи команды LOOP. А я Вам скажу, что это совсем не очевидно. К сожалению, в документации по функциям API и сообщениям не указывается, какие регистры микропроцессора сохраняются, а какие нет. Все это придется устанавливать экспериментально. Что и было сделано мною в данном примере. Сообщение WM_VKEYTOITEM приходит при нажатии какой-либо клавиши, при наличии фокуса на списке. При этом список должен иметь свойство LBS_WANTKEYBOARDINPUT. Именно потому, что данное свойство установлено только у левого списка, у нас нет необходимости проверять, от какого списка пришло сообщение.


Глава 5. Управление файлами

Файл - важнейшая образующая практически любой программы. С появлением скоростных дисков больших объемов значение файлов сильно возросло. Использование API-функций управления файлами может сделать вашу программу более эффективной и производительной. Большинство программ данной главы являются консольными, потому что консольная программа как никакая другая подходит для демонстрации файловой обработки.

I

32-битная FAT. Характеристики файлов

Давая описание характеристикам файлов, я буду основываться на характеристиках, которыми манипулируют функции API. О типах и структуре файловых систем речь пойдет далее.

Атрибут файла. Размер - DWORD.
FILE_ATTRIBUTE_READONLY equ 1h
Атрибут - "только чтение". Приложения могут лишь читать данный файл. Соответственно, попытка открыть файл для записи вызовет ошибку.
FILE_ATTRIBUTE_HIDDEN equ 2h
Атрибут - "скрытый файл". "Невиден" при обычном просмотре каталога (см. ниже, поиск файлов).
FILE_ATTRIBUTE_SYSTEM equ 4h
Атрибут - "системный файл". Говорит о том, что данный файл принадлежит операционной системе.
FILE_ATTRIBUTE_DIRECTORY equ 10h
Атрибут - "директорий", С файлами с таким атрибутом операционная система обращается особым образом, считая его каталогом, т.е. считая его списком файлов, состоящим из записей по 32 байта.
FILE_ATTRIBUTE_ARCHIVE equ 20h
Со времен MS DOS таким атрибутом отмечались файлы, над которыми не произведена операция BACKUP или XCOPY. Для целей программирования данный атрибут эквивалентен нулевому значению атрибута.
FILE_ATTRIBUTE_NORMAL equ 80h
Данный атрибут означает, что у файла не установлены другие атрибуты.
FILE_ATTRIBUTE_TEMPORARY equ 100h
Атрибут означает, что данный файл предназначен для временного хранения. После закрытия файла система должна его удалить.
FILE_ATTRIBUTE_COMPRESSED equ 800h
Для файла это означает, что он сжат системой; для директория - что вновь создаваемый файл по умолчанию должен быть сжат.
FILE_ATTRIBUTE_OFFLINE equ 1000h
Данный атрибут означает, что данные файла не доступны в данный момент.

Смену атрибута можно осуществить функцией SetFileAttributes, получить значение атрибута функцией GetFileAttributes. Следует заметить, что если операционная система не накладывает никаких ограничений на возможности изменения атрибутов файлов, то фактически обесценивается смысл самих атрибутов - всегда можно снять атрибут "только чтение" и делать с файлом, что заблагорассудится.

Файл имеет три временные характеристики: время создания, время последней модификации, время последнего доступа. Время отсчитывается в наносекундных интервалах начиная с 12.00 по полудни 1 января 1600 года и хранится в двух 32-битных величинах. Надо сказать, что время хранится в так называемых универсальных координатах и должно еще быть преобразовано в локальное время (функция FileTimeToLocalFileTime). Получить значение всех трех времен можно функцией GetFileTime.

Длина файла в байтах хранится обычно в двух 32-битных величинах либо в одной 64-битной величине. Если 32-битные величины обозначить как l1 (младшая часть) и l2 (старшая часть), то 64-битная величина выразится формулой l2*0FFFFH+l1. Paзмер файла можно получить функцией GetFileSize.

Кроме указанных характеристик, файл, разумеется, имеет имя. При этом мы будем различать длинное и короткое имя. Точно также будем различать полный путь (со всеми длинными именами) и укороченный путь (все длинные имена заменены укороченными). Необходимость использования укороченного имени и пути диктуется, прежде всего, тем, что некоторые программы получают путь или имя на стандартный вход и трактуют пробелы как разделители для параметров. Преобразование длинного имени в короткое можно осуществить функцией GetShortPathName, которая работает и для имени, и для пути. Обратное преобразование можно осуществить функиией GetFullPathName.

В данной книге мы не рассматриваем вопроса о прямом доступе к диску. Но вопрос о структуре записей каталога может у читателя все же возникнуть. Это и понятно, ведь с переходом к FAT3233, во-первых, появилась возможность хранения файлов с длинным именем, во-вторых, у файла, кроме времени и даты модификации, появились еще время и дата создания и доступа. Где же все это хранится?

Для того чтобы ответить на поставленный вопрос, вспомним, что каталог в файловых системах FAT34 делится на записи длиной 32 байта. Ниже приводится структура записи для FAT32. Пустыми записями считаются записи, содержащие нулевые байты, либо начинающиеся с кода E5H (для удаленных записей). На файл с обычным именем (восемь байт на имя и 3-на расширение) отводится 32 байта. В байте со смещением +11 содержится атрибут файла. Если атрибут файла равен 0FH, то система считает, что здесь содержится длинное имя. Длинное имя кодируется в UNICODE и записывается в обратном порядке. За одной или несколькими записями с длинным именем должна следовать запись с обычным именем, содержащим знак "~" - тильда. Здесь содержится также остальная информация о файле. Как видите, алгоритм просмотра каталога с выявлением информации о файле весьма прост. Обратимся теперь к структуре записи каталога. В старой операционной системе MS DOS байты с 12 по 21 никак не использовались системой (см. [1]). Новой системе они пригодились. Ниже в таблице дана новая структура записи каталога.

СмещениеРазмерСодержимое
(+0) 8Имя файла или каталога, выровненное на левую границу и дополненное пробелами.
(+8) 3Расширение имени файла, выровненное на левую границу и дополненное пробелами.
(+11)1Атрибут файла.
(+12)2Время доступа.
(+14)2Время создания.
(+16)2Дата создания.
(+18)2Дата доступа.
(+20)2Два старших байта номера первого кластера файла.
(+22)2Время модификации файла.
(+24)2Дата модификации файла.
(+26)2Два младших байта номера первого кластера файла.
(+28)4Размер файла в байтах.

Как видите, все байты 32-байтной записи каталога теперь заняты. Лишний раз убеждаешься в первоначальной непродуманности файловой системы MS DOS, Это касается, в частности, длины файла. Как можно заметить, на длину файла отводится всего 4 байта. А как найти длину файла, если на нее требуется более 4 байт? Разумеется, в этом случае следует считать, что в каталоге хранятся младшие байты длины, а полную длину легко определить, обратившись к таблице размещения файлов. Но, согласитесь, что это уже явная недоработка. Странно также выглядит функция GetFileSize, которая возвращает четыре младших байта длины файла, старшие же байты возвращаются во втором параметре функции.

Иное дело в файловой системе NTFS, поддерживаемой Windows NT, изначально планируемой для работы с файлами больших размеров. Здесь для индексации кластеров используются 64-битные поля.


33 В начале Windows 95 работала с 16-битной FAT, но длинные имена уже поддерживала.

34 FAT (File Allocation Table) - один из элементов, на котором базируются файловые системы MS DOS и Windows 9х. По этой причине часто такие файловые системы называют FAT системами.


II

Поиск файлов. Для поиска файлов в Windows существуют две функции FindFirstFile и FindNextFile, очень похожие на аналогичные функции MS DOS и, как и там, работающие в паре. При успешном поиске первая функция возвращает некое число или идентификатор, который затем используется второй функцией для продолжения поиска.

Первым параметром функции FindFirstFile является указатель на строку для поиска файлов, второй параметр - указатель на структуру, которая получает информацию о найденных файлах. Функция FindNextFile первым своим параметром имеет идентификатор, полученный первой функцией, а вторым параметром - указатель на структуру. Эту структуру можно изучить по программе на Рис. 2.5.1.

Основным отличием этих функций от соответствующих функций MS DOS является то, что поиск ограничивается только маской поиска (*.*, *.ЕХЕ и т.п.). Если файл найден, то тогда по возвращаемой структуре, где содержится вся информация о нем, Вы уже можете решать, подходит файл или нет.

На Рис. 2.5.1 представлена программа, осуществляющая поиск файлов в указанном каталоге. Программа может иметь один или два параметра, или не иметь их вовсе. Если имеются два параметра, то первый параметр трактуется как каталог для поиска, причем программа учитывает, есть ли на конце косая черта или нет (допустимо c:, c:\, c:\windows\, c:\windows\system и т.п.). Второй параметр (в программе он третий, так как первым считается командная строка), если он есть, представляет собой маску поиска. Если его нет, то маска поиска берется в виде "*.*". Наконец, если параметров нет вообще, то поиск осуществляется в текущем каталоге по маске "*.*". Эту программу легко развить и сделать из нее полезную утилиту. Предоставляю это Вам, дорогой читатель. Ниже будет дан комментарий к означенной программе.

; файл FILES.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN lstrcatA@8:NEAR
EXTERN FindFirstFileA@8:NEAR
EXTERN FindNextFileA@8:NEAR
EXTERN FindClose@4:NEAR
;-----------------------------
; структура, используемая для поиска файла
; при помощи функций FindFirstFile и FindNextFile
_FIND STRUC
; атрибут файла
ATR DWORD ?
; время создания файла
CRTIME DWORD ?
DWORD ?
; время доступа к файлу
ACTIME DWORD ?
DWORD ?
; время модификации файла
WRTIME DWORD ?
DWORD ?
; размер файла
SIZEH DWORD ?
SIZEL DWORD ?
; резерв
DWORD ?
DWORD ?
; длинное имя файла
NAM DB 260 DUP (0)
; короткое имя файла
ANAM DB 14 DUP(0)
_FIND ENDS
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
BUF DB 0
DB 100 dup(0)
LENS DWORD ? ; количество выведенных символов
HANDL DWORD ?
HANDL1 DWORD ?
MASKA DB "*.*",0
АР DB "\",0
FIN _FIND <0>
TEXT DB "Для продолжения нажмите клавишу ENTER",13,10,0
BUFIN DB 10 DUP(0)
FINDH DWORD ?
NUM DB 0
NUMF DWORD 0 ; счетчик файлов
NUMD DWORD 0 ; счетчик каталогов
FORM DB "Число найденных файлов: %lu",0
FORM1 DB "Число найденных каталогов: %lu",0
BUFER DB 100 DUP (?)
DIR DB "<DIR>",0
PAR DB 0 ; количество параметров
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; преобразовать строки для вывода
PUSH OFFSET TEXT
PUSH OFFSET TEXT
CALL CharToOemA@8
PUSH OFFSET FORM
PUSH OFFSET FORM CALL
CharToOemA@8
PUSH OFFSET FORM1
PUSH OFFSET FORM1
CALL CharToOemA@8
; получить количество параметров
CALL NUMPAR
MOV PAR,EAX
; если параметр один, то искать в текущем каталоге
CMP EAX, 1
JE NO_PAR
;-------------------------------------------------
; получить параметр номером EDI
MOV EDI, 2
LEA EBX,BUF
CALL GETPAR
PUSH OFFSET BUF
CALL LENSTR
; если в конце нет "\" - добавим
CMP BYTE PTR [BUF+EBX-1],"\"
JE NO_PAR
PUSH OFFSET AP
PUSH OFFSET BUF
CALL lstrcatA@8
; нет ли еще параметра, где задана маска поиска
CMP PAR,3
JB NO_PAR
; получить параметр - маску поиска
MOV EDI,3
LEA EBX,MASKA
CALL GETPAR
NO_PAR:
;-------------------------------------------------
CALL FIND
; вывести количество файлов
PUSH NUMF
PUSH OFFSET FORM
PUSH OFFSET BUFER
CALL wsprintfA
LEA EAX, BUFER
MOV EDX,1
CALL WRITE
; вывести количество каталогов
PUSH NUMD
PUSH OFFSET FORM1
PUSH OFFSET BUFER
CALL wsprintfA
LEA EAX, BUFER
MOV EDX, 1
CALL WRITE
_END:
PUSH 0
CALL ExitProcess@4
;************************
; область процедур
;************************
; вывести строку (в конце перевод строки)
; EAX - на начало строки
; EDX - с переводом строки или без
WRITE PROC
; получить длину параметра
PUSH EAX
CALL LENSTR
MOV ESI,EAX
CMP EDX,1
JNE NO_ENT
; в конце - перевод строки
MOV BYTE PTR [EBX+ESI],13
MOV BYTE PTR [EBX+ESI+1],10
MOV BYTE PTR [EBX+ESI+2],0
ADD EBX,2
NO_ENT:
; вывод строки
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH EAX
PUSH HANDL
CALL WriteConsoleA@20
RET
WRITE ENDP
; процедура определения длины строки
; строка - [EBP+08Н]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
;----------------------
CLD
MOV EDI, DWORD PTR [EBP+08Н]
MOV EBX, EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ;-длина строки, включая 0
MOV EBX, EDI
DEC EBX
;----------------------
POP EAX
POP EBP
RET 4
LENSTR ENDP
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX, 0
JMP L2
L3:
OR EDX,1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI, EAX ; указатель на строку
XOR ECX, ECX ; счетчик
MOV EDX, 1 ; признак
L1:
CMP BYTE PTR [ESI], 0
JE L4
CMP BYTE PTR [ESI], 32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL,BYTE PTR [ESI]
MOV BYTE PTR [EBX],AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX], 0
RET
GETPAR ENDP
; поиск в каталоге файлов и их вывод
; имя каталога в BUF
FIND PROC
; путь с маской
PUSH OFFSET MASKA
PUSH OFFSET BUF
CALL lstrcatA@8
; здесь начало поиска
PUSH OFFSET FIN
PUSH OFFSET BUF
CALL FindFirstFileA@8
CMP EAX,-1
JE _ERR
; сохранить дескриптор поиска
MOV FINDH,EAX
LF:
; исключить "файлы" "." и ".."
CMP BYTE PTR FIN.NAM,"."
JE _NO
; не каталог ли?
TEST BYTE PTR FIN.ATR,10H
JE NO_DIR
PUSH OFFSET DIR
PUSH OFFSET FIN.NAM
CALL lstrcatA@8
INC NUMD
DEC NUMF
NO_DIR:
; преобразовать строку
PUSH OFFSET FIN.NAM
PUSH OFFSET FIN.NAM
CALL CharToOemA@8
; здесь вывод результата
LEA EAX, FIN.NAM
MOV EDX,1
CALL WRITE
; увеличить счетчики
INC NUMF
INC NUM
; конец страницы?
CMP NUM, 22
JNE _NO
MOV NUM, 0
; ждать ввод строки
MOV EDX,0
LEA EAX, TEXT
CALL WRITE
PUSH 0
PUSH OFFSET LENS
PUSH 10
PUSH OFFSET BUFIN
PUSH HANDL1
CALL ReadConsoleA@20
_NO:
; продолжение поиска
PUSH OFFSET FIN
PUSH FINDH
CALL FindNextFileA@8
CMP EAX,0
JNE LF
; закрыть поиск
PUSH FINDH
CALL FindClose@4
_ERR:
RET
FIND ENDP
_TEXT ENDS
END START

Рис. 2.5.1 Пример простой программы, которая осуществляет поиск файлов и выводит их название на экран.

Программа на Рис. 2.5.1 довольно проста. Из нового здесь Вы обнаружите лишь то, как обращаться с функциями FindFirstFile и FindNextFile. Процедуры, которые используются для работы с параметрами командной строки, Вы уже встречали ранее. Вывод информации осуществляется в текущую консоль, с чем Вы тоже знакомы. Для получения дескриптора консоли используется функция GetStdHandle. Процедура WRITE позволила несколько упростить те участки программы, которые отвечают за вывод информации на экран. Ранее я обещал, что мы не обойдем вниманием строковые API-функции. В данной программе это обещание выполнено, и наряду со строковыми процедурами "собственного изготовления" используется строковая функция lstrcat, которая осуществляет сложение (конкатенацию) строк. По поводу параметра в командной строке замечу, что при наличии в имени каталога пробела Вам придется задавать имя в укороченном виде. Так, например, вместо C:\Program Files придется написать C:\Progra~1. Это должно быть понятно - пробелы отделяют параметры. Чтобы корректно решать проблему, необходимо ввести специальный разделитель для параметров, например "-" или "/".

Данная программа осуществляет поиск в указанном или текущем каталоге. Если бы программа была написана на языке высокого уровня, например Си, ее легко можно было бы видоизменить так, чтобы она осуществляла поиск по дереву каталогов. Собственно, небольшая модификация потребовалась бы только для процедуры FIND, которая должна была бы вызываться рекурсивно. Можно видеть, что эта легкость произрастает из наличия в языках высокого уровня такого элемента, как локальная переменная. Попробуем осуществить это, основываясь на материале Главы 1.2. А можно осуществить это без использования локальных переменных?

Программа на Рис. 2.5.2 немного похожа на предыдущую программу. Но поиск она осуществляет по дереву каталогов, начиная с заданного каталога. Эта программа - одна из самых сложных в книге, поэтому советую читателю скрупулезно в ней разобраться. Может быть, Вам удастся ее усовершенствовать. Я могу дать и направление, в котором возможно такое усовершенствование. Дело в том, что вторым параметром командной строки можно указать маску поиска. Если, например, указать маску "*.ЕХЕ", по этой маске будет осуществляться поиск не только файлов, но и каталогов. Этот недостаток и следовало бы устранить в первую очередь.

Поиск по дереву каталогов оптимально производить рекурсивным образом, однако для этого необходимы локальные переменные35. Смысл использования локальной переменной в рекурсивном алгоритме заключается в том, что часть данных должна сохраняться при возврате из процедуры.

В данной программе я, ради простоты, отказался от процедуры LENSTR и использую функцию API lstrlen. Кроме того, я усовершенствовал вывод так, чтобы на экран выводилось полное имя файла.


35 Конечно, можно обойтись и без них, храня данные, например, в глобальном массиве, обращаясь к той или иной области массива в зависимости от уровня рекурсии.


; файл FILES.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN lstrcatA@8:NEAR
EXTERN lstrcpyA@8:NEAR
EXTERN lstrlenA@4:NEAR
EXTERN FindFirstFileA@8:NEAR
EXTERN FindNextFileA@8:NEAR
EXTERN FindClose@4:NEAR
;----------------------------
; структура, используемая для поиска файла
; при помощи функций FindFirstFile и FindNextFile
_FIND STRUC
; атрибут файла
ATR DWORD ?
; время создания файла
CRTIME DWORD ?
DWORD ?
; время доступа к файлу
ACTIME DWORD ?
DWORD ?
; время модификации файла
WRTIME DWORD ?
DWORD ?
; размер файла
SIZEH DWORD ?
SIZEL DWORD ?
; резерв
DWORD ?
DWORD ?
; длинное имя файла
NAM DB 260 DUP (0)
; короткое имя файла
ANAM DB 14 DUP (0)
_FIND ENDS
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
BUF DB 0
DB 100 dup (0)
LENS DWORD ? ; количество выведенных символов
HANDL DWORD ?
HANDL1 DWORD ?
MASKA DB "*.*"
DB 50 DUP (0)
AP DB "\",0
FIN _FIND <0>
TEXT DB "Нажмите клавишу ENTER",13, 10, 0
BUFIN DB 10 DUP (0) ; буфер ввода
NUM DB 0
NUMF DWORD 0 ; счетчик файлов
NUMD DWORD 0 ; счетчик каталогов
FORM DB "Число найденных файлов: %lu",0
FORM1 DB "Число найденных каталогов: %lu",0
DIRN DB " <DIR>",0
PAR DWORD 0
PRIZN DB 0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; преобразовать строки для вывода
PUSH OFFSET TEXT
PUSH OFFSET TEXT
CALL CharToOemA@8
PUSH OFFSET FORM
PUSH OFFSET FORM
CALL CharToOemA@8
PUSH OFFSET FORM1
PUSH OFFSET FORM1
CALL CharToOemA@8
; получить количество параметров
CALL NUMPAR
MOV PAR,EAX
; если параметр один, то искать в текущем каталоге
CMP EAX, 1
JE NO_PAR
;----------------------------------
; получить параметр номером EDI
MOV EDI,2
LEA EBX,BUF
CALL GETPAR
CMP PAR,3
JB NO_PAR
; получить параметр - маску поиска
MOV EDI,3
LEA EBX, MASKA
CALL GETPAR
NO_PAR:
;----------------------------------
PUSH OFFSET BUF
CALL FIND
; вывести количество файлов
PUSH NUMF
PUSH OFFSET FORM
PUSH OFFSET BUF
CALL wsprintfA
LEA EAX, BUF
MOV EDX,1
CALL WRITE
;+++++++++++++++++
; вывести количество каталогов
PUSH NUMD
PUSH OFFSET FORM1
PUSH OFFSET BUF
CALL wsprintfA
LEA EAX, BUF
MOV EDX, 1
CALL WRITE
_END:
PUSH 0
CALL ExitProcess@4
; область процедур
;*************************************
; вывести строку (в конце перевод строки)
; EAX - на начало строки
; EDX - с переводом строки или без
WRITE PROC
; получить длину параметра
PUSH EAX
PUSH EAX
CALL lstrlenA@4
MOV ESI,EAX
POP EBX
CMP EDX, 1
JNE NO_ENT
; в конце - перевод строки
MOV BYTE PTR [EBX+ESI],13
MOV BYTE PTR [EBX+ESI+1],10
MOV BYTE PTR [EBX+ESI+2],0
ADD EAX,2
NO_ENT:
; вывод строки
PUSH 0
PUSH OFFSET LENS
PUSH EAX
PUSH EBX
PUSH HANDL
CALL WriteConsoleA@20
RET
WRITE ENDP
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX, 1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL, BYTE PTR [ESI]
MOV BYTE PTR [EBX], AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX], 0
RET
GETPAR ENDP
;-----------------------------------
; поиск в каталоге файлов и их вывод
; локальные переменные
FINDH EQU [EBP-4] ; дескриптор поиска
DIRS EQU [EBP-304] ; полное имя файла
DIRSS EQU [EBP-604] ; для хранения каталога
DIRV EQU [EBP-904] ; для временного хранения
DIR EQU [EBP+8] ; параметр - имя каталога
FIND PROC
PUSH EBP
MOV EBP,ESP
SUB ESP,904
; инициализация локальных переменных
MOV ECX,300
MOV AL,0
MOV EDI,0
CLR:
MOV BYTE PTR DIRS+[EDI],AL
MOV BYTE PTR DIRSS+[EDI],AL
MOV BYTE PTR DIRV+[EDI],AL
INC EDI
LOOP CLR
; определить длину пути
PUSH DIR
CALL lstrlenA@4
MOV EBX,EAX
MOV EDI, DIR
CMP BYTE PTR [EDI],0
JE _OK
;если в конце нет "\" - добавим
CMP BYTE PTR [EDI+EBX-1],"\"
JE _OK
PUSH OFFSET AP
PUSH DWORD PTR DIR
CALL lstrcatA@8
_OK:
; запомним каталог
PUSH DWORD PTR DIR
LEA EAX,DIRSS
PUSH EAX
CALL lstrcpyA@8
; путь с маской
PUSH OFFSET MASKA
PUSH DWORD PTR DIR
CALL lstrcatA@8
; здесь начало поиска
PUSH OFFSET FIN
PUSH DWORD PTR DIR
CALL FindFirstFileA@8
CMP EAX,-1
JE _ERR
; сохранить дескриптор поиска
MOV FINDH,EAX
LF:
; исключить "файлы" "." и ".."
CMP BYTE PTR FIN.NAM,"."
JE _FF
;---------------------
LEA EAX,DIRSS
PUSH EAX
LEA EAX,DIRS
PUSH EAX
CALL lstrcpyA@8
;---------------------
PUSH OFFSET FIN.NAM
LEA EAX, DIRS
PUSH EAX
CALL lstrcatA@8
; не каталог ли?
TEST BYTE PTR FIN.ATR, 10H
JE NO_DIR
; добавить в строку <DIR>
PUSH OFFSET DIRN
LEA EAX, DIRS
PUSH EAX
CALL lstrcatA@8
; увеличим счетчики
INC NUMD
DEC NUMF
; установим признак каталога
MOV PRIZN,1
; вывести имя каталога
LEA EAX, DIRS
PUSH EAX
CALL OUTF
JMP _NO
NO_DIR:
; вывести имя файла
LEA EAX, DIRS
PUSH EAX
CALL OUTF
; признак файла (не каталога)
MOV PRIZN,0
_NO:
CMP PRIZN,0
JZ _F
; каталог, готовимся в рекурсивному вызову
LEA EAX,DIRSS
PUSH EAX
LEA EAX, DIRV
PUSH EAX
CALL lstrcpyA@8
PUSH OFFSET FIN.NAM
LEA EAX,DIRV
PUSH EAX
CALL lstrcatA@8
; осуществляем вызов
LEA EAX, DIRV
PUSH EAX
CALL FIND
; продолжение поиска
_F:
INC NUMF
_FF:
PUSH OFFSET FIN
PUSH DWORD PTR FINDH
CALL FindNextFileA@8
CMP EAX,0
JNE LF
; закрыть дескриптор поиска
PUSH DWORD PTR FINDH
CALL FindClose@4
_ERR:
MOV ESP, EBP
POP EBP
RET 4
FIND ENDP
;----------------------------------
; страничный вывод имен найденных файлов
STRN EQU [EBP+8]
OUTF PROC
PUSH EBP
MOV EBP,ESP
; преобразовать строку
PUSH DWORD PTR STRN
PUSH DWORD PTR STRN
CALL CharToOemA@8
; здесь вывод результата
MOV EAX, STRN
MOV EDX, 1
CALL WRITE
INC NUM
; конец страницы?
CMP NUM, 22
JNE NO
MOV NUM, 0
; ждать ввод строки
MOV EDX, 0
LEA EAX, TEXT
CALL WRITE
PUSH 0
PUSH OFFSET LENS
PUSH 10
PUSH OFFSET BUFIN
PUSH HANDL1
CALL ReadConsoleA@20
NO:
POP EBP
RET 4
OUTF ENDP
_TEXT ENDS
END START

Рис. 2.5.2. Пример программы, которая осуществляет рекурсивный поиск по дереву каталогов.

Разберем ту роль, которую играют локальные переменные в процедуре FIND. Переменная FINDH - здесь хранится дескриптор поиска в данном каталоге. Рекурсивный вызов процедуры FIND может происходить и тогда, когда поиск в текущем каталоге еще не закончился. Следовательно, после возврата из рекурсии поиск должен быть продолжен. Это можно обеспечить только старым значением дескриптора. Локальная переменная обеспечивает такую возможность, поскольку она разрушается только при переходе на более низкий уровень (к родительскому каталогу).

Аналогичную роль играет переменная DIRSS. В ней хранится текущий каталог. Это важно, т.к. с помощью этой переменной формируется полное имя файла.

Переменные DIRS и DIRV играют вспомогательную роль. В принципе, вместо них можно было бы использовать и глобальные переменные. Хотя, с точки зрения эффективности рекурсивных алгоритмов, чем меньше объем локальных переменных - тем лучше.

Еще один вопрос я хочу здесь обсудить. Для передачи имени каталога при вызове процедуры используется переменная DIRV. Почему же для этой цели нельзя использовать переменную DIRSS? Причина вот в чем. В процедуру передается не само значение, а указатель (адрес). Следовательно, любые изменения с параметром DIR приведет к аналогичным изменениям с переменной DIRSS на нижнем уровне рекурсии, В чем мы, разумеется, не заинтересованы.

Трансляция программы в TASM. Основная проблема при трансляции программ на Рис. 2.5.1 и Рис. 2.5.2 возникнет с локальными метками. Локальная метка - это метка, которая действует в пределах некоторого блока программы. В нашем случае таким блоком программы является процедура. Транслятор MASM автоматически различает метки, находящиеся в пределах процедуры, и считает их локальными. Поэтому не возникает проблемы, когда в разных процедурах встречаются метки с одинаковым именем. В TASM несколько иной подход: по умолчанию метки считаются глобальными. Локальные метки должны иметь перед именем обозначение "@@". Кроме того, в начале программы следует поставить директиву LOCALS. Сделав нужные метки локальными и поставив директиву LOCALS, Вы без труда, уже известными действиями, приведете программу к виду, приемлемому для TASM. Не забудьте о преобразовании wsprintfA -> _wsprintfA.

III

Приемы работы с двоичными файлами*. Манипуляция внешними файлами36 основывается на нескольких функциях API, главной и наиболее сложной из которых является функция CreateFile.

В связи с ограниченностью объема книги, мы не можем подробно остановиться на свойствах функции CreateFile. Однако замечу, что с помощью этой функции можно не только создавать или открывать файл, но и такие объекты как каналы (PIPE), консоли, устройства жесткого диска (disk device), коммуникационный ресурс и др. Функция различает устройство по структуре имени. К примеру, "C:\config.sys" определяет файл, a "CONOUT$" - буфер вывода текущей консоли.

Сейчас я представлю две простые, но весьма важные программы (Рис. 2.5.3(1) и Рис. 2.5.3(2)). Обе программы выводят содержимое текстового файла37, имя которого указано в командной строке, в текущую консоль. В первом случае мы получаем дескриптор текущей консоли стандартным способом. Во втором случае - открываем консоль как файл и, соответственно, выводим туда информацию, как в файл. Хочу обратить Ваше внимание на роль буфера, в который читается содержимое файла. Поэкспериментируйте с размером буфера, взяв для пробы большой текстовый файл. Интересно, что в указанных программах никак не учитывается структура текстового файла. Для такого ввода-вывода это ни к чему. Ниже мы поговорим и о структуре текстового файла.


36 Имеется в виду файлами, расположенными на внешнем устройстве.

37 Точнее любого файла, но смысл выводить файл на консоль именно таким образом имеется только для текстового файла.

* Кажется, автор имеет ввиду текстовые файлы, а не бинарные, как это следует из дальнейшего повествования :)


;файл FILES1.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ equ 80000000h
GENERIC_WRITE equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING equ 3
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN ReadFile@20:NEAR
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HFILE DWORD ?
BUF DB 100 DUP (0)
BUFER DB 300 DUP (0)
NUMB DWORD ?
NUMW DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; получить количество параметров
CALL NUMPAR
CMP EAX,1
JE NO_PAR
;------------------------------
; получить параметр номером EDI
MOV EDI,2
LEA EBX, BUF
CALL GETPAR
; открыть файл
PUSH 0 ; должен быть равен 0
PUSH 0 ; атрибут файла (если создаем)
PUSH OPEN_EXISTING ; как открывать
PUSH 0 ; указатель на security attr
PUSH 0 ; режим общего доступа
PUSH GEN ; режим доступа
PUSH OFFSET BUF ; имя файла
CALL CreateFileA@28
CMP EAX,-1
JE NO_PAR
MOV HFILE, EAX
L00:
; прочесть в буфер
PUSH 0
PUSH OFFSET NUMB
PUSH 300
PUSH OFFSET BUFER
PUSH HFILE
CALL ReadFile@20
; вывести содержимое буфера на консоль
PUSH 0
PUSH OFFSET NUMW
PUSH NUMB
PUSH OFFSET BUFER
PUSH HANDL
CALL WriteConsoleA@20
; проверить, не последние ли байты прочитаны
CMP NUMB, 300
JE L00
; закрыть файл
PUSH HFILE
CALL CloseHandle@4
; конец работы программы
NO_PAR:
PUSH 0
CALL ExitProcess@4
; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->ЕАX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX.EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
INC ESI
JMP L1
L4:
MOV EAX, ECX
RET
NUMPAR ENDP
; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI], 0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL, BYTE PTR [ESI]
MOV BYTE PTR [EBX], AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX], 0
RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.5.3(1). Вывод на консоль содержимого текстового файла. Первый способ.

; файл FILES2.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ equ 80000000h
GENERIC_WRITE equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING equ 3
; прототипы внешних процедур
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN ReadFile@20:NEAR
EXTERN WriteFile@20:NEAR
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
; includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HFILE DWORD ?
BUF DB 100 DUP (0)
BUFER DB 300 DUP (0)
NUMB DWORD ?
NUMW DWORD ?
NAMEOUT DB "CONOUT$"
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода (консоли) как файла
PUSH 0
PUSH 0
PUSH OPEN_EXISTING
PUSH 0
PUSH 0
PUSH GEN
PUSH OFFSET NAMEOUT
CALL CreateFileA@28
MOV HANDL,EAX
; получить количество параметров
CALL NUMPAR
CMP EAX, 1
JE NO_PAR
;---------------------------------------------
; получить параметр номером EDI
MOV EDI,2
LEA EBX,BUF
CALL GETPAR
; открыть файл
PUSH 0
PUSH 0
PUSH OPEN_EXISTING
PUSH 0
PUSH 0
PUSH GEN
PUSH OFFSET BUF
CALL CreateFileA@28
CMP EAX,-1
JE NO_PAR
MOV HFILE,EAX
L00:
; прочесть в буфер
PUSH 0
PUSH OFFSET NUMB
PUSH 300
PUSH OFFSET BUFER
PUSH HFILE
CALL ReadFile@20
; вывести на консоль как в файл
PUSH 0
PUSH OFFSET NUMW
PUSH NUMB
PUSH OFFSET BUFER
PUSH HANDL
CALL WriteFile@20
CMP NUMB, 300
JE L00
; закрыть файл
PUSH HFILE
CALL CloseHandle@4
; конец работы программы
NO_PAR:
PUSH 0
CALL ExitProcess@4
; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL,BYTE PTR [ESI]
MOV BYTE PTR [EBX], AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX], 0
RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.5.3(2). Вывод на консоль содержимого текстового фаша. Второй способ.

Сейчас мы поговорим более подробно о структуре текстового файла. При работе с языками высокого уровня теряются определенные алгоритмические навыки. Это касается, в частности, и работы с текстовыми файлами. Ассемблер не дает расслабиться. Рассмотрим возможные варианты работы с текстовыми файлами.

Основным признаком текстового файла является то, что он состоит из строк разной длины. Строки отделены друг от друга разделителями. Чаще всего это последовательность двух кодов - 13 и 10. Возможны и другие варианты, например, некоторые DOS-редакторы отделяли строки только одним кодом 13.

Построчное чтение текстового файла можно осуществить четырьмя наиболее очевидными способами.

    Побайтное чтение из файла. Как только достигаем символа-разделителя, производим действие над считанной строкой и переходим к чтению следующей строки. При этом, разумеется, следует учесть, что на конце файла может не быть символа-разделителя. Если кто-то решит, что это слишком медленный способ, то замечу, что Windows неплохо кэширует диск, так что все выглядит не так уж плохо. Чтение в небольшой буфер, но так чтобы туда входила, по крайней мере, одна строка. Прочитав, находим в буфере конец строки и производим над ней какое-либо действие. Далее следует обратиться к файлу и передвинуть указатель так, чтобы он был в файле на начале следующей строки и, разумеется, повторить действие. Чтение в произвольный буфер. После чтения производится поиск всех строк, попавших в буфер, и совершение над ними действий. При этом с большой вероятностью должна возникнуть ситуация, когда одна строка неполностью умещается в буфере. Мы обязаны учесть такую возможность. Чтение в буфер, в который помещается весь файл. Это частный случай третьего подхода, и наиболее простой с точки зрения программирования.

В программе на Рис. 2.5.4 реализуется третий подход.

; файл FILES2.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ equ 80000000h
GENERIC_WRITE equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING equ 3
; прототипы внешних процедур
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN ReadFile@20:NEAR
EXTERN WriteFile@20:NEAR
EXTERN CharToOemA@8:NEAR
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ? ; дескриптор консоли
HFILE DWORD ? ; дескриптор файла
BUF DB 100 DUP (0) ; буфер для параметров
BUFER DB 1000 DUP (0) ; буфер для файла
NAMEOUT DB "CONOUT$"
INDS DD 0 ; номер символа в строке
INDB DD 0 ; номер символа в буфере
NUMB DD ?
NUMC DD ?
PRIZN DD 0
STROKA DB 300 DUP (0)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода (консоли) как файла
PUSH 0
PUSH 0
PUSH OPEN_EXISTING
PUSH 0
PUSH 0
PUSH GEN
PUSH OFFSET NAMEOUT
CALL CreateFileA@28
MOV HANDL,EAX
; получить количество параметров
CALL NUMPAR
CMP EAX, 1
JE NO_PAR
;---------------------------------------------------
; получить параметр номером EDI
MOV EDI,2
LEA EBX,BUF
CALL GET PAR
; открыть файл
PUSH 0
PUSH 0
PUSH OPEN_EXISTING
PUSH 0
PUSH 0
PUSH GEN
PUSH OFFSET BUF
CALL CreateFileA@28
CMP EAX,-1
JE NO_PAR
MOV HFILE, EAX
;++++++++++++++++++++++++++++
L00:
; читать 1000 байт
PUSH 0
PUSH OFFSET NUMB
PUSH 1000
PUSH OFFSET BUFER
PUSH HFILE
CALL ReadFile@20
MOV INDB, 0
; проверим, есть ли в буфере байты
CMP NUMB, 0
JZ _CLOSE
; заполняем строку
L001:
MOV EDI,INDS
MOV ESI,INDB
MOV AL,BYTE PTR BUFER[ESI]
CMP AL,13 ; проверка на конец строки
JE _ENDSTR
MOV BYTE PTR STROKA[EDI],AL
INC ESI
INC EDI
MOV INDS,EDI
MOV INDB,ESI
CMP NUMB, ESI ; проверка на конец буфера
JNBE L001
; закончился буфер
MOV INDS,EDI
MOV INDB,ESI
JMP L00
_ENDSTR:
; делаем что-то со строкой
CALL OUTST
; обнулить строку
MOV INDS,0
; перейти к следующей строке в буфере
ADD INDB,2
; не закончился ли буфер?
MOV ESI,INDB
CMP NUMB, ESI
JAE L001
JMP L00
;++++++++++++++++++++++++++++++
_CLOSE:
; проверим, не пустая ли строка
CMP INDS,0
JZ CONT
; делаем что-то со строкой
CALL OUTST
CONT:
; закрыть файлы
PUSH HFILE
CALL CloseHandle@4
; конец работы программы
NO_PAR:
PUSH 0
CALL ExitProcess@4
; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ; указатель на строку
XOR ECX,ECX ; счетчик
MOV EDX,1 ; признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ; номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL,BYTE PTR [ESI]
MOV BYTE PTR [EBX],AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX],0
RET
GETPAR ENDP
; вывести строку в консоль с разделителем
OUTST PROC
MOV EBX,INDS
MOV BYTE PTR STROKA[EBX],0
PUSH OFFSET STROKA
PUSH OFFSET STROKA
CALL CharToOemA@8
; в конце строки - разделитель
MOV BYTE PTR STROKA[EBX],6
INC INDS
; вывести строку
PUSH 0
PUSH OFFSET NUMC
PUSH INDS
PUSH OFFSET STROKA
PUSH HANDL
CALL WriteFile@20
RET
OUTST ENDP
_TEXT ENDS
END START

Рис. 2.5.4. Пример обработки текстового файла.

Программа на Рис. 2.5.4 демонстрирует один из возможных алгоритмов обработки текстового файла - построчное чтение текстового файла. Часть программы, занимающаяся чтением и анализом текстового файла, сосредоточена между метками L00 и CONT. Детально разберитесь в алгоритме и проникнитесь тем, что язык высокого уровня никогда не будет стимулировать написание таких алгоритмов, а значит, язык ассемблера делает нас интеллектуально богаче.



Глава 6. Макросредства ассемблера и программирование в Windows

I

Большая часть данной главы будет носить справочный характер. Почему я привожу справочный материал в середине книги, а не в ее начале? Просто я убежден, что справка в начале книги может отбить всякую охоту читать дальше. Многое из того, что я привожу в данной главе. Вы уже знаете и, следовательно, Вам интересно будет читать материал этой главы.

Метки

1. Метка с двоеточием после имени определяет адрес следующей за меткой команды.

2. Директива LABEL позволяет определить явно тип метки. Значение же определенной таким образом метки равно адресу команды или данных, стоящих далее. Например,
LABEL L1 DWORD.

3. Выражение ИМЯ PROC определяет метку, переход на которую обычно происходит по команде CALL. Блок кода, начинающийся с такой метки, называют процедурой. Впрочем, переход на такую метку можно осуществлять и с помощью JMP, как, впрочем, и команду CALL можно использовать для перехода на обычную метку. В этом, несомненно, состоит сила и гибкость ассемблера.

4. В строке за меткой может стоять директива резервирования данных, например: ERR DB 'Ошибка' или NUM DWORD 0. С точки зрения языка высокого уровня таким образом мы определяем глобальную переменную. С точки же зрения ассемблера нет никакой разницы между командой и данными, поэтому между меткой, определяющей команду, и меткой, определяющей данные, нет никакой разницы. Раз уж речь пошла о данных, перечислю их типы:
BYTE (DB) - байт,
WORD (DW) - 2 байта,
DWORD (DD) - 4 байта,
FWORD (DF) - 6 байт,
QWORD (DQ) - 8 байт,
TBYTE (DT) - 10 байт.

5. С помощью директивы EQU в терминах языков высокого уровня определяются константы. Например - MES EQU "ERROR!", LAB EQU 145Н. С помощью EQU значение данной метке может быть присвоено только один раз. С правой стороны от EQU может стоять выражение с использованием арифметических, логических и битовых операций. Вот эти операции: "+", "-", "*", "/", "MOD"-ocтaток от деления, "AND", "OR", "NOT", "XOR", "SHR", "SHL". Используются также операции сравнения: EQ, GE, GT, LE, LT, NE. Выражение с операцией сравнения считается логическим и принимает значение 0, если условие не выполняется, и 1 - если выполняется. С помощью директивы "=" можно присваивать только целые значения, но зато производить переприсваивание. Заметим, что выражение может являться операндом команды: MOV EAX,16*16-1.

6. Метка "$" всегда определяет текущий адрес.

7. В MASM метки, стоящие в процедуре, автоматически считаются локальными и, следовательно, имена меток в процедурах могут дублироваться. В TASM все метки по умолчанию считаются глобальными. Чтобы сделать метки, стоящие в процедуре локальными, они должны иметь префикс @@, а в начале программы следует указать директиву LOCALS (см. предыдущую главу).

Структура

1. Директива STRUC позволяет объединить несколько разнородных данных в одно целое. Эти данные называются полями. Вначале при помощи STRUC определяется шаблон структуры, затем с помощью директивы < > можно определить любое количество структур. Рассмотрим пример:

STRUC COMPLEX
RE DD ?
IM DD ?
STRUC ENDS
...
;в сегменте данных
COMP1 COMPLEX <?>
COMP2 COMPLEX <?>

Доступ к полям структуры осуществляется посредством точки: COMP1.RE.

2. Объединение. Объединение определяется при помощи ключевого слова UNION. От структуры объединение отличается только тем, что все поля располагаются в структуре с нулевым смещением, т.е. накладываются друг на друга.

Условное ассемблирование

1. Условное ассемблирование дает возможность при трансляции обходить тот или иной участок программы. Существует три вида условного ассемблирования.

а)
IF выражение
...
ENDIF
б)
IF выражение
...
ELSE
...
ENDIF
в)
IF выражение 1
...
ELSEIF выражение 2
...
ELSEIF выражение 3
...
ELSE
...
ENDIF
Условие считается не выполненным, если выражение принимает значение 0 и выполненным, если выражение отлично от нуля.

2. Ассемблеры MASM и TASM поддерживают также несколько условных специальных директив, назовем некоторые из них.

а)

IFE выражение
...
ELSEIFE
...
ENDIFE

б) Операторы IF1 и IF2 проверяют первый и второй проход при ассемблировании.

в) Оператор IFDEF - проверяет, определено ли в программе символическое имя, IFDEFN - обратный оператор. И другие IF операторы. Они имеются в любом справочнике по ассемблеру.

г) Имеется целый набор директив, начинающихся с .ERR. Например, .ERRE выражение - вызовет прекращение трансляции и сообщение об ошибке, если выражение станет равным 0.

Условное ассемблирование понадобится нам в конце главы для написания программы, транслируемой как в MASM, так и TASM.

Вызов процедур

1. С упрощенным вызовом процедур в MASM Вы уже познакомились. Это директива INVOKE. Процедура должна быть заранее определена с использованием ключевого слова PROTO. Например:

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
...
invoke MessageBox, h, ADDR TheMsg, ADDR TitleW, MB_OK
Здесь h - дескриптор окна, откуда вызывается сообщение, TheMsg - строка сообщения, TitleW - заголовок окна, MB_OK - тип сообщения. ADDR в данном случае синоним OFFSET.

2. Оказывается, в синтаксисе TASM тоже имеется свой упрощенный вызов.

EXTERN MESSAGEBOX:PROC
...
call MessageBox PASCAL,h,ADDR TheMsg,ADDR TitleW, MB_OK
PASCAL - тип вызова, точнее порядок следования параметров. Можно поставить параметр C, тогда порядок будет обратным.

Макроповторения

1. Повторение, заданное опеделенное число раз. Используется макродиректива REPT. Например:

A EQU 10
REPT 100
DB A
ENDM
Будет сгенерировано 100 директив DB 10. С этой директивой удобно использовать оператор "=", который позволяет изменять значение переменной многократно, т.е. использовать выражение типа А = А + 5.

2. Директива IRP.

IRP параметр,<список>
...
ENDM
Блок будет вызываться столько раз, сколько параметров в списке. Например:
IRP REG, <EAX,EBX,ECX,EDX,ESI,EDI>
PUSH REG
ENDM
Приведет к генерации следующих строк:
PUSH EAX
PUSH EBX
PUSH ECX
PUSH EDX
PUSH ESI
PUSH EDI

3. Директива IRPC.

IRPC параметр, строка
Операторы
ENDM

Пример:

IRPC CHAR,azklg
CMP AL,'&CHAR&'
JZ EndC
ENDM
EndC:

Данный фрагмент эквивалентен следующей последовательности:
CMP AL,'a'
JZ EndC
CMP AL,'z'
JZ EndC
CMP AL,'k'
JZ EndC
CMP AL,'l'
JZ EndC
CMP AL,'g'
JZ EndC
EndC:

Амперсант (&) в последнем примере используется для того, чтобы задать вычисление параметра блока повторения даже внутри кавычек. Амперсант - это макрооперация, которая работает в блоке повторения, поскольку блоки повторения представляют собой один из типов макрокоманды.

Макроопределения

Общий вид макроопределения.

Имя MACRO параетры
...
ENDM
Определив блок один раз, можно использовать его в программе многократно. Причем в зависимости от значений параметров заменяемый участок может иметь разные значения. Если заданный участок предполагается многократно использовать, например в цикле, макроопределение имеет несомненные преимущества перед процедурой, т.к. несколько убыстряет выполнение кода. Пример:
ЕХС MACRO par1,par2
PUSH par1
POP par2
ENDM
Данное макроопределение приводит к обмену содержимым между параметрами.

ЕХС EAX,EBX эквивалентно PUSH EAX\POP EAX; ЕХС MEM1,ESI - PUSH MEM1\POP ESI и т. д. Заметим, что если первый параметр будет непосредственно числом, то это приведет к загрузке данного числа во второй операнд.

Важным вопросом в связи с макроопределениями является проблема меток. Действительно, если мы будем применять в макроопределении обычные метки, то при использовании его более чем один раз возникнет коллизия. Коллизия эта разрешается при помощи объявления локальных меток. Для этого используется ключевое слово LOCAL. Например,

ЕХС MACRO par1,par2
LOCAL EXI
CMP par1,par2
JE EXI
PUSH par1
POP par2
EXI:
ENDM

Данное макроопределение можно использовать сколь угодно много раз - при каждой подстановке ассемблер будет генерировать уникальную метку. Для выхода из макроопределения (т.е. для прекращения генерации макроопределения) применяется директива EXITM. Она может понадобиться, если в макроопределении Вы используете условные конструкции типа IF..ENDIF.

Некоторые другие директивы транслятора ассемблера

1. Кроме объявлений с использованием директив PUBLIC и EXTERN, возможно объявление при помощи директивы GLOBAL, которая действует, как PUBLIC и EXTERN одновременно.

2. PURGE имя макроса. Отменяет загрузку макроса. Используется при работе с библиотекой макросов, чтобы не перегружать память38.

3. LENGTHOF - определяет число элементов данных. SIZEOF - определяет размер данных (отсутствуют в TASM).

4. Директивы задания набора команд.
.8086 - разрешены только команды микропроцессора 8086. Данная директива работает по умолчанию.
.186 - разрешены команды 186.
.286 и .286Р - разрешены команды 286-ого микропроцессора. Добавка "P" здесь и далее означает разрешение команд защищенного режима.
.386 и .386P - разрешение команд 386-ого микропроцессора.
.486 и .486Р - разрешение команд 486-ого процессора.
.586 и .586Р - разрешены команды Р5 (Pentium).
.686 и .686Р - разрешены команды Pб (Pentium Pro, Pentium II).
.8087 - разрешены команды арифметического сопроцессора 8087.
.287 - разрешены команды арифметического сопроцессора 287.
.387 - разрешены команды арифметического сопроцессора 387.
.MMX - разрешены команды расширения ММХ.

5. Директивы управления листингом.
NAME - задать имя модуля.
TITLE - определяет заголовок листинга.
По умолчанию и имя модуля, и заголовок листинга совпадают с именем файла.
SUBTTL - определяет подзаголовок листинга.
PAGE - определяет размеры страницы листинга: длина, ширина. Директива PAGE без аргументов начинает новую страницу листинга.
.LIST - выдавать листинг.
.XLIST - запретить выдачу листинга.
.SALL - подавить печать макроопределений.
.SFCOND - подвить печать условных блоков с ложными условиями.
.LFCOND - печатать условные блоки с ложными условиями.
.CREF - разрешить листинг перекрестных ссылок.
.XCREF - запретить листинг перекрестных ссылок.


38 В операционной системе MS DOS это было существенно.


Конструкции времени исполнения программы

1. Условные конструкции.

а)
.IF условие
...
.ENDIF
б)
.IF условие
...
.ELSE
...
.ENDIF
в)
.IF условие 1
...
.ELSEIF условие 2
...
.ELSEIF условие 3
...
...
.ELSE
...
.ENDIF

Рассмотрим следующий фрагмент, содержащий условную конструкцию и соответствующий ей ассемблерный код.

.IF EAX==12H
MOV EAX,10H
.ELSE
MOV EAX,15Н
.ENDIF
эквивалентен следующему ассемблерному коду:
CMP EAX,12Н
JNE NO_EQ
MOV EAX,10H
JMP EX_BLOK
NO_EQ:
MOV EAX,15Н
EX_BLOK:
Весьма удобная штука, но не увлекайтесь: на мой взгляд, это сильно расслабляет.

2. Цикл "пока".

.WHILE условие
...
.ENDW

Пример.

WHILE EAX<64H
ADD EAX,10
ENDW

Для MASM:

JMP L2
L1:
ADD EAX,10Н
L2:
CMP EAX,64Н
JB L1

Для TASM:

L1:
CМР EAX,64Н
JNB EXI
ADD EAX,10Н
JMP L1
EXI:

Есть некоторое отличие в том, как два ассемблера транслируют директивы .IF и .WHILE. Транслятор TASM32 производит автоматически оптимизацию на предмет выравнивания по границе учетверенного слова, добавляя дополнительно команды NOP. Это несколько ускоряет выполнение программы, но увеличивает ее объем. Мне ближе позиция MASM.

II

Сейчас мы рассмотрим вопрос о написании программ, которые одинаково транслировались бы и в MASM, и в TASM. Для этого прекрасно подходит условное ассемблирование. Удобнее всего использовать IFDEF и возможности трансляторов задавать символьную константу, все равно - TASM или MASM. И в ML, и в TASM32 определен ключ /D, позволяющий задавать такую константу.

На Рис. 2.6.1 представлена программа, транслируемая и в MASM, и TASM. Программа весьма проста, но рассмотрения ее вполне достаточно для создания более сложных подобных совместимых программ.

.386P
; плоская модель
.MODEL FLAT, STDCALL
; проверить, определена символьная константа MASM или нет
IFDEF MASM
; работаем в MASM
EXTERN ExitProcess@4:NEAR
EXTERN MessageBoxA@16:NEAR
includelib с:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\user32.lib
ELSE
; работаем в TASM
EXTERN ExitProcess:NEAR
EXTERN MessageBoxA:NEAR
includelib c:\tasm32\lib\import32.lib
ExitProcess@4 = ExitProcess
MessageBoxA@16 = MessageBoxA
ENDIF
;-----------------------------------------------------------
;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG DB "Простая программа",0
TIT DB "Заголовок",0
_DATA ENDS
;сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
PUSH 0
PUSH OFFSET TIT
PUSH OFFSET MSG
PUSH 0 ; дескриптор экрана
CALL MessageBoxA@16
;-----------------------------------
PUSH 0
CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 2.6.1. Пример использования условного ассемблирования для написания совместимой программы.

Трансляция в MASM:

ML /с /coff /DMASM PROG.ASM
LINK /SUBSYSTEM:WINDOWS PROG.OBJ

Трансляция в TASM:

TASM32 /ml PROG.ASM
TLINK32 -aa PROG.OBJ

Как видите, все сводится к проверке, определена символьная константа MASM или нет (ключ /DMASM). Еще одна сложность - добавка в конце имени @N. Эту проблему мы обходим, используя оператор "=", с помощью которого переопределяем имена (см. секцию "работаем в TASM").