Сообщений 4    Оценка 45        Оценить  
Система Orphus

Как сделать нестандартную кнопку на основе битмапа?

Автор: Игорь Вартанов
Опубликовано: 10.12.2001
Исправлено: 13.03.2005
Версия текста: 1.2

Демонстрационный проект bmpbtn (33.1KB)

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

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

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

Рассмотрим основные этапы отрисовки контрола, имеющего стиль xx_OWNERDRAW.

  1. Родительскому окну контрола приходит сообщение WM_MEASUREITEM, в котором передается указатель на структуру MEASUREITEMSTRUCT через параметр lParam. Обработчик сообщения должен установить значения полей itemWidth и itemHeight структуры так, чтобы они содержали ширину и высоту контрола соответственно. Если мы обработали сообщение, обработчик должен вернуть значение TRUE из оконной процедуры. Это сообщение приходит владельцу один раз при создании контрола.
  2. Каждый раз при необходимости перерисовать контрол его владельцу приходит сообщение WM_DRAWITEM. Параметр lParam сообщения содержит указатель на структуру DRAWITEMSTRUCT, подготовленную системой. В задачу данного сообщения входит предоставление контекста, в котором будет происходить отрисовка контрола. Хэндл контекста сопровождает дополнительная информация о внутреннем состоянии контрола, необходимая (возможно) для изменения его внешнего вида, а также информация о виде действия, производимого в настоящий момент с контролом. Далее мы увидим, каким образом эта информация может быть использована для изменения внешнего вида кнопки. И, опять-таки, если мы обрабатываем данное сообщение, обработчик обязан вернуть из оконной процедуры значение TRUE.
Это - теория. Практика же показывает, что сообщение WM_MEASUREITEM в кнопку со стилем BS_OWNERDRAW не приходит. Удивлены? Мы - группа авторов сайта, обсуждавших этот эффект - тоже были удивлены. Информация по данному вопросу, приводимая в различных выпусках MSDN, противоречива, однако последние версии говорят, что подобное поведение системы считается нормой. Чтобы проверить, какие же сообщения все таки приходят в кнопку, в диалог демонстрационного проекта включен листбокс, в который выводится информация о приходящих сообщениях.

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

Если вы хотите, чтобы ваша кнопка в точности повторяла поведение стандартных кнопок, то вам предстоит потрудиться. Для начала изучите собственно стандартные кнопки - как они ведут себя в реальной жизни. Причем имейте ввиду, что наиболее сложное их поведение наблюдается в Windows 2000. Если с момента создания окна диалога ни разу не была нажата клавиша TAB, то невзирая на то, что ваша кнопка получила фокус ввода, рамка фокуса не должна выводиться - об этом говорит установленный флаг ODS_NOFOCUSRECT, причем он может приходить вместе с флагом ODS_FOCUS ( счастливые обладатели Windows 2000 могут в этом убедиться, запустив демонстрационную программу, нажав кнопку при свернутом диалоге и затем, развернув диалог, понаблюдать за флагами в сообщениях ). Разумеется, приоритет флага запрета отрисовки фокуса выше, и пунктирный прямоугольник не рисуется. После первого нажатия TAB этот флаг перестает устанавливаться (в Windows 9x и Windows NT он вообще отсутствует).

Практически та же картина наблюдается в Windows 2000 с подчеркиванием символа акселерации - до первого нажатия кнопки ALT кнопка получает флаг ODS_NOACCEL, что означает подавление подчеркивания акселератора.

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

  • выпуклая без фокуса и акселератора
  • выпуклая с фокусом без акселератора
  • выпуклая без фокуса с акселератором
  • выпуклая с фокусом и акселератором
  • вдавленная без фокуса и акселератора
  • вдавленная с фокусом без акселератора
  • вдавленная без фокуса с акселератором
  • вдавленная с фокусом и акселератором
  • неактивная

Вы, как разработчик, вправе принять решение, насколько точно вы будете следовать данной методике (я, к примеру, в демонстрационном приложении остановился на варианте без акселераторов :)).

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

Что касается кода, реализующего необходимую логику работы, то он может выглядеть примерно так:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBm[BM_COUNT] = { NULL, NULL, NULL, NULL, NULL };
    . . .
    case WM_INITDIALOG:
        . . .
        LoadBitmaps(hBm);
        . . .
    case WM_DRAWITEM:
        return DrawFreeStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
    . . .
}

