ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 63 от 10 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие друзья!


 CТАТЬЯ

Растровые изображения с прозрачными областями

Автор: Ron Gery
Перевод: Виталий Брусенцев
Демонстрационная программа к статье (12 кБ)
Предисловие переводчика

Да, эта статья написана в 1992 году, и самая свежая информация в ней относится к Windows 3.1. Но описываемые здесь алгоритмы и методы растровой графики до сих пор (за одним-двумя исключениями) работают в Windows. Также приятно, что приведенные алгоритмы описаны доходчиво и в деталях. В-общем, если Вы хотите разобраться в том, как работает прозрачность в Windows GDI - читайте эту статью!

Введение

Установив с помощью вызова функции SetBkMode() режим отображения фона как TRANSPARENT, можно выводить текст с прозрачным фоном, пунктирные линии с прозрачными разрывами и кисти с прозрачными областями. К сожалению, среда Windows не предоставляет таких же простых средств для вывода прозрачных растров. (Ну хорошо, представляет, но поддерживается этот метод далеко не везде - подробнее об этом ниже, в разделе "Простая растровая прозрачность".) К счастью, можно сымитировать этот эффект, используя маскирующий растр и несколько раз вызвав функцию BitBlt с правильными параметрами растровых операций.

Что из себя представляет растр с прозрачностью? Это растровая картинка, сквозь которую видна часть фонового изображения. Простой пример этого - иконка Control Panel. [Здесь речь идет о системе Windows 3.x - прим. перев. ] Эта иконка, вообще-то - прямоугольник, но когда Control Panel минимизируется, сквозь некоторые ее части просматривается рабочий стол. Говоря упрощенно, иконка - прямоугольный растр, некоторые пикселы которого помечены прозрачными. При отображении на экран они не изменяют область назначения. Еще более интересно применение прозрачности растровых изображений в задачах движения, непрямоугольности картинок и т.д. Изложенные методы имитации помогут решить эти и другие проблемы, связанные с прозрачностью.

Обозначения

В этой статье для описания пикселов исходного растрового изображения используются термины "прозрачный" (transparent) и "непрозрачный" (opaque). Прозрачными будем называть пикселы, которые не влияют на конечное изображение. Непрозрачные пикселы рисуются поверх точек назначения, заменяя их собой.

Предполагается, что черный цвет кодируется значениями "0" во всех двоичных разрядах, а белый цвет - значениями "1" соответственно. Это выполняется на всех известных графических драйверах Windows, включая основанные на палитре.

Базовая описываемая операция - перенос (blting) битов изображения из источника в место назначения. Дополнительные операции переноса используют монохромную маску. Источник и приемник представляют собой хэндлы графического контекста (HDC). Они обозначаются hdcSrc и hdcDest соответственно и могут представлять как растр (в памяти), так и непосредственно графическое устройство. Маска, обозначаемая hdcMask, должна представлять собой монохромное растровое изображение, выбранное в совместимом графическом контексте.

Основные понятия

До того, как перейти к собственно рисованию, вспомним основные понятия, используемые в растровой графике.

Растровая операция

Последний параметр функции BitBlt указывает код растровой операции (ROP). Он определяет, какая комбинация битов источника, приемника и шаблона (текущей выбранной кисти) создаст конечную картинку. Так как растр - всего лишь набор битов, ROP можно назвать булевским уравнением над битами. В зависимости от вида графического устройства, биты растра имеют разное значение:

  • Для монохромных устройств каждый бит представляет один пиксел: черный - значением 0, белый - значением 1.
  • Для цветных устройств каждый пиксел описывается набором битов - либо индексом в таблице цветов (палитре), либо непосредственным значением цвета.

Вне зависимости от конкретного назначения битов, ROP просто выполняет действия над ними.

