Assembler под Windows для чайников. Часть 23

В прошлый раз мы приступили к написанию простого графического редактора, который для начала должен хотя бы предоставлять возможность открывать графический файл для просмотра. Общую часть мы достаточно подробно изучили. Детально описали структуру файлового формата BMP. Теперь мы готовы приступить к текущей основе нашей программы – процедуре открытия файла.

Выбор пункта меню "Открыть" или нажатие комбинации "Ctrl+O" приводит к получению сообщения WM_COMMAND с идентификатором IDM_OPEN в младшем слове параметра [wparam]. Тогда будет выполнен код на метке ".OPEN" – вызов процедуры open_file. В прошлой части статьи данная процедура являлась "пустышкой" и не выполняла никаких действий. Сегодня мы наполним процедуру необходимым кодом:

proc open_file

mov [ofn.Flags], OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_EXPLORER
invoke GetOpenFileName,ofn
cmp eax,0
je .cancel
invoke CreateFile,fname,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
mov [hfile],eax ;сохраняем дескриптор открытого файла
invoke GetFileSize,[hfile],0
cmp eax,-1
je .failed
mov [FileSize],eax

cmp [pmem],0
jne .heap_realloc
;выделяем память
invoke GetProcessHeap
mov [hheap],eax
invoke HeapAlloc,[hheap],0,[FileSize]
mov [pmem],eax ;указатель на память, он же -
mov ebx,eax ;указатель на BITMAPFILEHEADER
jmp .read_file
.heap_realloc:
invoke HeapReAlloc,[hheap],0,[pmem],[FileSize]
mov [pmem],eax ;указатель на память, он же -
mov ebx,eax ;указатель на BITMAPFILEHEADER

.read_file:
invoke ReadFile,[hfile],[pmem],[FileSize],BytesRead,0
cmp eax,0
je .read_error
;проверим, все ли верно с картинкой
mov eax,[BytesRead] ;прочитано байт
mov cx,[ebx] ;первые два байта должны быть "BM", если это bmp-файл
mov edx,[ebx+2] ;размер файла, заявленный в заголовке

cmp eax,[FileSize]
jne .read_error
cmp eax,edx
jne .read_error
cmp cx,"BM"
jne .read_error

invoke CloseHandle,[hfile]
mov eax,sizeof.BITMAPFILEHEADER ;BITMAPINFO следует
add eax,ebx ;сразу за BITMAPFILEHEADER.
mov [pBMInfo],eax ;сохраним этот указатель.

sub eax,sizeof.BITMAPFILEHEADER.bfOffBits ;в eax указатель на BITMAPFILEHEADER.bfOffBits
add ebx,[eax] ; [pmem]+[BITMAPFILEHEADER.bfOffBits]=указатель на битовый массив картинки
mov [pBits],ebx

mov eax,sizeof.BITMAPINFOHEADER.biSize
add eax,[pBMInfo]
mov ebx,[eax]
mov [bWidth],ebx
add eax,sizeof.BITMAPINFOHEADER.biWidth
mov ebx,[eax]
mov [bHeight],ebx

invoke InvalidateRect,[hwnd],0,1
mov eax,0
ret

.read_error:
invoke HeapFree,[hheap],0,[pmem]
mov [pmem],0
.failed:
invoke CloseHandle,[hfile]
invoke MessageBox,0,0,0,0
.cancel:
ret

endp

Данную процедуру нельзя назвать простой, но сейчас мы разберем ее по винтикам, и все встанет на свои места. Вызов системного диалога открытия файла осуществляется через функцию GetOpenFileName. Все ее параметры содержатся в структуре OPENFILENAME, которая у нас числится под псевдонимом "ofn". Список элементов структуры OPENFILENAME вы можете посмотреть в ..FASMINCLUDEEQUATES COMDLG32.INC. За подробным описанием элементов отправляйтесь к 7-й статье данного цикла, либо ищите описание в MSDN. Элемент [ofn.Flags] мы заполняем непосредственно перед вызовом, ввиду того, что эту структуру в будущем мы будем использовать и для диалога сохранения файла, а значения данного элемента будут различны для открытия и для сохранения. Остальные значимые элементы структуры мы заполнили еще в секции данных. В частности, указали адрес размещения фильтров файловых расширений (filter), адрес размещения буфера под полный путь к открываемому файлу (fname) и его размер (MAXSIZE). Если открываемый файл будет выбран, то функция вернет отличное от ноля значение и поместит полный путь к файлу в предоставленный нами буфер. В случае отмены открытия функция вернет ноль, и нам останется лишь перейти к метке ".cancel" для возвращения из процедуры.