BOOL DrawFreeStyleBtn(LPDRAWITEMSTRUCT pis, HBITMAP* phBm)
{
    if(IDC_BMPBTN == pis->CtlID)
    {
        HBITMAP hbm  = phBm[BM_UP]; // по-умолчанию выпуклая без фокуса

        switch(pis->itemAction)
        {
        case ODA_SELECT:
            if( pis->itemState & ODS_SELECTED )
            {
                if( pis->itemState & ODS_FOCUS 
#if(_WIN32_WINNT >= 0x0500)
                    && !(pis->itemState & ODS_NOFOCUSRECT)  // только W2000
#endif                  
                )
                    hbm = phBm[BM_DOWN_FOCUS];  // вдавленная с фокусом
                else
                    hbm = phBm[BM_DOWN];        // вдавленная без фокуса
            }
            break;
        case ODA_DRAWENTIRE:
            if( pis->itemState & ODS_DISABLED )
                hbm = phBm[BM_DISABLE];         // неактивная
            break;
        case ODA_FOCUS:
            if( pis->itemState & ODS_FOCUS )
                hbm = phBm[BM_FOCUS];           // выпуклая с фокусом
            break;
        }
        // отрисуем внешний вид кнопки
        DrawState(
            pis->hDC, NULL, NULL, (LONG)hbm, 0, 
            0, 0, 0, 0, DST_BITMAP | DSS_NORMAL );

        return TRUE;
    }
    return FALSE;
}

Как видим, ничего сложного. Код распадается на две части: в первой на основе сведений о выполняемых действиях (itemAction) и текущем состоянии кнопки (itemState) производится выбор необходимого битмапа, во второй части происходит вывод выбранного битмапа в контекст кнопки.
Ранний вариант приведенного выше кода содержал вместо вызова DrawState() следующий фрагмент.
        HDC hCompDC = CreateCompatibleDC( pis->hDC );
        hOld = (HBITMAP) SelectObject( hCompDC, hbm );
        BitBlt(
            pis->hDC,
            pis->rcItem.left, 
            pis->rcItem.top,
            pis->rcItem.right - pis->rcItem.left,
            pis->rcItem.bottom - pis->rcItem.top,
            hCompDC, 0, 0, SRCCOPY );
        SelectObject( pis->hDC, hOld );
        DeleteDC( hCompDC );
На мой взгляд оба варианта равноценны с точки зрения функциональности, но все же во фрагменте с BitBlt() больше возможностей допустить ошибку, приводящую к утечке ресурсов.
Код обрамляется проверкой на необходимый идентификатор контрола, поскольку в рабочей программе подобных контролов может быть несколько.

Внимательный читатель готов задать вопрос о том, что в самом начале упоминались не только механизмы (реализованные, как мы выяснили, через сообщения WM_MEASUREITEM и WM_DRAWITEM), но и API?

Действительно, имеется несколько функций, облегчающих придание стандартного вида OWNERDRAW-контролам. Разработчик готовит только основной битмап для кнопки, а для отрисовки границ и состояний кнопки (неактивное и в фокусе) пользуется функциями WinAPI - DrawEdge() (границы контрола - "выпуклый/вдавленный"), DrawState() (состояние "активный/неактивный") и DrawFocusRect() (состояние "в фокусе"). В таком случае вышеприведенный код примет вид:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBm = NULL;
    . . .
    case WM_INITDIALOG:
        . . .
        LoadBitmap(&hBm);
        . . .
    case WM_DRAWITEM:
        return DrawClassicStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
    . . .
}

BOOL DrawClassicStyleBtn(LPDRAWITEMSTRUCT pis, HBITMAP hBm, int deflate = 4)
{
    UINT uState = DSS_NORMAL;    // активная
    UINT uEdge  = EDGE_RAISED;   // выпуклая
    int  x = 0, y = 0;
    BOOL bFocus = FALSE;         // без фокуса
    RECT rFocus;

    if(IDC_BMPBTN == pis->CtlID)
    {
        switch(pis->itemAction)
        {
        case ODA_SELECT:
            if(pis->itemState & ODS_SELECTED) 
            {
                x += 1; // смещение битмапа вниз-право
                y += 1; // создает эффект нажатия кнопки
                uEdge = EDGE_SUNKEN; // вдавленная граница
            }
        case ODA_DRAWENTIRE:
            if(pis->itemState & ODS_DISABLED) 
            {
                uState = DSS_DISABLED; // неактивная
            }
        case ODA_FOCUS:
            if(pis->itemState & ODS_FOCUS)
            {
                memcpy(&rFocus, &pis->rcItem, sizeof(RECT));
                rFocus.left   += deflate;
                rFocus.top    += deflate;
                rFocus.right  -= deflate;
                rFocus.bottom -= deflate;
                bFocus = TRUE; // кнопка получила фокус
#if(_WIN32_WINNT >= 0x0500)
                if( pis->itemState &ODS_NOFOCUSRECT )  // только W2000
                    bFocus = FALSE;
#endif
            }
            break;
        }

        // отрисовка битмапа
        DrawState(
            pis->hDC, NULL, NULL, (LONG)hBm, 0, 
            x, y, 0, 0, DST_BITMAP | uState);

        // отрисовка границы
        DrawEdge(pis->hDC, &pis->rcItem, uEdge, BF_RECT);

        // отрисовка прямоугольника фокуса
        if( bFocus )
            DrawFocusRect(pis->hDC, &rFocus);
            
        return TRUE;
    }
    return FALSE;
}

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

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


Автор благодарит Александра Шаргина за информацию об обнаружении эффекта отсутствия WM_MEASUREITEM для ownerdrawn-кнопок.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 4    Оценка 45        Оценить