Шаг 62.
Всплывающие подсказки

    На этом шаге мы рассмотрим реализацию всплывающих подсказок.

    Здесь мы рассмотрим весьма интересный вопрос о всплывающих подсказках. В визуальных языках программирования всплывающие подсказки организуются посредством установки соответствующих свойств объектов, расположенных на объекте-контейнере. Наша с вами задача - разработать механизм, позволяющий без каких-либо дополнительных библиотек устанавливать подсказки на любые объекты, расположенные в окне. Итак, приступаем.

    Прежде всего заметим, что всплывающая подсказка - это всего лишь окно с определенными свойствами. Вот эти свойства: DS_3DLOOK, WS_POPUP, WS_VISIBLE, WS_BORDER. В принципе можно экспериментировать - добавлять или удалять свойства. Но без одного свойства вы никак не обойдетесь - это WS_POPUP. Собственно POPUP можно перевести как поплавок. Кроме того, определение всплывающего окна в файле ресурсов не должно содержать опции CAPTION.

    Появление подсказки не должно менять ситуацию в диалоговом окне. Это значит - вызов подсказки должен быть немодальным, при помощи функции CreateDialogIndirect. Кроме того, следует предусмотреть переустановку фокуса на диалоговое окно. Для этого достаточно в нужном месте вызвать функцию SetFocus.

    Итак, подсказка - это диалоговое окно, и, следовательно, оно должно иметь свою функцию. Что должна содержать эта функция? По крайней мере, обработку трех событий: WM_INITDIALOG, WM_PAINT, WM_TIMER. По получении сообщения WM_INITDIALOG следует определить размер и положение подсказки. Кроме того, если мы предполагаем, что подсказка должна спустя некоторое время исчезать, следует установить таймер. По получении сообщения WM_PAINT следует вывести в окно подсказки текст. Если определять размер окна подсказки точно по строке выводимого текста, то цвет фона подсказки будет полностью определяться цветом выводимого текста. Наконец по приходе сообщения WM_TIMER мы закрываем подсказку.

    С самой подсказкой более или менее ясно. Определимся теперь, как и где будет вызываться эта подсказка. Реализуем такой подход: в основном диалоговом окне определяем таймер, в функции которого будет проверяться положение курсора. В зависимости от этого положения и будет вызываться или удаляться подсказка. В функции таймера нужно предусмотреть:

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

    Файл pr62_1.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 BS_PUSHBUTTON      0x00000000L
//Центрировать  текст на  кнопке.
#define BS_CENTER          0x00000300L
//Тип окна - "поплавок".
#define WS_POPUP           0x80000000L
//Стиль - диалоговое  окно Windows 95.
#define DS_3DLOOK          0x0004L
//Определение диалогового окна. 
DIAL1  DIALOG  0,  0,  240, 100
STYLE WS_SYSMENU | DS_3DLOOK 
CAPTION  "Окно с всплывающими подсказками" 
FONT 8,   "Arial" 
{
  //Окно редактирования, идентификатор 1.
  CONTROL  "",   1,    "edit",   ES_LEFT | WS_CHILD 
     | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 100, 5, 130, 12
  //Кнопка, идентификатор 2. 
  CONTROL "Выход",   2,    "button",   BS_PUSHBUTTON
     | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 
}
//Диалоговое окно подсказки. 
HINTW DIALOG  0,   0,   240,   8
STYLE DS_3DLOOK  | WS_POPUP | WS_VISIBLE | WS_BORDER 
FONT 8, "MS Sans Serif"
{
}

    Файл pr62_1.inc:

;Константы.
;Цвет фона окна подсказки.
RED      = 255
GREEN    = 255
BLUE     = 150
RGBB     equ  (RED or (GREEN shl 8)) or (BLUE shl 16)
;Цвет текста окна подсказки.
RED      = 20
GREEN    = 20
BLUE     = 20
RGBT     equ (RED or (GREEN shl 8)) or (BLUE shl 16)
;Сообщение приходит при закрытии окна.
WM_CLOSE      equ 10h
WM_INITDIALOG equ 110h
;Сообщение приходит при событии с элементом на окне.
WM_COMMAND    equ 111h
;Сообщение от таймера.
WM_TIMER      equ 113h
;Сообщение посылки текста элементу.
WM_SETTEXT    equ 0Ch
WM_PAINT      equ 0Fh
;Прототипы внешних процедур.
IFDEF MASM
    EXTERN  CreateDialogParamA@20:NEAR
    EXTERN  SetFocus@4:NEAR
    EXTERN  lstrcpyA@8:NEAR
    EXTERN  DestroyWindow@4:NEAR
    EXTERN  lstrlenA@4:NEAR
    EXTERN  GetDlgItem@8:NEAR
    EXTERN  GetCursorPos@4:NEAR
    EXTERN  TextOutA@20:NEAR
    EXTERN  SetBkColor@8:NEAR
    EXTERN  SetTextColor@8:NEAR
    EXTERN  BeginPaint@8:NEAR
    EXTERN  EndPaint@8:NEAR
    EXTERN  GetTextExtentPoint32A@16:NEAR
    EXTERN  MoveWindow@24:NEAR
    EXTERN  GetWindowRect@8:NEAR
    EXTERN  ReleaseDC@8:NEAR
    EXTERN  GetDC@4:NEAR
    EXTERN  SendDlgItemMessageA@20: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  CreateDialogParamA:NEAR
    EXTERN  SetFocus:NEAR
    EXTERN  lstrcpyA:NEAR
    EXTERN  DestroyWindow:NEAR
    EXTERN  lstrlenA:NEAR
    EXTERN  GetDlgItem:NEAR
    EXTERN  GetCursorPos:NEAR
    EXTERN  TextOutA:NEAR
    EXTERN  SetBkColor:NEAR
    EXTERN  SetTextColor:NEAR
    EXTERN  BeginPaint:NEAR
    EXTERN  EndPaint:NEAR
    EXTERN  GetTextExtentPoint32A:NEAR
    EXTERN  MoveWindow:NEAR
    EXTERN  GetWindowRect:NEAR
    EXTERN  ReleaseDC:NEAR
    EXTERN  GetDC:NEAR
    EXTERN  SendDlgItemMessageA:NEAR
    EXTERN  ExitProcess:NEAR
    EXTERN  GetModuleHandleA:NEAR
    EXTERN  DialogBoxParamA:NEAR
    EXTERN  EndDialog:NEAR
    EXTERN  SetTimer:NEAR
    EXTERN  KillTimer:NEAR
    CreateDialogParamA@20    = CreateDialogParamA
    SetFocus@4               = SetFocus
    lstrcpyA@8               = lstrcpyA
    DestroyWindow@4          = DestroyWindow
    lstrlenA@4               = lstrlenA
    GetDlgItem@8             = GetDlgltem
    GetCursorPos@4           = GetCursorPos
    TextOutA@20              = TextOutA
    SetBkColor@8             = SetBkColor
    SetTextColor@8           =  SetTextColor
    BeginPaint@8             = BeginPaint
    EndPaint@8               = EndPaint
    GetTextExtentPoint32A@16 = GetTextExtentPoint32A
    MoveWindow@24            = MoveWindow
    GetWindowRect@8          = GetWindowRect
    ReleaseDC@8              = ReleaseDC
    GetDC@4                  = GetDC
    SendDlgItemMessageA@20   = SendDlglteroMessageA
    ExitProcess@4            = ExitProcess
    GetModuleHandleA@4       = GetModuleHandleA
    DialogBoxPararaA@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	
;Cтруктура размера окна.
RECT  STRUC		
      L  DD ?		
      T  DD ?		
      R  DD ?
      B  DD ? 
RECT  ENDS
;Структура координаты точки. 
SIZ  STRUC
     X DD ?
     Y DD ? 
SIZ  ENDS
;Структура для BeginPaint. 
PAINTSTR STRUC
         hdc    DWORD 0
         fErase DWORD 0
         left   DWORD 0
         top    DWORD 0
         right  DWORD 0
         bottom DWORD 0
         fRes   DWORD 0
         flncUp DWORD 0
         Reserv DB 32 dup(0) 
PAINTSTR ENDS
;Структура для получения позиции курсора 
POINT  STRUC
       X DD  ?
       Y DD  ? 
POINT  ENDS 

    Файл pr62_1.asm:

.386P
;Плоская модель.
.MODEL FLAT, STDCALL
include pr62_1.inc
;Директивы компоновщику для подключения библиотек.
IFDEF MASM
;Для компоновщика  LINK.EXE.
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \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
     HIN        DB "HINTW",0
     XX         DD ?
     YY         DD ?
;-------------------------------------
     R1         RECT  <?>
     R2         RECT  <?>
     S          SIZ   <?>
     PS         PAINTSTR <?>
     PT         POINT <?>
;Ддескрипторы окон-подсказок для первого и второго элемента.
     H1         DD 0
     H2         DD 0
;Строка-подсказка.
     HINTS      DB  60  DUP(?)
;Перечень подсказок.
     HINT1      DB "Редактирование строки",0
     HINT2      DB "Кнопка выхода",0 