Когда файл для открытия выбран, мы приступаем к его чтению. Для этого вызываем CreateFile, указывая имя открываемого для чтения файла в первом параметре, и необходимые значения в остальных параметрах. Дескриптор успешно открытого файла [hfile] понадобится нам для получения размера файла, чтения данных из файла в память и закрытия доступа к файлу по завершении чтения. Функция GetFileSize вернет нам размер файла в байтах. А если ей это не удастся, то мы узнаем об этом по возвращенной минус единице и прыгнем на метку ".failed", где перед возвращением из процедуры закроем доступ к открытому ранее файлу и выведем сообщение "Ошибка".

Итак, размер файла мы получили и сохранили в [FileSize], теперь нам надо определиться, будем ли мы выделять блок памяти в куче, или он уже был выделен ранее, если это уже не первый файл, открываемый в нашем вьювере. Определить это можно по переменной [pmem], которая изначально содержит лишь ноль, но после первого выделения памяти будет хранить указатель на выделенный блок. Так что, если не ноль – прыгаем на ".heap_realloc", иначе выделяем память. Для выделения блока в куче следует получить дескриптор стандартной кучи нашего процесса: invoke GetProcessHeap. Функция HeapAlloc выделяет блок памяти в куче, дескриптор которой указан в первом параметре функции (hheap). Второй параметр может содержать дополнительные опции выделения или быть нолем. В третьем параметре необходимо указать размер выделяемого блока в байтах – у нас он равен размеру файла (FileSize). После успешного выполнения функция вернет в eax указатель на выделенный блок памяти. Указатель – это адрес первого байта выделенного блока. Он понадобится нам при чтении файла в этот блок, а также для освобождения памяти, когда она нам перестанет быть нужна. Заметьте, что при выходе из программы нам нет нужды освобождать выделенный блок памяти, так как вся стандартная куча процесса будет автоматически развеяна по ветру при завершении процесса. Еще мы сохраняем копию указателя в регистр ebx, чтобы позже, заключив ebx в квадратные скобки, можно было бы прочитать определенные байты, находящиеся по адресу, содержащемуся в ebx. С указателями всегда такая путаница: pmem без квадратных скобок – лишь константа, адрес ячейки, в которой хранится указатель на память (адрес выделенного блока); [pmem] – это уже содержимое ячейки, сам указатель. Но чтобы прочитать или записать что-либо в выделенную память, нам пришлось бы обращаться к [[pmem]] – память по адресу, который хранится по адресу pmem. Такое обращение процессор понять не может, поэтому приходится действовать через регистры. Помещаем в регистр содержимое [pmem], а затем уже берем сам регистр в квадратные скобки и обращаемся к памяти, которая находится по адресу, содержащемуся в регистре. В высокоуровневых языках такие моменты иногда выглядят проще, но программист не видит, сколько лишних процессорных команд может создать компилятор из этой простоты. Так что не пугаемся, не паникуем, а потихонечку усваиваем и идем дальше.

На метку ".heap_realloc" мы попадаем, если блок уже был выделен ранее, когда мы открывали первую картинку, а теперь – необходимо лишь изменить размер блока. Функция HeapReAlloc своими параметрами похожа на HeapAlloc, только размер блока указывается в четвертом параметре, а в третьем необходимо сообщить указатель на изменяемый блок. По завершении функции мы снова сохраняем указатель на измененный блок памяти, так как иногда функция может переместить блок в другое место, если его нужно увеличить настолько, что он не умещается на старом месте.

На метке ".read_file" мы можем приступать к чтению файла, то есть – копированию его содержимого в оперативную память. Функция ReadFile как раз для этого подойдет. В параметрах сообщаем дескриптор открытого файла - [hfile], указатель на буфер для получения данных - [pmem], количество байт для чтения - [pmem], указатель на переменную, в которую функция запишет количество успешно прочитанных байт – BytesRead, указатель на структуру OVERLAPPED для дополнительных сведений (нам он не нужен – указываем ноль).