Весь фокус заключается, конечно же, в том, чтобы получить осмысленную комбинацию битов. В приложении A к Руководству программиста по Windows 3.1 SDK приведен список из 256 возможных тернарных ROP. Они предоставляют множество способов комбинировать растровые данные, и зачастую один и тот же эффект можно получить разными путями. В этой статье мы будем иметь дело лишь с четырьмя ROP.

ПРИМЕЧАНИЕ
Тернарная операция - это операция над тремя операндами. Применительно к растрам это означает взаимодействие битов источника, назначения и выбранной в контексте устройства кисти (Brush или Pattern). Список упоминаемых здесь кодов ROP вы можете найти в MSDN в разделе Platform SDK/Graphics And Multimedia Services/ Windows GDI/Painting And Drawing/Painting And Drawing Reference/Raster Operation Codes. У наиболее применимых кодов ROP существуют символические имена, определенные в заголовочном файле windows.h
- прим. перев.
Название Логическая операция Как используется при имитации прозрачности
SRCCOPY src Копирует источник (src) непосредственно на место назначения (dst).
SRCAND src AND dest Заполняет черным цветом те области назначения, которым в источнике соответствуют области черного цвета. Не затрагивает те области назначения, которым в источнике соответствуют области белого цвета.
SRCINVERT src XOR dest Производит операцию логического умножения (XOR) над битами источника и приемника. Результат помещает в приемник. При повторном применении восстанавливает предыдущее состояние. При некоторых обстоятельствах можно использовать вместо SRCPAINT.
SRCPAINT src OR dest Отрисовывает не-черные области источника на приемнике. Черные области источника не влияют на приемник.

Некоторые принтеры не поддерживают определенные коды растровых операций - в особенности ROP, которые затрагивают область назначения. По этой причине описываемые здесь методы касаются дисплейных устройств и не обязательно будут работать на принтерных (таких, как PostScriptR).

Маски прозрачности

В этой статье слово "маска" означает не ту штуку, которую Бэтмен носит на лице, а растр, ограничивающий видимую порцию другого растра. Маска содержит непрозрачную составляющую (черную), "сквозь которую" виден исходный растр, и прозрачную (белую) область, в которой пикселы приемника останутся нетронутыми. Так как маска состоит лишь из двух цветов, ее удобно представлять в виде монохромного растра [т.е., растра с форматом 1 бит на пиксел - прим. перев.]. Но ничто не помешает хранить такую маску в многоцветном растре (но содержащем лишь черные и белые пикселы). Как обсуждается ниже, в разделах "Метод истинной маски" и "Метод черного источника", перенос маски является частью многопроходного процесса рисования: он подготавливает приемник к окончательной отрисовке исходного растра с прозрачностью. Приводимое в качестве примера приложение TRANSBLT использует монохромную маску с пикселами, равными 1 для прозрачных и 0 для непрозрачных областей. При желании приложение может обращать эти два значения и компенсировать это в процессе преобразования из монохромного формата в цветной, как описано ниже в этом разделе.

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

Преобразование из монохромного формата в цветной

Имитация прозрачности может также включить имеющийся в Windows механизм преобразования растров из черно-белого формата в цветной (и наоборот). Для отображения между форматами используется принятые в Windows обозначения: цвет текста (text color, foreground color) и цвет фона (background color). Во время переноса бит на цветной приемник монохромный источник (и, если необходимо, кисть) "на лету" преобразуется в цветной формат - до того, как выполнится ROP над битами. Пикселы со значением 0 (черные) преобразуются в цвет текста назначения, а, соответственно, белые (со значением 1) - в цвет фона. И наоборот, когда формат назначения - монохромный, Windows преобразует цветной источник в этот формат. В этом случае все пикселы источника, имеющие цвет, совпадающий с цветом фона, становятся единицами в битовом представлении, а пикселы с цветом текста - нулями. Так как во всех приводимых ниже примерах используется монохромная маска, для приложения жизненно важно правильно установить цвета текста и фона (с помощью вызовов SetTextColor и SetBkColor) перед выполнением операций переноса.

