ЛАБОРАТОРНАЯ РАБОТА №5 РАЗРАБОТКА ПРИЛОЖЕНИЙ ПОД Win32 1. ЦЕЛЬ РАБОТЫ Цель данной работы - изучение приемов программирования приложений под Win32. 2. Простейшая программа под Win32 Под Windows написать приложение гораздо проще, чем для DOS благодаря большому количеству функций вызываемых из системных библиотек (свыше 2000). Например, для запуска другого приложения необходим лишь один вызов функции. Точно так же можно загружать не только программы, но и документы, графические и текстовые файлы и даже почтовые и WWW-адреса — все, для чего в реестре Windows записано действие, выполняющееся при попытке открытия. ; winurl.asm ; Пример програмы для win32. ; Запускает установленный по умолчанию браузер на адрес, указанный в строке URL ; аналогично можно запускать любую программу, документ, и любой другой файл, ; для которого определена операция open ; include shell32.inc include kernel32.inc .386 .model flat .const URL db 'http://www.lionking.org/~cubbi/',0 .code _start: ; метка точки входа должна начинаться с подчеркивания xor ebx,ebx push ebx ; для исполнимых файлов - способ показа push ebx ; рабочий каталог push ebx ; командная строка push offset URL ; имя файла с путем push ebx ; операция open или print (если NULL - open) push ebx ; идентификатор окна, которое получит сообщения call ShellExecute ; ShellExecute(NULL,NULL,url,NULL,NULL,NULL) push ebx ; код выхода call ExitProcess ; ExitProcess(0) end _start В этой программе выполняется вызов двух системных функций Win32 — ShellExecute() (открыть файл) и ExitProcess() (завершить процесс). Чтобы вызвать системную функцию Windows, программа должна поместить в стек все параметры от последнего к первому и передать управление дальней командой CALL. Все эти функции сами освобождают стек (завершаясь командой RET N) и возвращают результат работы в регистре ЕАХ. Такая договоренность о передаче параметров называется STDCALL. С одной стороны, это позволяет вызывать функции с нефиксированным числом параметров, а с другой — вызывающая сторона не должна заботиться об освобождении стека. Кроме того, функции Windows сохраняют значение регистров ЕВР, ESI, EDI и EBX, Поэтому 0 хранится в регистре EBX на всем протяжении кода.1-байтная команда PUSH EBX применяется вместо 2-байтной PUSH 0. В файлы kernel32.inc и shell32.inc помещаются директивы, описывающие вызываемые системные функции (из kernel32.dll и shell32.dll): ; kernel32.inc ; включаемый файл с определениями функций из kernel32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ExitProcess:near else includelib kernel32.lib ; истинные имена используемых функций extrn __imp__ExitProcess@4:dword ; присваивания для облегчения читаемости кода ExitProcess equ __imp__ExitProcess@4 endif ; shell32.inc ; включаемый файл с определениями функций из shell32.dll ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ShellExecuteA:near ; присваивания для облегчения читаемости кода ShellExecute equ ShellExecuteA else includelib shell32.lib ; истинные имена используемых функции extrn __imp__ShellExecuteA@24:dword ; присваивания для облегчения читаемости кода ShellExecute equ __imp__ShellExecuteA@24 endif Имена всех системных функций Win32 модифицируются так, что перед именем функции ставится подчеркивание, а после — знак «@» и число байт, которое занимают параметры, передаваемые ей в стеке, так ExitProcess() превращается в _ExitProcess@4(). Компиляторы с языков высокого уровня часто останавливаются на этом и вызывают функции по имени _ExitProcess@4(), но реально вызывается небольшая процедура-заглушка, которая ничего не делает, а только передает управление на такую же метку, но с добавленным «__imp_» — __imp__ExitProcess@4(). Во всех наших примерах мы будем обращаться напрямую к __imp__ExitProcess@4(). К сожалению, TASM (а точнее TLINK32) использует собственный способ вызова системных функций, который нельзя так обойти, и программы, скомпилированные с его помощью, оказываются немного больше и в некоторых случаях работают медленнее. Мы отделили описания функций для TASM во включаемых файлах при помощи директив условного ассемблирования, которые будут использовать их, если в командной строке ассемблера указать /D_TASM_. Кроме этого, все функции, работающие со строками (как, например, ShellExecute()), существуют в двух вариантах. Если строка рассматривается в обычном смысле, как набор символов ASCII, к имени функции добавляется «A» (ShellExecuteA()). Другой вариант функции, использующий строки в формате UNICODE (два байта на символ), заканчивается буквой «U». Во всех наших примерах будем использовать обычные ASCIIфункции, но, если вам потребуется перекомпилировать программы на UNICODE, достаточно только поменять «А» на «U» во включаемых файлах. Итак, теперь, когда у нас есть все необходимые файлы, можно скомпилировать первую программу для Windows. Компиляция MASM: ml /с /coff /Cp winurl.asm link winurl.obj /subsystem:windows (здесь и далее используется 32-битная версия link.exe) Компиляция TASM: tasm /m /ml /D_TASM_ winurl.asm tlink32 /Tpe /aa /c /x winurl.obj Компиляция WASM: wasm winurl.asm wlink file winurl.obj form windows nt op с Также для компиляции потребуются файлы kernel32.lib и shell32.lib в первом и третьем случае и import32.lib — во втором. Эти файлы входят в дистрибутивы любых средств разработки для Win32 от соответствующих компаний — Microsoft, Watcom (Sybase) и Borland (Inprise), хотя их всегда можно воссоздать из файлов kernel32.dll и shell32.dll, находящихся в каталоге WINDOWS/SYSTEM. Иногда вместе с дистрибутивами различных средств разработки для Windows идет файл windows.inc, в котором дано макроопределение Invoke или заменена макросом команда call так, что они принимают список аргументов, первым из которых идет имя вызываемой функции, а затем через запятую — все параметры. С использованием этих макроопределений наша программа выглядела бы так: _start: xor ebx,ebx Invoke ShellExecute, ebx, ebx, offset URL, ebx, \ ebx, ebx Invoke ExitProcess, ebx end _start И этот текст компилируется в точно такой же код, что и у нас, но выполняется вызов не функции __imp__ExitProcess@4(), а промежуточной функции _ExitProcess@4(). Использование этой формы записи не позволяет применять отдельные эффективные приемы оптимизации, которые мы будем приводить в наших примерах, — помещение параметров в стек заранее и вызов функции командой JMP. И наконец, файла windows.inc у вас может просто не оказаться, так что будем писать push перед каждым параметром вручную. 3. Работа с меню и диалогом Пример ниже демонстрирует работу с графическим интерфейсом Windows (GUI Graphical User Interface). Для организации такого интерфейса необходимо создать окно и определить процедуру - обработчик событий. Данная процедура имеет следующий прототип: LRESULT WndProc(HWND,UINT,WPARAM,LPARAM) Где LRESULT 32 битное возвращаемое значение, зависимое от типа сообщения (события). Все параметры тоже 32 битные значения: HWND – дескриптор окна, UINT - тип сообщения (события), WPARAM – W сообщение LPARAM – L сообщение. W и L сообщения зависят от типа сообщения. Существуют предопределенные окна – диалоговые панели. Данный вид окон также имеет обработчик событий. Но для создания такого окна нужно описать специальный ресурс. Все виды ресурсов представляют собой бинарные файлы с определенной структурой. Для создания ресурса необходимо выполнить описание на специальном языке. В файле *.rc и откомпилировать этот файл компилятором ресурсов. Существует множество видов ресурсов. Кроме того, Windows позволяет создавать пользовательские виды ресурсов. В лабораторной работе будут использованы два вида ресурсов: диалог и меню. В состав Win32 входят несколько групп функций для работы с ресурсами. Для указания системе как найти нужный ресурс используется следующая информация: дескриптор приложения, тип ресурса, имя ресурса. Ниже приведен пример описания ресурсного файла: // windlg.rc // файл ресурсов, описывающий диалог для программы windlg.asm // все следующие определения стилей можно заменить на #include <winuser.h> // стили диалогов #define DS_CENTER 0x0800L #define DS_MODALFRAME 0x80L #define DS_3DLOOK 0x0004L // стили окон #define WS_MINIMIZEBOX 0x00020000L #define WS_SYSMENU 0x00080000L #define WS_VISIBLE 0x10000000L #define WS_OVERLAPPED 0x00000000L #define WS_CAPTION 0xC00000L // стили для edit control #define ES_AUTOHSCROLL 0x80L #define ES_LEFT 0 #define ZDLG_MENU 7 // идентификаторы элементов диалоговой панели #define IDC_EDIT 0 #define IDC_BUTTON 1 #define IDC_EXIT 2 // идентификаторы пунктов меню #define IDM_GETTEXT 10 #define IDM_CLEAR 11 #define IDM_EXIT 12 ZZZ_Dialog DIALOG 10,10,205,30 // x, y, ширина, высота STYLE DS_CENTER | DS_MODALFRAME | DS_3DLOOK | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED CAPTION "win32 assembly dialog example" // заголовок MENU ZDLG_MENU // меню BEGIN // список контролов диалога EDITTEXT IDC_EDIT,15,7,111,13,ES_AUTOHSCROLL | ES_LEFT PUSHBUTTON "E&xit",IDC_EXIT,141,8,52,13 END ZDLG_MENU MENU BEGIN POPUP "Test" BEGIN MENUITEM "Get Text",IDM_GETTEXT MENUITEM "Clear Text",IDM_CLEAR MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT END END // меню диалога Скомпилировав данный файл компилятором ресурсов, получаем файл с расширением res. Этот файл «понимает» редактор связей tlink32.exe. Файл описывает данные для создания диалоговой панели и меню. Текст программы работающий с данными ресурсами: ; windlg.asm ; Графическое win32-приложение, демонстрирующее работу с диалогом ; ; Компиляция TASM ; tasm /m /ml /D_TASM_ windlg.asm ; brcc32 windlg.rc ; tlink32 /Tpe /aa /c /x windlg.obj,,,,,windlg.res ; ; идентификаторы контролов (элементов диалога) IDC_EDIT equ 0 IDC_BUTTON equ 1 IDC_EXIT equ 2 ; идентификаторы элементов меню IDM_GETTEXT equ 10 IDM_CLEAR equ 11 IDM_EXIT equ 12 include def32.inc include kernel32.inc include user32.inc .386 .model flat .data dialog_name db "ZZZ_Dialog",0 ; имя диалога в ресурсах .data? buffer db 512 dup(?) ; буфер для введённого текста .code _start: xor ebx,ebx ; в EBX будет 0 для команд push 0 (короче в 2 раза) ; определим идентификатор нашей программы ; Вызов GetModuleHandle(NULL) push ebx call GetModuleHandle ; запустим диалог ; Вызов DialogBoxParam(hModule,DlgName,hwnd,DlgProc,NULL) push ebx ; значение, которое перейдёт как параметр WM_INITDIALOG push offset dialog_proc ; адрес процедуры типа DialogProc push ebx ; идентификатор окна-предка (0 - ничей диалог) push offset dialog_name ; адрес имени диалога в ресурсах push eax ; идентификатор программы, в ресурсах которой ; находится диалог (наш идентификатор в EAX) call DialogBoxParam ; выход из программы ; Вызов ExitProcess(NULL) push ebx call ExitProcess ; ; процедура dialog_proc ; обработчик событий ; вызывается диалогом каждый раз, когда в нём что-нибудь происходит ; именно здесь будут происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и EBX ! ; dialog_proc proc near ; так как мы получаем параметры в стеке, построим стековый кадр push ebp mov ebp,esp ; процедура типа DialogProc вызывается со следующими параметрами dp_hWnd equ dword ptr [ebp+08h] ; идентификатор диалога dp_uMsg equ dword ptr [ebp+0Ch] ; номер сообщения dp_wParam equ dword ptr [ebp+10h] ; первый параметр dp_lParam equ dword ptr [ebp+14h] ; второй параметр mov ecx,dp_hWnd ; ECX будет хранить идентификатор диалога mov eax,dp_uMsg ; а EAX - номер сообщения cmp eax,WM_INITDIALOG ; если мы получили WM_INITDIALOG jne not_initdialog ;Вызов GetDlgItem(hwnd,id) push IDC_EDIT push dp_hWnd call GetDlgItem ; определим идентификатор ;Вызов SetFocus(hwnd) push eax ; окошка редактирования текста call SetFocus ; и передадим ему фокус not_initdialog: cmp eax,WM_CLOSE ; если мы получили WM_CLOSE jne not_close ;Вызов EndDialog(hwnd,0) push 0 push ecx call EndDialog ; закрыть диалог not_close: cmp eax,WM_COMMAND ; если мы получили WM_COMMAND jne not_command mov eax,dp_wParam ; EAX = wParam (номер сообщения) cmp dp_lParam,0 ; если lparam ноль - сообщение от меню jne lParam_not_0 cmp ax,IDM_GETTEXT ; если это пункт меню Get Text jne not_gettext ;Вызов GetDlgItemText(hwnd,id,buffer,size_of_buffer) push 512 ; размер буфера push offset buffer ; адрес буфера push IDC_EDIT ; номер конрола редактирования push ecx call GetDlgItemText ; считаем текст в buffer ;Вызов MessageBox(hwnd,Text,Title,mode) push MB_OK push offset dialog_name push offset buffer push dp_hWnd call MessageBox ; и покажем его в MessageBox not_gettext: cmp eax,IDM_CLEAR ; если это пункт меню Clear jne not_clear ;Вызов SetDlgItemText(hwnd,id,text) push 0 ; NULL push IDC_EDIT ; номер контрола push ecx call SetDlgItemText ; установим новый текст not_clear: cmp eax,IDM_EXIT ; если это пункт меню Exit jne not_exit ;Вызов EndDialog(hwnd,0) push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог lParam_not_0: ; lParam не ноль - сообщение от контрола cmp eax,IDC_EXIT ; если сообщение от кнопки Exit jne not_exit shr eax,16 cmp eax,BN_CLICKED ; если её нажали jne not_exit ;Вызов EndDialog(hwnd,0) push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог not_exit: xor eax,eax ; после обработки команды inc eax ; DialogProc должен возвращать TRUE (eax=1) leave ret 16 ; конец процедуры not_command: ; сюда передаётся управление если мы получили ; какое-то незнакомое сообщение xor eax,eax ; код возврата FALSE (eax=0) leave ret 16 ; конец процедуры dialog_proc endp end _start Текст программы состоит из трех частей. 1. Описание данных 2. Вход в программу 3. Обработчик диалоговой панели. При запуске программы вызывается функция, создающая диалоговую панель из ресурсов. Далее до закрытия диалоговой панели работают системные функции Windows. При возникновении событий (нажатии кнопок, выборов элементов меню, перемещении мыши и т.д.), Windows вызывает пользовательский обработчик событий. Именно обработчик сообщений является особенностью программирования под Windows. Он представляет собой программный блок типа switch/case, который по передаваемым параметрам в процедуру устанавливает тип сообщения и связанные с ним данные (параметры WPARAM и LPARAM). Действия выполняются на основе событий. В программе обрабатываются следующие события: WM_INITDIALOG создание диалоговой панели WM_CLOSE закрытие диалоговой панели (пользователь нажал на кнопку закрыь окно) WM_COMMAND сообщения от элементов диалоговой панели или меню. В свою очередь, при возникновении события WM_COMMAND проверяется какой элемент вызвал это событие (IDM_GETTEXT, IDM_CLEAR,IDM_EXIT – нажатие на пункты меню, IDC_EXIT – нажатие на кнопку «Exit») В программе используются следующие функции Win32: GetModuleHandle – для получения дескриптора текущей программы, DialogBoxParam – для создания диалоговой панели из ресурсов, ExitProcess – для выхода из программы, GetDlgItem – получение дескриптора элемента диалога по его идентификатору, EndDialog – для закрытия диалоговой панели, GetDlgItemText – для получения введенного пользователем текста в определенный элемент диалога, MessageBox – выдача информационного окна, SetDlgItemText – установка текста в определенный элемент диалога 4. Варианты заданий Используя вышеприведенный шаблон выполнить согласно варианту следующие преобразования с введенными пользователем данными 1. Вывести количество гласных букв во введенной строке 2. Определить четное или нечетное число введено 3. Определить количество цифровых символов в строке 4. Определить количество слов в строке 5. Вывести название месяца (или диагностировать ошибку ввода) для даты, введенной в цифровом формате 6. Вывести количество секунд, прошедших с полуночи, для времени, введенном в формате чч:мм:СС 7. Перевести число, введенное в десятичном формате в шестнадцатеричное 8. Вывести площадь квадрата, сторону которого ввели в виде строки 9. Вывести длину в сантиметрах, введенную в дюймах 10. Вывести строку прописью для двухразрядного десятичного числа, введенного цифрами