;Для временного хранения контекста устройства.
     DC         DD ? 
;Счетчик.
     P1         DD ? 
_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
;-------------------------------------
;-------------------------------------
;Процедура окна.
;Расположение параметров в стеке: 
;[ЕВР+014Н] LPARAM 
;[ЕВР+10Н] WAPARAM 
;[ЕВР+0СН] MES 
;[ЕВР+8] HWND 
WNDPROC PROC
        PUSH EBP
        MOV  EBP, ESP
        PUSH EBX
        PUSH ESI
        PUSH EDI
;-------------------------------------
        CMP  DWORD PTR [EBP+0CH],WM_CLOSE
        JNE  L1
;Здесь реакция на закрытие окна. 
;Удалить таймер.
L4:
        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
;Здесь начальная инициализация. 
;Установить таймер.
        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_COMMAND 
        JNE  L3 
;Кнопка выхода?
        CMP  WORD PTR [EBP+10H],2
        JNE  L3
        JMP  L4 
L3: 
FINISH:
        POP  EDI
        POP  ESI
        POP  EBX
        POP  EBP
        MOV  EAX,0
        RET  16
WNDPROC ENDP
;-------------------------------------
;Процедура таймера. 
;Расположение параметров в стеке.
;[ЕВР+014Н]   LPARAM - промежуток запуска Windows.
;[ЕВР+10Н]    WAPARAM - идентификатор таймера.
;[EBP+OCH]    WM_TIMER
;[ЕВР+8]      HWND
TIMPROC PROC 
        PUSH EBP 
        MOV  EBP,ESP
;Получить положение курсора.
        PUSH OFFSET PT
        CALL GetCursorPos@4 
;Запомнить координаты.
        MOV  EAX,PT.X
        MOV  XX, EAX
        MOV  EAX,PT.Y
        MOV  YY,EAX
;Получить положение элементов.
;Окно редактирования.
        PUSH 1
        PUSH DWORD PTR [EBP+08H]
        CALL GetDlgItem@8
        PUSH OFFSET R1
        PUSH EAX
        CALL GetWindowRect@8 
;Кнопка выхода.
        PUSH 2
        PUSH DWORD PTR [EBP+08H]
        CALL GetDlgItem@8
        PUSH OFFSET R2
        PUSH EAX
        CALL GetWindowRect@8 
;Увеличить счетчик.
        INC  P1
        MOV  ECX,XX 
        MOV  EDX,YY 
;Проверка условий. 
.IF H1==0 &&  P1>5
.IF EDX<=R1.B && EDX>=R1.T && ECX>=R1.L && ECX<=R1.R 
;Подготовить  строку.
        PUSH OFFSET HINT1 
        PUSH OFFSET HINTS 
        CALL lstrcpyA@8
;Создать диалоговое окно - подсказку 
        PUSH 0
        PUSH OFFSET HINT 
        PUSH DWORD PTR [EBP+08H] 
        PUSH OFFSET HIN 
        PUSH [HINST]
        CALL CreateDialogParamA@20 
        MOV  H1,EAX 
;Установить фокус.
        PUSH DWORD PTR [EBP+08H] 
        CALL SetFocus@4 
;Обнулить счетчик. 
        MOV  P1,0 
        JMP  _END 
.ENDIF 
.ENDIF 
.IF H1!=0
.IF (EDX>R1.B || EDX<R1.T) || (ECX<R1.L || ECX>R1.R) 
;Удаление подсказки в связи с перемещением курсора. 
        PUSH H1
        CALL DestroyWindow@4 
        MOV  H1,0 
        JMP  _END 
.ENDIF 
.ENDIF
.IF H2==0 && P1>5
.IF EDX<=R2.B && EDX>=R2.T && ECX>=R2.L && ECX<=R2.R 
;Подготовить строку.
        PUSH OFFSET HINT2 
        PUSH OFFSET HINTS
        CALL lstrcpyA@8 
;Сздать диалоговое окно - подсказку.
        PUSH 0
        PUSH OFFSET HINT
        PUSH DWORD PTR [EBP+08H]
        PUSH OFFSET HIN
        PUSH [HINST]
        CALL CreateDialogParamA@20
        MOV  H2,EAX 
;Установить фокус.
        PUSH DWORD PTR [EBP+08H]
        CALL SetFocus@4 
;Обнулить счетчик.
        MOV  P1,0
        JMP _END 