Производительность и мерцание

Интенсивные растровые операции ведут к падению производительности из-за вовлечения большого количества бит в обработку. Кроме того, при выводе непосредственно на экран возникает мерцание - чем больше размер затрагиваемой области, тем заметнее. Хотя не существует волшебного способа ускорить обработку, мерцание можно устранить - используя "теневые" растры. Для этого в растр, находящийся в памяти, копируется область экрана, в которую будет происходить вывод. Затем над этим растром (вместо непосредственно экрана) производятся необходимые операции, например, для получения эффекта прозрачности. И наконец, сформированный "теневой" растр выводится на экран. Мерцание устраняется, так как содержимое экрана изменилось всего один раз. Очевидно, что два дополнительных переноса бит вызовут падение скорости (хотя на некоторых устройствах перенос в/из памяти будет быстрее, чем с участием экрана), но исчезновение мерцания может создать ощущение того, что работа приложения ускорилась (по-разному, в зависимости от вида обработки и реального размера растров). Вывод также становится намного симпатичнее без мерцания. Необходимость применения "теневых" растров определяется исходя из назначения приложения.

Метод истинной маски

Для работы данного метода не требуется никаких изменений в исходном растре, что может быть полезно. Маскированный перенос использует трехпроходный процесс и маску, содержащую прозрачные (со значением 1) и непрозрачные (со значением 0) пикселы. Вот пример псевдокода:


// Подготовить приемник для монохромного переноса (необходимо только 
// для монохромной маски). Это - значения по умолчанию и не могут быть 
// изменены. Их также необходимо восстановить после переноса    
        
SetBkColor(hdcDest, RGB(255, 255, 255));	// все 1 --> 0xFFFFFF
SetTextColor(hdcDest, RGB(0, 0, 0));		// все 0 --> 0x000000 
// Реальная работа
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCINVERT);
BitBlt(hdcDest, x, y, dx, dy, hdcMask, 0, 0, SRCAND);
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCINVERT);        

При переносе выполняются следующие действия:

  1. Первый шаг (BitBlt со значением ROP, равным SRCINVERT) изменяет с помощью XOR биты приемника, используя биты источника. Это выглядит немного забавно, но второй XOR вернет картинку в исходное состояние.
  2. Второй шаг (BitBlt со значением SRCAND) - операция маскирования. При наложении с помощью операции AND маски на биты приемника все прозрачные пикселы оставляют изображение нетронутым, тогда как непрозрачные сбрасывают его в 0 (черный цвет). Теперь приемник содержит черные пикселы в непрозрачной области и инвертированные источником пикселы - в прозрачной.
  3. На третьем шаге (BitBlt со значением SRCINVERT) вновь биты источника накладыватся XOR на приемник. Прозрачные пикселы восстанавливаются в исходное состояние (после двух последовательных XOR), а непрозрачные копируются с источника (значение XOR 0 = значение).

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

Метод черного источника

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


// Подготовить приемник для монохромного переноса (необходимо только 
// для монохромной маски). Это - значения по умолчанию и не могут быть 
// изменены. Их также необходимо восстановить после переноса
SetBkColor(hdcDest, RGB(255, 255, 255));	// все 1 --> 0xFFFFFF
SetTextColor(hdcDest, RGB(0, 0, 0));		// все 0 --> 0x000000
// Реальная работа
BitBlt(hdcDest, x, y, dx, dy, hdcMask, 0, 0, SRCAND);
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCPAINT);

И вновь используется маска, чтобы заполнить черным цветом непрозрачные места и оставить оставшиеся пикселы нетронутыми. Затем источник накладывается на место назначения с помощью OR, рисуя на не-черных областях приемника. Так как в прозрачных местах источника содержатся только черные пикселы, операция OR оставляет приемник в этих местах нетронутым. Заметьте, что для второго BitBlt могла быть с успехом применена операция SRCINVERT вместо SRCPAINT. Предварительная подготовка источника устраняет возможность случая (1 XOR 1), в котором эти две операции отличаются.

