Урок 3.

Продолжим о консоли.

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

.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
EXTERN lstrlenA@4: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 lstrlenA@4
MOV EBX,EAX
;в конце - перевод строки
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

;функция определяет количество параметров (->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
      

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

  1. Обработка событий от клавиатуры и мыши.

  2. Создание консоли. 

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

    Обратимся теперь к нашей основной теме:   обработка событий от мыши и клавиатуры.   Начнем с программы.

.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
EXTERN lstrlenA@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 ?
    TITL DB "Обработка событий мыши",0
    BUF DB 200 dup(?)
    LENS DWORD ? ;количество выведенных символов
    CO 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
PUSH OFFSET TITL
CALL CharToOemA@8
PUSH OFFSET TITL
CALL SetConsoleTitleA@4
;******************************
;перекодировка строки
PUSH OFFSET STR1
PUSH OFFSET STR1
CALL CharToOemA@8
;длина строки
PUSH OFFSET STR1
CALL lstrlenA@4
;вывести строку
PUSH 0
PUSH OFFSET LENS
PUSH EAX
PUSH OFFSET STR1
PUSH HANDL
CALL WriteConsoleA@20
;цикл ожиданий: движение мыши или двойной щелчок
LOO:
;координаты курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD 
PUSH HANDL
CALL SetConsoleCursorPosition@8
;прочитать одну запись о событии
PUSH OFFSET CO
PUSH 1
PUSH OFFSET MOUS_KEY
PUSH HANDL1
CALL ReadConsoleInputA@16
;проверим, не с мышью ли что?
CMP WORD PTR MOUS_KEY,MOUSE_EV
JNE LOO1
;здесь преобразуем координаты мыши в строку
MOV AX,WORD PTR MOUS_KEY+6 ;Y-мышь
;копирование с обнулением старших битов
MOVZX EAX,AX 
PUSH EAX
MOV AX,WORD PTR MOUS_KEY+4 ;X-мышь
;копирование с обнулением старших битов
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 lstrlenA@4
;вывести на экран координаты курсора
PUSH 0
PUSH OFFSET LENS
PUSH EAX
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
JMP LOO ;к началу цикла
LOO1:
;нет ли события от клавиатуры?
CMP WORD PTR MOUS_KEY,KEY_EV
JNE LOO
;есть, какое?
CMP BYTE PTR MOUS_KEY+14,27
JNE LOO
;******************************
;закрыть консоль
CALL FreeConsole@0
PUSH 0
CALL ExitProcess@4
RET
_TEXT ENDS
END START

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

ML /c /coff consol4.asm

link32 /subsystem:console consol4.obj

consol4.asm - название последней программы. 

А теперь приступаем к коментарию программы.  Комментировать придется много, готовьтесь.

  1. Прежде, однако, мы рассмотрим одну весьма необычную, но чрезвычайно полезную API-функцию. Эта функция wsprintfA. Я подчеркиваю, что это именно API-функция, которая предоставляется системой приложению. Эта функция является неким аналогом библиотечной СИ-функции – sprintf. Первым параметром функции является указатель на буфер, куда помещается результат форматирования. Второй – указатель на форматную строку, например: “Числа: %lu, %lu”. Далее идут указатели на параметры (либо сами параметры, если это числа, см. ниже), число которых определено только содержимым форматной строки. А теперь - самое главное. Поскольку количество параметров не определено, то стек придется освобождать нам. Пример использования этой функции будет дан ниже.  Наконец отметим, что если функция выполнена успешно, то в EAX будет возвращена длина скопированной строки.  Поскольку функция «не знает», сколько параметров может быть в нее отправлено, разработчики не стали усложнять текст этой функции, и оставили нам проблему освобождения стека. Это производится командой ADD ESP,N. Здесь N - это количество освобождаемых байтов.

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

  3. А теперь подробно разберемся со структурой, в которой содержится информация о консольном событии. Прежде всего замечу, что в Си эта структура записывается с помощью типа данных 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

    Смещение

    Длина

    Значение

    +4

    4

    При нажатии клавиши значение поля больше нуля.

    +8

    2

    Количество повторов при удержании клавиши.

    +10

    2

    Виртуальный код клавиши.

    +12

    2

    Скан-код клавиши.

    +14

    2

    Для функции ReadConsoleInputA – младший байт равен ASCII-коду клавиши. Для функции

    ReadConsoleInputW слово содержит код клавиши в двухбайтной кодировке (Unicode).

    +16

    4

    Содержится состояния управляющих клавиш. Может являться суммой следующих констант:

    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

    Смещение

    Длина

    Значение

    +4

    4

    Младшее слово – X-координата курсора мыши, старшее слово – Y-координата мыши.

    +8

    4

    Описывает состояние кнопок мыши. Первый бит – левая кнопка, второй бит – правая кнопка, третий бит – средняя кнопка. Бит установлен – кнопка нажата.

    +12

    4

    Состояние управляющих клавиш. Аналогично предыдущей таблице.

    +16

    4

    Может содержать следующие значения:

    MOUSE_MOV equ 1h; было движение мыши

    DOUBLE_CL equ 2h; был двойной щелчок

      

    Событие WINDOW_BUFFER_SIZE_EVENT

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

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

    Мы довольно подробно остановились на всех этих вопросах в силу их сложности и не возможности разобраться самостоятельно в них только на основе текста программы.

Назад