.ENDIF 
.ENDIF 
.IF H2!=0
.IF (EDX>R2.B || EDX<R2.T) || (ECX<R2.L || ECX>R2.R) 
;Удаление подсказки в связи с перемещением курсора.
        PUSH H2
        CALL DestroyWindow@4
        MOV  H2,0
        JMP  _END 
.ENDIF 
.ENDIF
;Восстановить стек. 
_END:
        POP  EBP
        RET  16
TIMPROC  ENDP
;-------------------------------------
;Процедура окна всплывающей подсказки 
HINT PROC
        PUSH EBP
        MOV  EBP,ESP
        CMP  DWORD PTR [EBP+0CH],WM_INITDIALOG
        JNE  NO_INIT 
;Инициализация. 
;Получить контекст.
        PUSH DWORD PTR [EBP+08H]
        CALL GetDC@4
        MOV  DC,EAX 
;Получить длину строки.
        PUSH OFFSET HINTS
        CALL lstrlenA@4 
;Получить длину и ширину строки.
        PUSH OFFSET  S
        PUSH EAX
        PUSH OFFSET  HINTS
        PUSH DC
        CALL GetTextExtentPoint32A@16 
;Установить положение и размер окна-подсказки.
        PUSH 0
        PUSH S.Y
        ADD  S.X,2
        PUSH S.X
        SUB  YY,20
        PUSH YY 
        ADD  XX,10 
        PUSH XX
        PUSH DWORD PTR [EBP+08H] 
        CALL MoveWindow@24 
;Закрыть контекст 
        PUSH DC
        PUSH DWORD PTR [EBP+08H] 
        CALL ReleaseDC@8 
;Установить  таймер. 
        PUSH 0
        PUSH 6000   ;Интервал 6 с.
        PUSH 3      ;Идентификатор таймера.
        PUSH DWORD PTR [EBP+08H] 
        CALL SetTimer@16 
        JMP  FIN 
NO_INIT:
        CMP  DWORD PTR [EBP+0CH],WM_PAINT
        JNE  NO_PAINT 
;Перерисовка окна. 
;Получить  контекст.
        PUSH OFFSET PS
        PUSH DWORD PTR [EBP+08H]
        CALL BeginPaint@8
        MOV  DC,EAX 
;Установить цвета фона и текста подсказки.
        PUSH RGBB
        PUSH EAX
        CALL SetBkColor@8
        PUSH RGBT
        PUSH DC
        CALL SetTextColor@8 
;Вывести текст.
        PUSH OFFSET HINTS
        CALL lstrlenA@4
        PUSH EAX
        PUSH OFFSET HINTS
        PUSH 0
        PUSH 0
        PUSH DC
        CALL TextOutA@20 
;Закрыть контекст.
        PUSH OFFSET PS
        PUSH DWORD PTR [EBP+08H]
        CALL EndPaint@8
        JMP  FIN 
NO_PAINT:
        CMP  DWORD PTR [EBP+0CH],WM_TIMER
        JNE  FIN
;Обработка события таймера. 
;Удалить таймер и удалить диалоговое окно. 
;Подсказка удаляется в связи с истечением срока 6 с.
        PUSH 3
        PUSH DWORD PTR [EBP+08H]
        CALL KillTimer@8
        PUSH DWORD PTR [EBP+08H]
        CALL DestroyWindow@4 
FIN:
        POP EBP
        RET  16
HINT ENDP 
_TEXT   ENDS	
        END START
Текст этой программы можно взять здесь.

    Результат работы приложения изображен на рисунке 1:


Рис.1. Результат работы приложения

    Прокомментируем приведенную программу.

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

    Трансляция программы:

    Как вы, наверное, уже поняли, процедура таймера проверяет каждые 0.5 с положение курсора. Если курсор находится на элементе (окне редактирования или кнопке) и подсказка еще не вызвана (H1 или Н2 отлично от нуля), то вызывается подсказка. При этом учитывается еще величина счетчика (Р1), чтобы подсказка появлялась с некоторой задержкой. Если при очередном вызове процедуры окажется, что курсор находится уже вне элемента, а подсказка еще на экране, то она удаляется. Данный механизм не учитывает случай, когда курсор быстро переходит от одного элемента к другому. В этом случае вероятна ситуация, когда на экране окажутся две подсказки! Впрочем, первая подсказка должна тут же исчезнуть.

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

    Функция GetCursorPos получает положение курсора в абсолютных координатах относительно экрана. Здесь не возникает проблем, т. к. функция GetWindowsRec также получает положение элемента окна в абсолютных координатах. Предварительно нам приходится определять дескриптор окна при помощи функции GetDlgItem.

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




Предыдущий шаг Содержание Следующий шаг