Экранное мерцание при этом методе значительно менее заметно, и прозрачность выглядит очень хорошо, если Вы поместили черные пикселы в нужных местах источника. Это - тот самый механизм, который используется Windows для рисования иконок. Файлы .ICO состоят из двух частей, XOR-маски и самой картинки. Для растров таких малых размеров прозрачность достигается очень легко.

Растровая прозрачность

Этот термин обычно описывает процесс превращения одного из цветов растра в прозрачный, так что при выводе растра на экран сквозь него видна часть изображения. Эту операцию можно имитировать построением соответствующей маски и использованием маскирующих технологий, описанных ранее. Следующие разделы описывают, как имитировать растровую прозрачность для дисплейных устройств, неспособных выполнять прозрачный перенос растров.

Построение маски

Создать монохромную маску из цветного растра довольно просто - встроенное в BitBlt преобразование проделает всю работу автоматически. Цель в том, чтобы в полученной маске все непрозрачные пикселы были установлены в 0, а прозрачные - в 1. Установив цвет фона равным прозрачному цвету, Вы именно это и проделаете. Нет необходимости устанавливать цвет текста, потому что он в преобразовании из цветного режима в монохромный не используется (все пикселы, отличные по цвету от фоновых, сбрасываются в 0). Это выполняет приведенный код:


SetBkColor(hdcSrc, rgbTransparent);
BitBlt(hdcMask, 0, 0, dx, dy, hdcSrc, x0, y0, SRCCOPY);

Он создает маску с единицами в тех местах, где пикселы источника имеют прозрачный цвет, и нулями - в остальных - то есть такую же, что мы использовали ранее.

Использование маски

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

Метод черного источника, с другой стороны, требует дополнительной работы над исходным растром - прозрачные биты нужно установить в 0. Конечно, если прозрачным цветом с самого начала является черный, растр уже готов к выводу. Сброс прозрачных пикселов в черный цвет на исходном растре очень похож на уже описанный сброс непрозрачных пикселов на приемнике. Он выполняется с использованием маски:


SetBkColor(hdcSrc, RGB(0,0,0));		// все 1 --> черный (0x000000) 
SetTextColor(hdcSrc,RGB(255,255,255));	// все 0 --> белый (0xFFFFFF)
BitBlt(hdcSrc, x0, y0, dx, dy, hdcMask, 0, 0, SRCAND);

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


SetBkColor(hdcSrc, rgbTransparent);	// все 1 --> прозрачный цвет 
SetTextColor(hdcSrc, RGB(0,0,0));	// все 0 --> черный (0x000000)
BitBlt(hdcSrc, x0, y0, dx, dy, hdcMask, 0, 0, SRCPAINT);

Так как необходимо затрагивать, а затем восстанавливать исходный растр, общее число битовых переносов достигло уже четырех. Это замедляет процесс, но так как 2 переноса выполняются в растр, находящийся в памяти, а не на экране, мерцание гораздо менее заметно, чем в методе истинной маски. Если исходный растр можно содержать с установленными в черный цвет прозрачными областями, то оба переноса становятся не нужны, и для вывода на экран требуется только два битовых переноса - это просто необходимо для анимации.

Простая растровая прозрачность

Некоторые драйверы устройств прямо поддерживвают прозрачность. Драйвер сообщает об этой способности с использованием бита C1_TRANSPARENT, возвращая его при вызове GetDeviceCaps с параметром CAPS1. Специальный режим фона NEWTRANSPARENT говорит о том, что последующие переносы бит являются прозрачными. Текущий цвет фона назначения при этом должен быть прозрачным. При наличии такой возможности в драйвере прозрачная отрисовка выполняется так:


// Пытаемся только если режим поддерживается
if(GetDeviceCaps(hdcDest, CAPS1) & C1_TRANSPARENT)
{
// Специальный режим прозрачного фона
    oldMode = SetBkMode(hdcDest, NEWTRANSPARENT);
    rgbBk =  SetBkColor(hdcDest, rgbTransparent);
// Простое копирование; прозрачность получится автоматически
    BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCCOPY);
    SetBkColor(hdcDest, rgbBk);
    SetBkMode(hdcDest, oldMode);
}       

Это, конечно упрощает жизнь программисту. К сожалению, этот режим в настоящее время поддерживается немногими драйверами устройств - те, что поставляются с Windows 3.1, его не поддерживают. Ситуация должна измениться к лучшему в ближайшем будущем.

ПРИМЕЧАНИЕ

Забудьте об этом. Константы CAPS1 и C1_TRANSPARENT убраны из Platform SDK. Режим NEWTRANSPARENT оставлен в mmsystem.h по всей видимости, по недосмотру. Чтобы узнать, как без проблем выводить прозрачные растры в новых версиях Windows, прочитайте в MSDN описание Image Lists и функции TransparentBlt, а также взгляните на статью "Прозрачность - это просто" на нашем сайте. - Прим. перев.

Прозрачность и DIB'ы

Если исходный растр является аппаратно-независимым (Device-Intependent Bitmap, DIB), весь процесс "маскировки" можно сильно упростить, используя его, и как источник, и как маску одновременно и манипулируя таблицей цветов. Этот процесс идентичен вышеописанному - кроме того, что приложение может выполнять цветовые преобразования, изменяя таблицу цветов, как в приведенном примере псевдокода:


// Сохранить копию таблицы цветов.
// Сохранить маску.
for (every color in the color table)
{
    if (color == rgbTransparent)
        color = white;
    else
        color = black; 
}

// Подготовить приемник с помощью переноса маски. 
StretchDIBits(hdcDest, lpDIB, SRCAND); // (Да, там есть еще параметры) 
// Теперь подготовим "зачерненный" источник для маскированного переноса.
for(every color in the color table)
{
    if (color == white)  // (мы его изменяли ранее)
        color = black;
    else
        color = original color from color table;
}
// Выведем   приемник с эффектом прозрачности. 
StretchDIBits(hdcDest, lpDIB, SRCPAINT); // (Да, там есть еще параметры) 

// Восстановим первоначальную таблицу цветов.

Заметьте, что в данном способе требуется только одна копия растра - и для источника, и для маски прозрачности, так как используется преимущество в виде таблицы цветов. Однако остаются дополнительные расходы по преобразованию DIB в аппаратно-зависимый растр.


 ВОПРОС - ОТВЕТ

Как обработать нажатие Enter в edit box'е?

Начнем с того, что для обработки нажатия Enter необходимо, чтобы (в общем случае) окно редактирования ожидало этого нажатия (т.е. имело стиль ES_MULTILINE). В противном случае система выполнит трансляцию этого нажатия в нажатие кнопки родительского окна, имеющей в текущий момент стиль BS_DEFAULTPUSHBUTTON. Кстати, это довольно неплохая методика для диалога, содержащего единственное окно ввода и имеющего кнопку по-умолчанию OK. Если же диалог (или окно) имеет несколько окон ввода, и логика работы приложения подразумевает, что нажатие Enter означает окончание ввода в выбранном окне и перевод фокуса на следующее, то скорее всего вам подойдет нижеследующая методика.

Основной вариант

Демонстрационный проект EditDlg

WinAPI
ПРИМЕЧАНИЕ
Обратите внимание, окно редактирования должно иметь стиль ES_MULTILINE.

Основная идея состоит в подмене стандартной процедуры окна редактирования (т.н. subclassing) при инициализации окна диалога, и выполнение в новой процедуре обработки нажатия клавиши. В нашем примере при обнаружении нажатия Enter выполняется копирование текста окна в буфер текста и перевод фокуса на следующий контрол диалогового окна. Если же была нажата иная клавиша, выполняется вызов стандартной оконной процедуры для окон класса "edit".