При успешном выполнении функции чтения файла мы получим в EAX отличный от ноля результат, и пойдем дальше только в этом случае. Тем не менее, прежде чем приступать к отображению картинки в окне, следует проверить еще несколько моментов. При успешном чтении корректного BMP-файла размер файла [FileSize] должен совпадать с количеством прочитанных байт [BytesRead] и, конечно, с размером файла, указанном в элементе bfSize заголовка файла, который у нас к этому времени находится по адресу [pmem]+2 или [ebx+2]. Ну и не мешает убедиться в том, что первые два байта заголовка (элемент bfType) являются символами "BM", как положено в формате BMP. В случае, если будет обнаружено хоть одно несоответствие, мы отправляемся в конец процедуры, где, несолоно хлебавши, освобождаем память, закрываем файл и выводим сообщение об ошибке.

Если же чтение данных в память прошло безупречно, мы все равно закрываем файл, так как картинка уже в оперативке, и готовим изображение к выводу на экран. Для этого нам необходимо выяснить ширину и высоту изображения, вычислить адрес начала структуры BITMAPINFO и, естественно, адрес, по которому располагается собственно битовая карта. Вспоминаем структуры, изученные на прошлом занятии, и аккуратно отсчитываем байты.
Префикс "sizeof." перед именем структуры либо ее элемента позволяет получить размер структуры либо ее элемента соответственно. Адрес начала целого файла и его первой структуры – BITMAPFILEHEADER – до сих пор хранится в регистре EBX. Не будем пока его изменять, а вычислим указатель на BITMAPINFO в регистре EAX: [BITMAPINFO] = [BITMAPFILEHEADER] + sizeof.BITMAPFILEHEADER.

Для вычисления указателя на битовую карту нам понадобится значение элемента bfOffBits, в котором записано смещение битовой карты от начала файла. Регистр EAX сейчас указывает на BITMAPINFOHEADER, так что для того, чтобы он указывал на последний элемент предыдущей структуры (bfOffBits), нам достаточно вычесть из EAX размер этого элемента (sizeof.BITMAPFILEHEADER.bfOffBits). Теперь прибавим значение по адресу EAX к содержимому EBX, и EBX уже будет указывать как раз на битовую карту.

Ширина изображения хранится в элементе biWidth, который следует сразу после первого элемента структуры BITMAPINFOHEADER – biSize. Так что сложим в регистре EAX размер элемента biSize и адрес BITMAPINFO и получим указатель на biWidth, содержимое которого и сохраним в переменной [bWidth]. Прибавим к адресу в EAX размер элемента biWidth, чтобы он указывал на следующий элемент – biHeight, и сохраним значение элемента в переменной [bHeight].

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

.wmpaint:
invoke BeginPaint,[hwnd],BmpPS
mov [hDC],eax

cmp [pmem],0
je @f
invoke SetDIBitsToDevice,[hDC],0,0,[bWidth],[bHeight],0,0,0,[bHeight],[pBits],[pBMInfo],0 ;DIB_RGB_COLORS=0
@@:
invoke EndPaint,[hwnd],BmpPS
mov eax,0
jmp .finish

Функция SetDIBitsToDevice осуществляет отрисовку изображения на совместимом контексте графического устройства (DC); источником данных изображения должна являться аппаратно-независимая битовая карта (DIB). Параметры функции:

1 – дескриптор DC приемника;

2, 3 – координаты X и Y левого верхнего угла прямоугольника, в который будет передано изображение;

4, 5 – ширина и высота изображения;

6, 7 – координаты X и Y левого верхнего угла изображения;

8 – номер строки, с которой начинается вывод изображения;

9 – число строк в выводимом изображении;

10 – указатель на битовую карту;

11 – указатель на структуру BITMAPINFO;

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

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

Вы, наверное, заметили, что обычно в программах просмотра изображений картинка отображается посередине окна. Да, так она выглядит приятнее, но в нашем вьювере все рисунки и фотографии почему-то липнут к правому верхнему углу окна. Что ж, вот вам и домашнее задание для самостоятельной работы: подставьте вместо ноликов во втором и третьем параметре SetDIBitsToDevice 32-битные переменные [Xpos] и [Ypos]. Производите перерасчет этих координат перед отрисовкой нового изображения и после изменения размеров окна. Координата [Xpos] – текущая ширина окна, деленная пополам, минус ширина рисунка, также деленная пополам. Координата [Ypos] – то же самое, только с высотой.

Тщательно усваивайте пройденный материал, смело совершенствуйте программу, если возникнут вопросы – пишите в соответствующей теме форума, вам ответят!

Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт. Жду ваших вопросов на q@sa-sec.org.

BarMentaLisk SASecurity gr.


Компьютерная газета. Статья была опубликована в номере 10 за 2009 год в рубрике программирование

©1997-2024 Компьютерная газета