Часть III. Более сложные примеры программирования в Windows
Глава 1. Примеры программ, использующих таймер
Таймер является одним из мощных инструментов, предоставляемых операционной
системой и позволяющих решать самые разнообразные задачи. С таймером Вы
познакомились, когда занимались консольными приложениями. Там мы пользовались
функциями timeSetEvent
и timeKillEvent
. Для консольных
приложений это очень удобные функции. В оконных приложениях чаще используют
функции SetTimer
и KillTimer
. Особенность таймера,
создаваемого функцией SetTimer
, заключается в том, что сообщение
WM_TIMER
, которое начинает посылать система приложению после
выполнения функции SetTimer
, приходит со всеми другими сообщениями
наравне, на общих основаниях. Следовательно, интервал между двумя приходами
сообщения WM_TIMER
может несколько варьироваться. В большинстве
случаев это не существенно.
У сообщения таймера есть еще одна особенность. Если система посылает сообщение приложению, а предыдущее сообщение еще стоит в очереди, то система объединяет эти два сообщения. Таким образом, "вынужденный простой" не приводит к приходу на приложение подряд нескольких сообщений таймера.
Вот те задачи, которые можно решить с помощью таймера.
- Отслеживание времени: секундомер, часы и т.д. Нарушение периодичности не имеет значения, так как по приходе сообщения время можно отследить, вызвав функцию получения системного времени.
- Таймер - один из способов осуществления многозадачности. Можно установить сразу несколько таймеров на разные функции, в результате периодически будет исполняться то одна, то другая функция. Более подробно о многозадачности будет сказано в следующей главе.
- Периодический вывод на экран обновленной информации.
- Автосохранение - функция особенно полезная для редакторов.
- Задание темпа изменения каких-либо объектов на экране.
- Мультипликация - по приходе сообщения от таймера обновляется графическое содержимое экрана или окна, так что возникает эффект мультипликации.
Рассмотрим, как нужно обращаться с функцией SetTimer
. Вот
параметры этой функции.
- 1-й параметр - дескриптор окна, с которым ассоциируется таймер. Если этот
параметр сделать равным
NULL (0)
, то будет проигнорирован и второй параметр. - 2-й параметр - определяет идентификатор таймера.
- 3-й параметр - определяет интервал посылки сообщения
WM_TIMER
. - 4-й параметр - определяет адрес функции, на которую будет приходить
сообщение
WM_TIMER
. Если параметр равенNULL
, то сообщение будет приходить на функцию окна.
Если функция выполнилась успешно, то возвращаемым значением будет являться
идентификатор таймера, который, естественно, будет совпадать со вторым
параметром, если первый параметр будет отличным от NULL
. В случае
неудачи функция возвратит ноль.
Из сказанного следует, что функция может быть вызвана тремя способами:
- Задан дескриптор окна, а четвертый параметр задается равным нулю.
- Задан дескриптор окна, а четвертый параметр определяет функцию, на которую
будет приходить сообщение
WM_TIMER
. - Дескриптор окна равен
NULL
, а четвертый параметр определяет функцию, на которую будет приходить сообщениеWM_TIMER
. Идентификатор таймера в этом случае будет определяться по возвращаемому функцией значению.
Функция, на которую приходит сообщение WM_TIMER
, имеет следующие
параметры:
- 1-й параметр - дескриптор окна, с которым ассоциирован таймер.
- 2-й параметр - сообщение
WM_T1MER
. - 3-й параметр - идентификатор таймера.
- 4-й параметр - время в миллисекундах, которое прошло с момента запуска Windows.
Функция KillTimer
удаляет созданный параметр и имеет следующие
параметры:
- 1-й параметр - дескриптор окна.
- 2-й параметр - идентификатор таймера.
I
Первый пример, рассматриваемый в данном разделе, представляет простейший
пример таймера. Таймер отсчитывает десять тиков и закрывает диалоговое окно,
выдавая MessageBox
с сообщением об окончании работы программы.
Данная программа являет собой пример организации таймера на базе самой функции
окна.
Заметим, что начиная с данной главы большинство приводимых программ могут транслироваться и в пакете MASM32, и в пакете TASM32 без всяких изменений.
// файл timer.rc // определение констант #define WS_SYSMENU 0x00080000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x00010000L // стиль - кнопка #define BS_PUSHBUTTON 0x00000000L // кнопка в окне должна быть видимой #define WS_VISIBLE 0x10000000L // центрировать текст на кнопке #define BS_CENTER 0x00000300L // стиль кнопки #define WS_CHILD 0х40000000L // возможность фокусировать элемент // при помощи клавиши TAB #define WS_TABSTOP 0x00010000L #define DS_3DLOOK 0x0004L //определение диалогового окна DIAL1 DIALOG 0, 0, 240, 120 STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK CAPTION "Пример диалогового окна с таймером" FONT 8, "Arial" { // кнопка, идентификатор 5 CONTROL "Выход", 5, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 } ; файл timer.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_COMMAND equ 111h WM_TIMER equ 113h ; прототипы внешних процедур IFDEF MASM EXTERN ReleaseDC@8:NEAR EXTERN GetDC@4:NEAR EXTERN TextOutA@20: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 SetTimer@16:NEAR EXTERN KillTimer@8:NEAR ELSE EXTERN ReleaseDC:NEAR EXTERN GetDC:NEAR EXTERN TextOutA:NEAR EXTERN MessageBoxA:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SendMessageA:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR ReleaseDC@8 = ReleaseDC GetDC@4 = GetDC TextOutA@20 = TextOutA MessageBoxA@16 = MessageBoxA ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog SendMessageA@16 = SendMessageA SetTimer@16 = SetTimer KillTimer@8 = KillTimer ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; файл timer.asm .386P ; плоская модель .MODEL FLAT, stdcall include timer.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 COUNT DD 0 TEXT DB 0 CAP DB 'Сообщение',0 MES 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+10Н] ; 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 ; здесь реакция на закрытие окна L3: ; удалить таймер PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH], WM_INITDIALOG JNE L5 ; здесь начальная инициализация ; установить таймер PUSH 0 ; параметр = NULL PUSH 1000 ; интервал 1 с. PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FINISH L5: CMP DWORD PTR [EBP+0CH], WM_COMMAND JNE L2 ; кнопка выхода? CMP WORD PTR [EBP+10H],5 JNE FINISH JMP L3 L2: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FINISH ; не пора ли заканчивать ? CMP COUNT,9 ; выход без предупреждения JA L3 ; выход через сообщение JE L4 ; пришло сообщение таймера ; подготовить текст MOV EAX, COUNT ADD EAX,49 MOV TEXT, AL ; получить контекст PUSH DWORD PTR [EBP+08H] CALL GetDC@4 ; запомнить контекст PUSH EAX ; вывести значение счетчика PUSH 1 PUSH OFFSET TEXT PUSH 10 PUSH 10 PUSH EAX CALL TextOutA@20 ; удалить контекст POP EAX PUSH EAX PUSH DWORD PTR [EBP+08H] CALL ReleaseDC@8 ; увеличить счетчик INC COUNT JMP FINISH L4: INC COUNT ; сообщение о выходе по таймеру PUSH 0 PUSH OFFSET CAP PUSH OFFSET MES PUSH DWORD PTR [EBP+08H] ; дескриптор окна CALL MessageBoxA@16 JMP L3 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;------------------------------------------------- _TEXT ENDS END START
Рис. 3.1.1 Пример реализации простейшего таймера.
Прокомментируем программу на Рис. 3.1.1. Организация таймера здесь проста и очевидна. Единственное, что может вызвать трудность понимания, это то, как удается оставить
MessageBox
и одновременно закрыть диалоговое окно. Но здесь тоже все достаточно просто: сообщение появляется приCOUNT=9
, а когда приходит следующее сообщение, тоCOUNT
уже больше 9, и выполняется ветка закрытия диалогового окна.Трансляция программы.
Пакет MASM32.ML /c /coff /DMASM timer.asm RC timer.rc LINK /SUBSYSTEM:WINDOWS timer.obj timer.resПакет TASM32.TASM32 /ml timer.asm BRCC32 timer.rc TLINK32 -aa timer.obj,,,,,timer.resII
Следующая программа несколько сложнее предыдущей. Здесь действуют два таймера. Можно считать, что запускаются одновременно две задачи39. Одна задача с периодичностью 0.5 сек. получает системное время и формирует строку для вывода (
STRCOPY
). Эта задача имеет свою собственную функцию, на которую приходит сообщениеWM_TIMER
. Вторая задача работает в рамках функции окна. Эта задача с периодичностью 1 сек. выводит время и дату в окно редактирования, расположенное на диалоговом окне. Таким образом, две задачи взаимодействуют друг с другом посредством глобальной переменнойSTRCOPY
.Еще один важный момент хотелось бы отметить в связи с данной программой. Поскольку на функцию таймера приходит сообщение, в котором указан идентификатор таймера, мы можем на базе одной функции реализовать любое количество таймеров.
// файл timer2.rc // определение констант #define WS_SYSMENU 0x00080000L // элементы на окне должны быть изначально видимы #define WS_VISIBLE 0x10000000L // бордюр вокруг элемента #define WS_BORDER 0x00800000L // при помощи TAB можно по очереди активизировать элементы #define WS_TABSTOP 0x00010000L // текст в окне редактирования прижат к левому краю #define ES_LEFT 0x0000L // стиль всех элементов на окне #define WS_CHILD 0x40000000L // запрещается ввод с клавиатуры #define ES_READONLY 0x0800L #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Диалоговое окно с часами и датой" FONT 8, "Arial" { CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12 } ; файл timer2.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h ;сообщение приходит при создании окна WM_INITDIALOG equ 110h ;сообщение приходит при событии с элементом на окне WM_COMMAND equ 111h ;сообщение от таймера WM_TIMER equ 113h ; сообщение посылки текста элементу WM_SETTEXT equ 0Ch ; прототипы внешних процедур IFDEF MASM EXTERN SendDlgItemMessageA@20:NEAR EXTERN wsprintfA:NEAR EXTERN GetLocalTime@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN SetTimer@16:NEAR EXTERN KillTimer@8:NEAR ELSE EXTERN SendDlgItemMessageA:NEAR EXTERN _wsprintfA:NEAR EXTERN GetLocalTime:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR SendDlgItemMessageA@20 = SendDlgItemMessageA wsprintfA = _wsprintfA GetLocalTime@4 = GetLocalTime ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog SetTimer@16 = SetTimer KillTimer@8 = KillTimer ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура данных дата-время DAT STRUC year DW ? month DW ? dayweek DW ? day DW ? hour DW ? min DW ? sec DW ? msec DW ? DAT ENDS ; файл timer2.asm .386P ; плоская модель .MODEL FLAT, stdcall include timer2.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 TIM DB "Дата %u/%u/%u Время %u:%u:%u",0 STRCOPY DB 50 DUP (?) DATA DAT <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+10Н] ; 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 ; здесь реакция на закрытие окна ; удалить таймер 1 PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; удалить таймер 2 PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH], WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; установить таймер 1 PUSH 0 ; параметр = NULL PUSH 1000 ; интервал 1 с. PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 ; установить таймер 2 PUSH OFFSET TIMPROC ; параметр = NULL PUSH 500 ; интервал 0.5 с. PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FINISH ; отправить строку в окно PUSH OFFSET STRCOPY PUSH 0 PUSH WM_SETTEXT PUSH 1 ; идентификатор элемента PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA@20 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;-------------------------------- ;процедура таймера ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM - промежуток запуска Windows ; [EBP+10Н] ; WAPARAM - идентификатор таймера ; [EBP+0CH] ; WM_TIMER ; [EBP+8] ; HWND TIMPROC PROC PUSH EBP MOV EBP,ESP ; получить локальное время PUSH OFFSET DATA CALL GetLocalTime@4 ; получить строку для вывода даты и времени MOVZX EAX,DATA.sec PUSH EAX MOVZX EAX,DATA.min PUSH EAX MOVZX EAX,DATA.hour PUSH EAX MOVZX EAX,DATA.year PUSH EAX MOVZX EAX,DATA.month PUSH EAX MOVZX EAX,DATA.day PUSH EAX PUSH OFFSET TIM PUSH OFFSET STRCOPY CALL wsprintfA ; восстановить стек ADD ESP,32 POP EBP RET 16 TIMPROC ENDP _TEXT ENDS END STARTPuc. 3.1.2. Пример использования двух таймеров.