#include <windows.h>
#include "resource.h"

WNDPROC oldEditProc = NULL;

LRESULT CALLBACK newEditProc(HWND hEdit, UINT msg, WPARAM wParam, 
                                           LPARAM lParam)
{
    switch(msg)
    {
    case WM_KEYDOWN:
        {
            if(VK_RETURN == wParam)
            {
                HWND hParent = GetParent(hEdit);
                SendMessage( hParent, msg, wParam, lParam);
                SetFocus( GetNextDlgTabItem( hParent, hEdit, FALSE ) );
                return 0;   // запрет обработки по-умолчанию
            }
        }
        break;
    case WM_CHAR:
        if(VK_RETURN == wParam)
            return 0;       // запрет обработки по-умолчанию
        break;
    }
    return CallWindowProc(oldEditProc, hEdit, msg, wParam, lParam);
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static char m_edText[256] = "";

    switch(msg)
    {
    case WM_INITDIALOG:
        oldEditProc = (WNDPROC) SetWindowLong(
                                    GetDlgItem(hDlg, IDC_EDIT1),
                                    GWL_WNDPROC, (LONG)newEditProc);
        break;
    case WM_COMMAND:
        if(wParam == IDCANCEL)
            EndDialog(hDlg, 0);
        break;
    case WM_KEYDOWN:
        if( VK_RETURN == wParam)
            GetDlgItemText(hDlg, IDC_EDIT1, m_edText, 256);
        break;
    }
    return 0;
}

int WINAPI WinMain(  HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine, int       nCmdShow )
{
    DialogBox(hInstance, "MAINDLG", HWND_DESKTOP, (DLGPROC)DlgProc);
    return 0;
}

Обратите внимание на то, что обработчики сообщений при обнаружении нажатия Enter возвращают из оконной процедуры нуль. Это делается для того, чтобы сообщения не передавались обработчику по-умолчанию (и, следовательно, не выполнялось нажатие кнопки по-умолчанию).

MFC
ПРИМЕЧАНИЕ
Обратите внимание, окно редактирования должно иметь стиль ES_MULTILINE.

Для реализации поведения приложения, аналогичного только что описанному, необходимо создать класс, производный от CEdit, имеющий собственные обработчики сообщений WM_KEYDOWN и WM_CHAR (при создании класса и добавлении обработчиков используйте ClassWizard).


// .h-файл класса ////////////////////////////////////////////////

. . .

class CEnterEdit : public CEdit
{
public:
    CEnterEdit();
public:
    virtual ~CEnterEdit();
protected:
    //{{AFX_MSG(CEnterEdit)
    afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
    afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

// .cpp-файл класса //////////////////////////////////////////////

. . .

BEGIN_MESSAGE_MAP(CEnterEdit, CEdit)
    //{{AFX_MSG_MAP(CEnterEdit)
    ON_WM_KEYDOWN()
    ON_WM_CHAR()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()


void CEnterEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    if(nChar == VK_RETURN)
    {
        // Предполагаем, что родительское окно эдит-бокса -
        // диалог класса CEditDlgDlg, который имеет буфер хранения
        // введенного текста m_edText типа CString.

        CEditDlgDlg* pDlg = (CEditDlgDlg*) GetParent();
        GetWindowText(pDlg->m_edText);
        pDlg->GetNextDlgTabItem(this)->SetFocus();
        return;   // запрет обработки по-умолчанию
    }
    CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}

void CEnterEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    if(nChar == VK_RETURN)
        return;   // запрет обработки по-умолчанию
    CEdit::OnChar(nChar, nRepCnt, nFlags);
}
ПРИМЕЧАНИЕ
Подмена оконной процедуры - универсальный метод для получения необходимой функциональности. Если же есть возможность получить доступ к циклу сообщений, то можно воспользоваться альтернативной методикой - обработкой сообщения WM_KEYDOWN в самом цикле (см. далее - Альтернативный вариант).

