Урок 4.

Еще о консольных приложениях. На подступах к приложению GUI.

 

        И так, мы с Вами научились создавать консольные приложения. Мы узнали, что можем управлять консольным окном: вводить и выводить техт, задавать заголовок, отслеживать нажатие клавишь и события от мыши. Существенно то, что консольное приложение по своим возможностям ничем не отличается от графических (GUI) приложений. Из консольного приложения мы можем создавать обычные  и диалоговые окна с элементами управления, в общем делать все то, что может делать GUI программа. Об этом речи еще будет в переди. Сегодня мы рассмотрим такой интересный вопрос, как таймер в консольном приложении.
        Программа, представленная ниже, открывает консоль и каждую секунду сообщает об одном таймерном тике. 

 

.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 lstrlenA@4:NEAR
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
PUSH OFFSET STR2
CALL CharToOemA@8
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
;процедура вызывается таймером
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 lstrlenA@4
PUSH 0
PUSH OFFSET LENS
PUSH EAX
PUSH OFFSET STR3
PUSH HANDL
CALL WriteConsoleA@20
INC NUM
POPA
RET 20 ;выход с освобождением стека
TIME ENDP
_TEXT ENDS
END START

Для функционирования  таймера мы применили функцию timeSetEvent. С помощью данной функции мы мы можем определить процедуру, которая будет автоматически вызываться через заданный промежуток времени. Вот параметры этой функции:

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

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

Сама вызываемая процедура получает также 5 параметров:

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

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

        Ну все, уф! Консольное приложение оставим пока в покое. Перейдем к приложениям графического интерфейса. 
        Центральными понятиеми приложения GUI является окно и цикл ожидания. Цикл ожидания принимает   сообщения и перенаправляет их окнам, точнее функциям окна. Функция окна отбирает нужное сообщение и формирует соответствующую реакцию на это сообщение. Так вкратце функционирует GUI приложение. А теперь подробнее. Ниже представлена стандартная структура цикла ожидания. 

 while (GetMessage (&msg,NULL,0,0))

 {
    //разрешить использование клавиатуры,
    //путем трансляции сообщений о виртуальных клавишах
    //в сообщения о алфавитно-цифровых клавишах

    TranslateMessage
(&msg);
    //вернуть управление
Windows и передать сообщение дальше 
    //
процедуре окна   
    DispatchMessage(&msg);  

}

Мы используем Си - нотацию, т.к. для начала это понятнее.  С помощью GetMessage мы улавливаем сообщения операционной системы. DispatchMessage направляет сообщение соответствующей функции окна. Функция TranslateMessage транслирует клавиатурные сообщения, так, что становится возможным обробатывать алфавитно-цифровые клавиши с помощью обычных ASCII кодов. К этому вопросу мы еще вернемся, сейчас же заметим, что кроме TranslateMessage в цикле могут и другие функции, но о них мы речь сейчас вести не будем. Кстати, когда произойдет выход из этого цикла? Правильно, когда будет получено нулевое сообщение.
    А теперь смотрите, как цикл ожидания будет выглядеть на ассемблере.

MSG_LOOP:
    
PUSH      0
    
PUSH      0
    
PUSH      0
    
PUSH      OFFSET MSG
    
CALL      GetMessageA@16
    
CMP       EAX, 0
    
JE        END_LOOP
    
PUSH      OFFSET MSG
    
CALL      TranslateMessage@4
    
PUSH      OFFSET MSG
    
CALL      DispatchMessageA@4
    
JMP       MSG_LOOP
END_LOOP:

Текст цикла весьма прозрачен, так что пояснять пока нечего. Структура MSG будет нами разъяснена позднее.
    Теперь обратимся к функции окна. Си-прототип будет иметь следующий вид.