Пример EditDlg демонстрирует обработку нажатия клавиши Enter. Он содержит два проекта - WinAPI и MFC.

Альтернативный вариант

Не всегда целесообразно обработку нажатия Enter возлагать на окно редактирования. Если в поведение приложения необходимо добавить указанную реакцию, но для самого окна достаточно обычной функциональности (однострочное окно редактирования), можно, не меняя стиля окна редактирования, самостоятельно обрабатывать нажатие Enter, анализируя содержимое сообщений в цикле обработки сообщений.

Необходимо помнить, что цикл обработки сообщений модального диалога реализуется самой системой и недоступен для программиста. В этом случае остается единственное средство - подмена оконной процедуры окна редактирования, описанная выше (см. Основной вариант).

Детали реализации этого метода очень сильно зависят от постановки задачи, среды разработки и организации цикла обработки сообщений. Общая схема такова:

  1. До выполнения DispacthMessage(&msg) необходимо проанализировать поле msg.message на приход сообщения WM_KEYDOWN.
  2. Если получено сообщение WM_KEYDOWN, и поле msg.wParam содержит VK_RETURN, то выполнить вызов функции-диспетчера нажатия Enter. При этом обычно необходимо избегать передачи полученного сообщения в функцию DispatchMessage(), чтобы не выполнялась обработка по-умолчанию.
  3. Для всех иных сообщений выполнить стандартную обработку.
MFC

Для программ, использующих MFC, все необходимые проверки выполняются в методе PreTranslateMessage() класса приложения или окна.


BOOL CMyWinApp::PreTranslateMessage ( MSG* pMsg )
{
    if(
        ( WM_KEYDOWN == pMsg->message ) &&
        ( VK_RETURN  == pMsg->wParam  )
      )
    {
        OnEnterPressed();  // вызов диспетчера нажатия Enter
        return TRUE;       // запрет дальнейшей обработки
    }
    // стандартная обработка сообщения
    return CWinApp::PreTranslateMessage ( pMsg );
}
WinAPI

Для приложений WinAPI реализация цикла обработки сообщений может выглядеть таким образом:


. . .
    while( GetMessage( &msg, NULL, 0, 0 ) )
    {
        if(
            ( WM_KEYDOWN == pMsg->message ) &&
            ( VK_RETURN  == pMsg->wParam  )
          )
        {
            OnEnterPressed();  // вызов диспетчера нажатия Enter
            continue;          // запрет дальнейшей обработки
        }
        // стандартная обработка сообщения
        TranslateMessage( &msg );    
        DispatchMessage ( &msg );
    }
. . .

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

Редкий вариант, но вдруг вам понравится...
ПРИМЕЧАНИЕ
Поскольку этот вариант является существенным только для модальных диалогов, в которых, для того чтобы добраться до цикла сообщений, необходимо применить то (сабклассинг окна диалога) или иное (постановка локального хука) ухищрение, и поскольку сказанное совершенно не относится к MFC, где модальные диалоги "от системы" практически не применяются, то мы рассмотрим только WinAPI-вариант.
...локальный хук?

Условимся заранее, что теорию применения хуков вы получите из любых других источников ( например, из статьи Kyle Marsh Хуки в Win32 или Dr. Joseph M. Newcomer Хуки и DLL на нашем сайте). Там же вы познакомитесь и с их разновидностями. Мы же продолжим решать нашу задачу - перехват нажатия Enter в модальном диалоге.

Итак, в качестве необходимого теоретического минимума заметим, что механизм "крюков" (hook - англ., крюк) позволяет приложению зарегистрировать некий обработчик, который система будет вызывать в ответ на события, происходящие в ее недрах, с целью оповещения пользовательского кода об этих событиях. Локальный хук вызывается только для событий, относящихся к процессу, поставившему хук, что практически никак не ухудшает общую производительность системы вцелом. И потому именно этот механизм подходит нам для наших целей.