LRESULT CALLBACK WindowFunc(
    HWND hwnd, 
    UINT message,              
    WPARAM wParam, 
    LPARAM lParam


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

WNDPROC  PROC
    PUSH EBP
    MOV
EBP,ESP ;теперь EBP указывает на вершину стека
;здесь можно обработать нужные сообщения

;--------------------------------------------------
    PUSH DWORD PTR [EBP+14H]; LPARAM (lParam)
    PUSH DWORD PTR [EBP+10H]; WPARAM (wParam)
    PUSH DWORD PTR [EBP+0CH]; MES (message)
    PUSH DWORD PTR [EBP+08H];HWND (hwnd)
    CALL DefWindowProcA@16
;-------------------------------------------------
    POP EBP
    RET 16
WNDPROC      ENDP

 В первую очередь я просил бы обратить бы внимание на досту к параметрам функции. О стеке, вызовах, параметрах, локальных переменных см. статьи из раздела "Ассемблер MASM32. Программирование"
        Программа GUI обычно начинается с создания главного окна.    Окно создается в три этапа:
а) регистарция класса окон.
б) создание окна.
в) вывод окна на экран.
        Рассмотрим все по порядку.
        Регистрация класса окон. Регистрация класса окон осуществляется с помощью функции RegisterClassA, единственным параметром которой является ука­затель на структуру WNDCLASS, содержащую информацию об окне.
        Создание окна. На основе зарегистрированного класса с помощью функции CreateWindowExA (или CreateWindowA) можно создать экземпляр окна. Как можно заметить, это весьма напоминает объектную модель программирования. 
    Вывод окна на экран осуществяется функцие ShowWindow. Кроме того следует выполнить функцию UpdateWindow. Смысл последней функции раскроется позднее.
    Теперь мы можем написать первую программу. Вот эта программа.

.386P
;плоская модель
.MODEL FLAT, stdcall
;константы 
;сообщение приходит при закрытии окна
WM_DESTROY equ 2 
;сообщение приходит при создании окна 
WM_CREATE equ 1 
;сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN equ 201h
;сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN equ 204h
;свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
style equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
;идентификатор стандартной иконки
IDI_APPLICATION equ 32512
;идентификатор курсора
IDC_CROSS equ 32515
;режим показа окна - нормальный
SW_SHOWNORMAL equ 1
;прототипы внешних процедур
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
;директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-----------------------------------------------
;структуры
;структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?;идентификатор окна, 
;получающего сообщение
MSMESSAGE DD ?;идентификатор сообщения
MSWPARAM DD ?;доп. информация о сообщении
MSLPARAM DD ?;доп. информация о сообщении
MSTIME DD ?;время посылки сообщения
MSPT DD ?;положение курсора, во время посылки
;сообщения 
MSGSTRUCT ENDS
;---------
WNDCLASS STRUC
CLSSTYLE DD ?;стиль окна
CLWNDPROC DD ?;указатель на процедуру окна
CLSCEXTRA DD ?;информация о доп. байтах для ;данной структуры
CLWNDEXTRA DD ?;информация о доп. байтах для
;окна
CLSHINSTANCE DD ?;дескриптор приложения
CLSHICON DD ?;идентификатор иконы окна
CLSHCURSOR DD ?;идентификатор курсора окна
CLBKGROUND DD ?;идентификатор кисти окна
CLMENUNAME DD ?;имя-идентификатор меню
CLNAME DD ?;специфицирует имя класса окон
WNDCLASS ENDS
;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
HINST DD 0 ;здесь хранится дескриптор приложения
TITLENAME DB 'Простой пример 32-битного приложения',0
CLASSNAME DB 'CLASS32',0
CAP DB 'Сообщение',0
MES1 DB 'Вы нажали левую кнопку мыши',0 
MES2 DB 'Выход из программы. Пока!',0
_DATA ENDS
;сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
;получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
;заполнить структуру окна
; стиль 
MOV [WC.CLSSTYLE],style 
;процедура обработки сообщений
MOV [WC.CLWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCEXTRA], 0
MOV [WC.CLWNDEXTRA], 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
;----------
MOV [WC.CLBKGROUND], 17 ;цвет окна
MOV DWORD PTR [WC.CLMENUNAME],0
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 TITLENAME ;имя окна
PUSH OFFSET CLASSNAME ;имя класса
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; команда перерисовать видимую
; часть окна, сообщение 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
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+014H] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0CH] MES
; [EBP+8] HWND
WNDPROC PROC
PUSH EBP
MOV EBP, ESP
CMP DWORD PTR [EBP+0CH], WM_DESTROY
JE WMDESTROY
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN ;левая ;кнопка
JE LBUTTON
CMP DWORD PTR [EBP+0CH],WM_RBUTTONDOWN ;правая ;кнопка
JE RBUTTON
JMP DEFWNDPROC
;нажатие правой кнопки мыши приводит к закрытию окна
RBUTTON:
JMP WMDESTROY
;нажатие левой кнопки мыши
LBUTTON:
;выводим сообщение
PUSH 0 ;MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES1
PUSH DWORD PTR [EBP+08H]
CALL MessageBoxA@16
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 ;MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES2
PUSH DWORD PTR [EBP+08H] ;дескриптор окна
CALL MessageBoxA@16
PUSH 0
CALL PostQuitMessage@4 ;сообщение WM_QUIT
MOV EAX, 0
FINISH:
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Окно на рисунке - результат работы нашей программы.    

Назад