Нам необходимо поставить хук типа , который позволяет проводить мониторинг событий в диалогах (в том числе и MessageBox), меню и полосах прокрутки. Код логически распадается на относительно стандартную часть, имеющую сходное строение для хуков любого типа, и специфическую часть, которая будет выполнять для нас полезную работу. Стандартный код может выглядеть следующим образом:

LRESULT DlgBoxMsgFilter( UINT code, WPARAM wParam, LPARAM lParam );

HHOOK g_hHook = NULL;

LRESULT CALLBACK HookProc( int code, WPARAM wParam, LPARAM lParam )
{
    LRESULT res = 0;
    
    // служебная обработка
    if( 0 > code )
        return CallNextHookEx( WH_MSGFILTER, code, wParam, lParam );

    // вызов пользовательской процедуры "полезного действия"
    res = DlgBoxMsgFilter( code, wParam, lParam );

    if( res > -1 )
        return res;
    return CallNextHookEx( WH_MSGFILTER, code, wParam, lParam );
}

BOOL CALLBACK DlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
    switch( msg )
    {
    case WM_INITDIALOG:
        // постановка хука...
        g_hHook = SetWindowsHookEx( WH_MSGFILTER, HookProc, 
                                    GetModuleHandle( NULL ), 
                                    GetCurrentThreadId() );
        break;

    case WM_COMMAND:
        switch( LOWORD(wParam) )
        {
        case IDCANCEL:
            if( BN_CLICKED == HIWORD(wParam) )
            {
                // ... и его снятие
                if( g_hHook )
                    UnhookWindowsHookEx( h_hHook );
                EndDialog( hDlg, 0 );
            }
            break;
        }
        break;
    }
    return 0;
}

Теперь обратимся к процедуре . Легко заметить, что она выполняет практически те же действия, что и из ОСНОВНОГО ВАРИАНТА, а именно - обнаружение нажатия Enter и переход на следующий контрол, имеющий стиль . Поскольку нас интересуют только события диалогов (а не меню, и не скроллбаров), то и фильтровать мы будем только коды типа .

LRESULT DlgBoxMsgFilter( UINT code, WPARAM wParam, LPARAM lParam )
{
    LPMSG pMsg   = (LPMSG)lParam;
    HWND  hEdit1 = GetDlgItem( g_hDlg, IDC_EDIT1 ), 
          hEdit2 = GetDlgItem( g_hDlg, IDC_EDIT2 );

    switch( code )
    {   
    case MSGF_DIALOGBOX:
        {
            // следим за нажатиями в обоих эдитбоксах
            if( hEdit1 != pMsg->hwnd && hEdit2 != pMsg->hwnd )
                return -1;

            switch(pMsg->message)
            {
            case WM_KEYDOWN:
                if( VK_RETURN == pMsg->wParam )
                {
                    // нажат Enter, сообщим об этом родительскому окну (диалогу)
                    SendMessage( g_hDlg, pMsg->message, pMsg->wParam, 
                                         pMsg->lParam );
                    
                    // перейдем к следующему TABSTOP-контролу диалога
                    SetFocus( GetNextDlgTabItem( g_hDlg, pMsg->hwnd, FALSE ) );
                    return TRUE;
                }
                break;
            }
        }
        break;
    }
    return -1;
}

На этом, собственно, мы и остановимся. Насколько понятно/удобно/оправдано пользоваться этим методом - судить вам.

ПРИМЕЧАНИЕ
В демонстрационном проекте вы найдете подпроект HkEdDlg, в котором продемонстрирована приведенная методика. Там же, кстати, вы сможете найти и пример реализации глобального (системного) хука, но это, как говорится, уже совсем другая история...


Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки