Учебник

Теоретический материал.

Глава IV

  • Растровая графика

    В главе 1 мы рассмотрели технику создания простейших графических примитивов, таких как: точка, линия, прямоугольник, эллипс и т. п. Однако часто возникает не- обходимость вывести в окне растровое изображение, представленное bmp-файлом, которое состоит из набора точек, каждой из которых сопоставлен битовый образ. Так, для 24-битного изображения точка описывается 3 байтами, где каждый байт задает интенсивность соответствующего цвета — красного, зеленого и синего.

  • Функция BitBlt()

    Простейшим вариантом использования растрового изображения является включе- ние его в ресурс приложения. Для этого необходимо на этапе создания проекта им- портировать bmp-файл в проект как ресурс Bitmap в меню Project | Add Resource… | Bitmap | Import…. Полученному ресурсу автоматически присваивается иденти- фикатор IDB_BITMAP1.
    Когда приложение начинает работать, изображение загружается функцией
    LoadBitmap(), что обычно происходит при обработке сообщения о создании окна.
    
    HBITMAP WINAPI LoadBitmapW(HINSTANCE hInstance, LPCWSTR lpBitmapName); 
    				
    Первый параметр hInstance — дескриптор приложения, второй lpBitmapName — идентификатор битового ресурса, который необходимо привести к типу LPCWTSTR макросом MAKEINTRESOURCE(), например:
    
    
    hBitmap = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));			
    Функция возвращает дескриптор битового образа hBitmap. После этого можно получить информацию о загруженном изображении обращени- ем к функции GetObject():
    
    int WINAPI GetObjectW(HANDLE h, int c, LPVOID pv);
    				
    Функция принимает дескриптор загруженного изображения h, размер c и адрес структуры BITMAP pv. В этой структуре мы получаем информацию об изображении: struct BITMAP
    
    {
    LONG
    bmType;
    //тип
    LONG
    bmWidth;
    //ширина изображения в пикселах
    LONG
    bmHeight;
    //высота изображения в пикселах
    LONG bmWidthBytes;//число байтов в строке изображения WORD bmPlanes; //количество цветов
    WORD bmBitsPixel; //число битов отображения цвета
    LPVOID bmBits; //указатель на область памяти битового образа
    };
    				
    Возвращаемое значение функции — размер битового образа в байтах. Нас чаще всего интересует содержимое двух полей структуры BITMAP, в которых возвращаются размеры загруженного изображения в пикселях — bmWidth и bmHeight. После того как мы загрузили изображение и получили его дескриптор, необходимо создать в памяти контекст устройства memBit, совместимый с текущим контекстом устройства вывода hdc. Для решения этой задачи используется функция CreateCompatibleDC():
    
    HDC WINAPI CreateCompatibleDC(HDC hdc);
    				
    Далее контексту memBit вызовом функции SelectObject() ставится в соответствие битовый образ.
    ПРИМЕЧАНИЕ
    Контекст памяти образует в оперативной памяти область хранения изображения в том же виде, что и в контексте устройства вывода в окно.
    Вывод битового изображения осуществляется обращением к функции BitBlt():
    
    BOOL WINAPI BitBlt(HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop );
    				
    Функция имеет 9 параметров: hdcDest — контекст устройства приемника изображения; nXDest, nYDest — x, y-координата левого верхнего угла приемника; nWidth, nHeight — ширина и высота изображения; hdcSrc — контекст устройства источника изображения; nXSrc, nYSrc — x, y-координата левого верхнего угла источника; dwRop — код растровой операции. При обработке сообщения WM_PAINT получаем контекст устройства вывода hdc и вызываем функцию BitBlt(), где последним аргументом выбираем операцию SRCCOPY, которая осуществляет побитовое копирование изображения из устройст- ва-источника memBit в устройство-приемник hdc.
    
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, memBit, 0, 0, SRCCOPY);
    				
    Другие растровые операции рассмотрим позднее, а сейчас приведем оконную функцию рассмотренной задачи (листинг 4.1).
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    HBITMAP hBitmap; static HDC memBit; static BITMAP bm; switch (message)
    {
    case WM_CREATE:
    hBitmap = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1)); GetObject(hBitmap, sizeof(bm), &bm);
    hdc = GetDC(hWnd);
    memBit = CreateCompatibleDC(hdc); SelectObject(memBit, hBitmap); ReleaseDC(hWnd, hdc);
    break;
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, memBit, 0, 0, SRCCOPY); EndPaint(hWnd, &ps);
    break;
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    Листинг 4.1. Вывод в окне растрового изображения из ресурса приложения Результат работы программы показан на рис. 4.1. Размеры окна пришлось изменять "вручную", поскольку они никак не связаны с размерами изображения.

    Рис. 4.1. Вывод растрового изображения в окне


  • Вывод изображения в заданный прямоугольник

    Растровое изображение в предыдущей задаче можно было бы вывести по размеру созданного окна, растягивая или сжимая исходное изображение, но в этом случае нужно использовать другую функцию StretchBlt():
    
    BOOL WINAPI StretchBlt(HDC hdcDest, int nXDest, int nYDest, int nWidth,
    int nHeight, HDC hdcSrc, int nXSrc, int nYSrc,
    int nWidthSrc, int nHeightSrc, DWORD dwRop);
    				
    Отличием этой функции от BitBlt() является наличие двух дополнительных пара- метров nWidthSrc и nHeightSrc, которые определяют ширину и высоту изображе- ния-источника, остальные параметры совпадают. Функция StretchBlt() осуществляет вывод изображения из источника hdcSrc с левым верхним углом (nXSrc, nYSrc) шириной nWidthSrc и высотой nHeightSrc в прямоугольную область приемника hdcDest с левым верхним углом (nXDest, nXDest) шириной nWidth и высотой nHeight. В рассмотренную ранее задачу (листинг 4.1) добавим обработчик сообщения об изменении размеров окна:
    
    case WM_SIZE:
    sx = LOWORD(lParam); sy = HIWORD(lParam); break;
    				
    Статические переменные sx,sy объявим в оконной функции:
    
    static int sx, sy;
    				
    Заменим в сообщении WM_PAINT обращение к функции BitBlt() на функцию:
    
    StretchBlt(hdc,0,0,sx,sy,memBit,0,0,bm.bmWidth,bm.bmHeight,SRCCOPY);
    				
    Теперь при запуске приложения изображение будет подстраиваться под реальный размер окна.
    ПРИМЕЧАНИЕ
    Изменение размеров растрового изображения приводит к существенной потере каче- ства.
    Обычно поступают наоборот — подстраивают размер окна под размер изображе- ния. Это можно сделать, переопределив размер окна в сообщении WM_SIZE функци- ей MoveWindow(). Нужно только учесть, что ширина окна должна быть увеличена относительно изображения на ширину бордюра окна, а высота окна включает так- же высоту заголовка и меню: caption, menu, border. Эти значения можно получить обращением к функции GetSystemMetrics():
    
    int WINAPI GetSystemMetrics(int nIndex);
    с соответствующим значением индекса. caption = GetSystemMetrics(SM_CYCAPTION); menu = GetSystemMetrics(SM_CYMENU);
    border = GetSystemMetrics(SM_CXFIXEDFRAME);
    MoveWindow(hWnd, 0, 0, bm.bmWidth + 2*border, bm.bmHeight + caption + menu + border, TRUE);
    				
    ПРИМЕЧАНИЕ
    Если размер изображения превысит размер экрана, необходимо организовать про- крутку изображения "скроллинг".
  • Загрузка изображения из файла

    Не всегда бывает удобно и возможно помещать изображение в ресурс приложения, поэтому в листинге 4.2 рассмотрим задачу о загрузке растрового изображения из bmp-файла во время выполнения приложения. Листинг 4.2. Загрузка изображения из файла
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    static int caption, menu, border; static HDC memBit;
    static HBITMAP hBitmap; static BITMAP bm; switch (message)
    
    {
    case WM_CREATE:
    caption = GetSystemMetrics(SM_CYCAPTION); menu = GetSystemMetrics(SM_CYMENU);
    border = GetSystemMetrics(SM_CXFIXEDFRAME);
    hBitmap = (HBITMAP)LoadImage(NULL, _T("test.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION);
    if (hBitmap == NULL)
    {
    
    MessageBox(hWnd,_T("Файл не найден"),_T("Загрузка изображения"), MB_OK | MB_ICONHAND);
    DestroyWindow(hWnd); return 1;
    }
    GetObject(hBitmap, sizeof(bm), &bm); hdc = GetDC(hWnd);
    memBit = CreateCompatibleDC(hdc); SelectObject(memBit, hBitmap); ReleaseDC(hWnd, hdc);
    break; case WM_SIZE:
    MoveWindow(hWnd,100, 50, bm.bmWidth+2*border, bm.bmHeight + caption
    + menu + border, TRUE);
    break;
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break; case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, memBit, 0, 0, SRCCOPY); EndPaint(hWnd, &ps);
    break;
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    Все, что нам нужно для работы с изображением — это его дескриптор, который можно получить обращением к функции LoadImage():
    
    HANDLE WINAPI LoadImageW(HINSTANCE hinst, LPCWTSTR lpszName, INT uType, int cxDesired, int cyDesired, UINT fuLoad);
    				
    hinst — дескриптор приложения для загрузки изображения из ресурса, NULL — если изображение загружается из файла; lpszName — идентификатор изображения, если hinst != NULL; иначе имя bmp- файла; uType — тип загружаемого изображения: IMAGE_BITMAP, IMAGE_CURSOR, IMAGE_ICON; cxDesired — ширина в пикселах для иконки или курсора; cyDesired — высота в пикселах для иконки или курсора; fuLoad — флаги чтения изображения: LR_LOADFROMFILE –– bmp-файл; LR_CREATEDIBSECTION –– изображение считывается в память в совместимом с текущим дисплеем представлении. Возвращается дескриптор изображения, но, поскольку функция предназначена для чтения не только растровых изображений, а также иконок и курсоров, то для воз- вращаемого значения необходимо указывать явное преобразование типа, в нашем случае (HBITMAP). Предусмотрим случай, когда файл с изображением не найден в текущей папке, то- гда функция LoadImage() возвращает дескриптор, равный NULL. Выводим преду- преждающее сообщение функцией MessageBox() и завершаем работу DestroyWin- dow(). Сейчас подойдем к проблеме согласования размеров изображения и окна. Вместо того чтобы менять размер изображения, построим окно, исходя из размеров загру- женного изображения (в этом случае в функции CreateWindow() необходимо ука- зать нулевые размеры окна). Размер изображения мы получим обращением к функции GetObject(), но для по- строения окна необходимо учесть наличие заголовка окна, строки меню и бордюра. Эти размеры получим обращением к функции GetSystemMetrics(). После того как вся необходимая информация получена, можно изменить размер окна функцией MoveWindow() при обработке сообщения WM_SIZE.
    ПРИМЕЧАНИЕ
    Если при создании окна определить стиль рамки WS_DLGFRAME, пользователь не сможет изменить размер окна.
    Вывести изображение в окно можно функцией BitBlt(). Нетрудно добавить к этой программе диалоговое окно выбора файла, но эту задачу мы оставим для самостоятельной работы.
  • Растровые операции

    Функция BitBlt() может работать с 15 различными растровыми операциями, ко- торые осуществляются над каждой точкой изображения: SRCCOPY — копирование источника в приемник; SRCPAINT — побитовая операция "ИЛИ" (OR) с предыдущим изображением; SRCAND — побитовая операция "И" (AND) с предыдущим изображением; SRCINVERT — побитовая операция "Исключающее ИЛИ" (XOR) с предыдущим изображением; SRCERASE — побитовая операция "И" (AND) цвета источника с инвертирован- ным цветом приемника; NOTSRCCOPY — инвертированный цвет источника; NOTSRCERASE — инверсия цвета, полученного битовой операцией AND над цве- том источника и приемника; MERGECOPY — побитовая операция "И" (AND) цвета источника с текущей кистью; MERGEPAINT — побитовая операция "ИЛИ" (OR) инвертированного цвета источ- ника с цветом приемника; PATCOPY — область заполняется текущей кистью; PATPAINT — побитовая операция "ИЛИ" (OR) текущей кисти и инвертированно- го цвета источника, после чего побитовая операция "ИЛИ" (OR) с цветом при- емника; PATINVERT — побитовая операция "Исключающее ИЛИ" (XOR) для кисти и цве- та приемника; DSTINVERT — инвертирование цвета приемника; BLACKNESS — область заполняется черным цветом; WHITENESS — область заполняется белым цветом.

    Рис. 4.2. Вывод тестового изображения


    Для демонстрации эффектов, возникающих при различных сочетаниях растровых операций, рассмотрим простой пример (листинг 4.3), где мы на синем фоне выве- дем простое изображение с различными кодами операций, после чего применим к полученному образцу весь набор операций. На рис. 4.2 показан фрагмент окна вывода. Листинг 4.3. Тест растровых операций для функции BitBlt()
    
    DWORD Op[15] = {SRCCOPY, SRCPAINT, SRCAND, SRCINVERT, SRCERASE, NOTSRCCOPY, NOTSRCERASE, MERGECOPY, MERGEPAINT, PATCOPY, PATPAINT, PATINVERT, DSTINVERT, BLACKNESS, WHITENESS};
    TCHAR *Name_Op[15]={_T("SRCCOPY"), _T("SRCPAINT"), _T("SRCAND"),
    _T("SRCINVERT"),_T("SRCERASE"), _T("NOTSRCCOPY"), _T("NOTSRCERASE"),
    _T("MERGECOPY"), _T("MERGEPAINT"), _T("PATCOPY"), _T("PATPAINT"),
    _T("PATINVERT"), _T("DSTINVERT"), _T("BLACKNESS"), _T("WHITENESS")};
    ///////////////////////////////////////////////////////////////////////
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPa- ram)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    int i, j, x;
    static HFONT hFont; static HDC memDC;
    static HBITMAP hPicture; static BITMAP bm;
    switch (message)
    {
    case WM_CREATE:
    hPicture = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1)); GetObject(hPicture, sizeof(bm), &bm);
    hdc = GetDC(hWnd);
    memDC = CreateCompatibleDC(hdc); SelectObject(memDC, hPicture); ReleaseDC(hWnd, hdc);
    hFont = CreateFont(12,0,0,0,FW_NORMAL,0,0,0,DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("Times New Roman"));
    break; case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    image
    166
    
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps); SelectObject(hdc,hFont);
    for (i = x = 0; i < 15; i++, x += bm.bmWidth + 10)
    {
    Глава 4
    TextOut(hdc, x, 10, Name_Op[i], _tcslen(Name_Op[i])); BitBlt(hdc, x, 30, bm.bmWidth, bm.bmHeight, memDC, 0, 0, Op[i]); for (j = 0; j < 15; j++) BitBlt(hdc, x, 30 + (j + 1)*
    (bm.bmHeight+2), bm.bmWidth, bm.bmHeight, hdc, x, 30, Op[j]);
    }
    EndPaint(hWnd, &ps); break;
    case WM_DESTROY: DeleteDC(memDC); DeleteObject(hFont); PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    Набор изображений выводим в строку, последовательно перебирая коды растровых операций из массива Op. Для наглядности в заголовке таблицы выводим список операций из массива Name_Op, всего таких операций 15. Следующие строки получаются последовательным применением тех же операций, но в качестве источника используем выведенное в первой строке изображение. Здесь в качестве приемника и источника изображения используется одно и то же устройство hdc, отличающееся лишь координатами:
    
    BitBlt(hdc, x, 30 + (j + 1)*(bm.bmHeight+2), bm.bmWidth, bm.bmHeight,
    hdc, x, 30, Op[j]);
    				
    Так, координаты начала изображения первой строки таблицы (x,30), а текущей строки (x, 30 + (j + 1)*(bm.bmHeight + 2)), где j — ее индекс. Мы сдвинули начало вывода на 30 единиц, учитывая выведенную строку заголовка. Чтобы задать фон окна, мы создали синюю кисть и установили ее в свойствах клас- са окна до регистрации в системе:
    
    wcex.hbrBackground = CreateSolidBrush(RGB(0,0,255));
    
    				
  • Анимация

    Функция BitBlt() позволяет более эффективно использовать графическую систе- му. Можно не перерисовывать каждый раз окно, а изменять лишь небольшой фрагмент изображения, представленный в виде битового образа. На рис. 4.3 показан пример программы, которая демонстрирует движение упругого шарика в замкнутой области, ограниченной рамкой окна.

    Рис. 4.3. Движение абсолютно упругого шарика с отражением от стенок


    Для наглядности скорость движения будем задавать визуально, щелкая мышью в нужном направлении от центра окна, где изображен шарик. Величина скорости будет пропорциональна длине вектора. Изображение шарика создадим в редакторе ресурсов, для простоты выбирая изо- бражение заданного размера 32 32 пиксела. Фон картинки и окна выберем одина- ковым — белым. Текст оконной функции задачи приведен в листинге 4.4. Еще мы изменили иконку приложения, но ранее мы это уже проделывали и трудностей здесь возникнуть не должно. Листинг 4.4. Оконная функция задачи "Ping-pong"
    
    const int SPAN = 10;
    #include 
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPa- ram)
    {
    PAINTSTRUCT ps;
    HDC hdc; int mx, my;
    static double mod, vx, vy, xt, yt; static HDC memDC;
    
    
    static HPEN hpen;
    static int x, y, cx, cy, scrx, scry; static HBITMAP hCircle;
    static bool play; switch (message)
    {
    case WM_CREATE:
    hpen = CreatePen(PS_SOLID, 4, RGB(255, 0, 0));
    hCircle = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_CIRCLE)); hdc = GetDC(hWnd);
    memDC = CreateCompatibleDC(hdc); SelectObject(memDC, hCircle); ReleaseDC(hWnd, hdc);
    break; case WM_SIZE:
    scrx = LOWORD(lParam); scry = HIWORD(lParam); x = (cx = scrx/2) - 16; y = (cy = scry/2) - 16; break;
    case WM_LBUTTONDOWN:
    if (!play)
    { mx = LOWORD(lParam); my = HIWORD(lParam); vx = mx - cx;
    vy = my - cy;
    mod = sqrt(vx*vx+vy*vy); vx = vx/mod;
    vy = vy/mod;
    hdc = GetDC(hWnd); SelectObject(hdc, hpen); MoveToEx(hdc, cx, cy, 0); LineTo(hdc, mx, my);
    LineTo(hdc, mx - (vx - vy)*SPAN, my - (vy + vx)*SPAN); MoveToEx(hdc, mx - (vx + vy)*SPAN, my - (vy - vx)*SPAN, 0); LineTo(hdc, mx, my);
    ReleaseDC(hWnd, hdc); play = true;
    
    }
    break; case WM_TIMER:
    hdc = GetDC(hWnd);
    BitBlt(hdc, x, y, 32, 32, NULL, 0, 0, PATCOPY);
    
    
    if (x + 31 > scrx || x < 1) vx = -vx; if (y + 31 > scry || y < 1) vy = -vy; xt += vx*10;
    yt += vy*10;
    x = int(xt + 0.5); y = int(yt + 0.5);
    BitBlt(hdc, x, y, 32, 32, memDC, 0, 0, SRCCOPY); ReleaseDC(hWnd, hdc);
    break; case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_PLAY_BEGIN:
    SetTimer(hWnd, 1,(int)(sqrt(double(cx*cx+cy*cy))/mod)*10 , NULL); xt = x;
    yt = y;
    InvalidateRect(hWnd, NULL, TRUE); break;
    case ID_PLAY_END: KillTimer(hWnd, 1); x = cx - 16;
    y = cy - 16;
    InvalidateRect(hWnd, NULL, TRUE); play = false;
    break;
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    BitBlt(hdc, x, y, 32, 32, memDC, 0, 0, SRCCOPY); EndPaint(hWnd, &ps);
    break;
    case WM_DESTROY: DeleteDC(memDC); DeleteObject(hpen); PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    При обработке сообщения WM_CREATE создаем красное перо hpen для прорисовки вектора скорости:
    
    hpen = CreatePen(PS_SOLID, 4, RGB(255, 0, 0));
    				
    Загружаем изображение:
    
    hCircle = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_CIRCLE));
    				
    Создаем контекст устройства в памяти memDC и ставим ему в соответствие загру- женное изображение hCircle:
    
    hdc = GetDC(hWnd);
    memDC = CreateCompatibleDC(hdc); SelectObject(memDC, hCircle); ReleaseDC(hWnd, hdc);
    				
    Начальное значение логической переменной play = false — игра еще не нача- лась. В сообщении WM_SIZE вычисляем координаты x и y для вывода изображения шарика по центру окна, попутно сохраняя координаты центра окна в переменных cx и cy:
    
    scrx = LOWORD(lParam); scry = HIWORD(lParam); x = (cx = scrx/2) - 16; y = (cy = scry/2) - 16;
    				
    Если скорость движения шарика еще не определена (переменная play == false), в сообщении WM_LBUTTONDOWN вычисляем размер вектора для ее определения. Век- тор строится от центра окна до позиции курсора:
    
    mx = LOWORD(lParam); my = HIWORD(lParam); vx = mx - cx;
    vy = my - cy;
    mod = sqrt(vx*vx+vy*vy);
    				
    Нам удобнее работать в относительных единицах, поэтому нормируем координаты вектора:
    
    vx = vx/mod; vy = vy/mod;
    				
    Мы не случайно определили переменные vx и vy как double, иначе, для целого типа, результатом этой операции был бы 0. Сейчас можно нарисовать вектор. Для этого получаем контекст устройства hdc, строим линию из центра окна в точку с позицией курсора:
    
    hdc = GetDC(hWnd); SelectObject(hdc, hpen); MoveToEx(hdc, cx, cy, 0); LineTo(hdc, mx, my);
    				
    и прорисовываем наконечник стрелки двумя маленькими линиями, размер которых определяется константой SPAN. Для того чтобы написать эти формулы, необходимы элементарные сведения из аналитической геометрии:
    
    LineTo(hdc, mx - (vx - vy)*SPAN, my - (vy + vx)*SPAN); MoveToEx(hdc, mx - (vx + vy)*SPAN, my - (vy - vx)*SPAN, 0); LineTo(hdc, mx, my);
    				
    После чего освобождаем контекст устройства:
    
    ReleaseDC(hWnd, hdc);
    				
    Теперь переменной play присваивается значение true для того, чтобы предотвра- тить переопределение скорости при случайном щелчке в области окна. Ритм движения шарика будет определяться таймером, который мы создадим при обработке пункта меню Begin с идентификатором ID_PLAY_BEGIN:
    
    SetTimer(hWnd, 1, (int)(sqrt(double(cx*cx+cy*cy))/mod)*10, NULL);
    				
    Третий параметр функции определяет частоту генерации сообщения WM_TIMER и, соответственно, скорость движения.
    ПРИМЕЧАНИЕ
    Нужно иметь в виду, что таймер не может отсчитать интервал времени меньше 58 мсек, поэтому мы подобрали еще и масштабный множитель для ускорения дви- жения.
    Скорость движения будет максимальной, если мы зададим максимальный размер вектора, щелкнув в углу окна. Начальные координаты шарика (x,y) поместим в переменные xt,yt типа double и инициируем перерисовку окна, объявив его недействительным. Перерисовка окна здесь необходима, чтобы убрать стрелку, показывающую на- правление и величину скорости. В сообщении WM_PAINT мы ограничиваемся лишь обращением к функции BitBlt() для вывода изображения шарика и перерисовы- ваем фон окна:
    
    BitBlt(hdc, x, y, 32, 32, memDC, 0, 0, SRCCOPY);
    				
    Вся работа по прорисовке движения шарика будет осуществляться при обработке сообщения WM_TIMER. Идея метода заключается в том, что мы в позицию "старого" положения шарика выводим цвет фона, используя растровую операцию PATCOPY:
    
    BitBlt(hdc, x, y, 32, 32, NULL, 0, 0, PATCOPY);
    				
    Таким образом, предыдущее изображение будет "затерто" текущей кистью, т. е. белым цветом. Теперь нужно решить вопрос — а не вышли ли мы за пределы окна, двигаясь в исходном направлении? Мы проверим отдельно x- и y-координаты:
    
    if (x + 31 > scrx || x < 1) vx = -vx; if (y + 31 > scry || y < 1) vy = -vy;
    				
    Если текущий шаг приводит к пересечению границы окна, меняем знак прираще- ния координаты vx или vy. Несложно убедиться, что отражение от стенки по зако- нам геометрической оптики требует именно такого преобразования. Теперь нара- щиваем координаты:
    
    xt += vx*10; yt += vy*10;	
    				
    Мы ввели масштабный коэффициент, равный 10, для увеличения скорости движе- ния. Результат округляем до целого:
    
    x = int(xt + 0.5); y = int(yt + 0.5);
    				
    и выводим изображение в новой позиции:
    
    BitBlt(hdc, x, y, 32, 32, memDC, 0, 0, SRCCOPY);	
    				
    Так мы обеспечили движение шарика и "отражение" от стенок окна. Для прекращения демонстрации предусмотрен пункт меню End с идентификато- ром ID_PLAY_END. При обработке этого сообщения уничтожаем таймер:
    
    KillTimer(hWnd, 1);
    				
    приводим переменные к исходному положению:
    
    x = cx - 16; y = cy - 16; play = false;
    				
    и перерисовываем окно, объявляя его недействительным. Теперь можно установить новое значение скорости и вновь запустить процесс. В сообщении WM_DESTROY перед завершением работы удаляем перо и контекст уст- ройства в памяти.
  • Функция PlgBlt()

    Основным недостатком функции BitBlt() является невозможность изменения размеров выводимого изображения и его деформации. Эту задачу решает функция PlgBlt(), появившаяся впервые в Windows NT, она осуществляет перенос точек из прямоугольника-источника в заданный параллелограмм-приемник:
    
    BOOL WINAPI PlgBlt(HDC hdcDest,CONST POINT *lpPoint,HDC hdcSrc,int nXSrc, int nYSrc,int nWidth,int nHeight,HBITMAP hbmMask,int xMask,int yMask);
    				
    hdcDest — дескриптор контекста устройства приемника; lpPoint — указатель на массив из трех углов параллелограмма; hdcSrc — дескриптор исходного устройства-источника; nXSrc, nYSrc — x-, y-координаты левого верхнего угла исходного прямо- угольника; nWidth,nHeight — ширина и высота исходного прямоугольника; hbmMask — дескриптор монохромного битового образа-маски; xMask, yMask — x,y-координаты левого верхнего угла маски. ПРИМЕЧАНИЕ При использовании файла-маски нужно учитывать, что это должно быть монохромное изображение такого же размера, что и выводимый рисунок. Точки рисунка, соответст- вующие черным точкам маски, не отображаются. Чтобы представить работу новой функции, напишем программу, которая выводит в окно изображение "как есть" и предоставляет возможность изменять координаты параллелограмма-приемника. Для наглядности будем изменять координаты трех его углов, "буксируя" их мышью в нужную позицию (очевидно, что для однознач- ного задания параллелограмма достаточно задать только три его угла). Результат работы программы изображен на рис. 4.4.

    Рис. 4.4. Деформация изображения функцией PlgBlt()


    Углы прямоугольника, задаваемые в массиве lpPoint, для наглядности выделили небольшими кружками. Текст оконной функции приведен в листинге 4.5. Демонстрационный bmp-файл мы импортируем в ресурсы приложения. Листинг 4.5. Демонстрационная задача для функции PlgBlt()
    
    		
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPa- ram)
    {
    PAINTSTRUCT ps;
    HDC hdc; RECT rt;
    int i, x, y, p, q; static int k;
    static bool Capture; static POINT pts[3]; static HDC memDC;
    static HBITMAP hPicture;
    
    
    static BITMAP bm; static HPEN hPen; switch (message)
    {
    Глава 4
    case WM_CREATE:
    hPicture = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_SPOOK)); GetObject(hPicture, sizeof(bm), &bm);
    hPen = CreatePen(PS_SOLID, 4, RGB(0, 0, 255));
    GetClientRect(hWnd, &rt);
    x = (rt.right - bm.bmWidth)/2; y = (rt.bottom - bm.bmHeight)/2; pts[0].x = pts[2].x = x;
    pts[0].y = pts[1].y = y; pts[1].x = x + bm.bmWidth; pts[2].y = y + bm.bmHeight; hdc = GetDC(hWnd);
    memDC = CreateCompatibleDC(hdc); SelectObject(memDC, hPicture); ReleaseDC(hWnd, hdc);
    break; case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_LBUTTONDOWN:
    x = LOWORD(lParam); y = HIWORD(lParam);
    for (k = 0; k < 3; k++)
    {
    p = x - pts[k].x;
    q = y - pts[k].y; if (p*p + q*q < 16)
    {
    SetCapture(hWnd); Capture = true; return 0;
    }
    }
    break;
    case WM_MOUSEMOVE:
    if (Capture)
    {
    image
    Растровая графика 175
    pts[k].x = LOWORD(lParam); pts[k].y = HIWORD(lParam); InvalidateRect(hWnd, NULL, TRUE);
    }
    break;
    case WM_LBUTTONUP:
    if (Capture)
    {
    ReleaseCapture(); Capture = false;
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    PlgBlt(hdc, pts, memDC, 0, 0, bm.bmWidth, bm.bmHeight, 0, 0, 0); SelectObject(hdc, (HPEN)hPen);
    for (i = 0; i < 3; i++)
    Ellipse(hdc, pts[i].x-4, pts[i].y-4, pts[i].x+4, pts[i].y+4); EndPaint(hWnd, &ps);
    break;
    case WM_DESTROY: DeleteDC(memDC); DeleteObject(hPen); PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    		
    При обработке сообщения WM_CREATE мы, как и в предыдущей задаче, загрузим картинку и сохраним ее дескриптор в статической переменной:
    
    hPicture = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_SPOOK));	
    				
    Определим параметры изображения, помещая их в структуру BITMAP bm:
    
     GetObject(hPicture, sizeof(bm), &bm);	
    				
    Нам нужно создать перо для выделения углов параллелограмма:
    
    hPen = CreatePen(PS_SOLID, 4, RGB(0, 0, 255));	
    				
    После этого определим размер клиентской области окна для вывода изображения по центру: GetClientRect(hWnd, &rt);
    
    x = (rt.right - bm.bmWidth)/2; y = (rt.bottom - bm.bmHeight)/2;			
    				
    Координаты x и y будут определять верхний левый угол изображения. ПРИМЕЧАНИЕ Хотя при обработке сообщения WM_CREATE окно еще не отображается, его размеры уже известны системе и могут быть получены функцией GetClientRect(). Вначале задаем прямоугольник с исходными размерами изображения, определяя массив pts, где 3 элемента типа POINT — координаты углов параллелограмма- приемника:
    
    pts[0].x = pts[2].x = x;
    pts[0].y = pts[1].y = y; pts[1].x = x + bm.bmWidth; pts[2].y = y + bm.bmHeight;
    				
    Теперь осталось получить контекст в памяти memDC, совместимый с контекстом окна, и связать его с загруженным изображением hPicture:
    
    memDC = CreateCompatibleDC(hdc);
    SelectObject(memDC, hPicture);
    				
    При обработке сообщения о нажатии левой кнопки мыши WM_LBUTTONDOWN мы долж- ны определить, указывает ли курсор мыши на угол параллелограмма и на какой именно. Поступим следующим образом — в цикле по трем точкам (углам параллело- грамма) измерим расстояние между координатами точек и указателем мыши:
    
    x = LOWORD(lParam); y = HIWORD(lParam);
    for (k = 0; k < 3; k++)
    { p = x - pts[k].x;
    q = y - pts[k].y; if (p*p + q*q < 16)
    				
    Если расстояние не превышает 4 пикселов (квадрат расстояния, соответственно, 16), считаем, что точка выбрана, производим "захват мыши", присваиваем пере- менной Capture значение true и завершаем обработку сообщения: SetCapture(hWnd); Capture = true; return 0; Можно было бы обойтись и без "захвата мыши", но тогда при выходе курсора за пределы окна оконная функция перестанет получать сообщения мыши, что вряд ли хорошо (мы уже обсуждали эту проблему при построении кривой Безье, лис- тинг 1.15). Функция SetCapture() обеспечит пересылку всех сообщений мыши приложению, "захватившему мышь", даже при выходе курсора за пределы окна, а возвращаемые координаты курсора мыши будут определяться относительно кли- ентской области окна. Однако необходимо помнить, что координаты могут быть отрицательными. ПРИМЕЧАНИЕ Прием с "захватом мыши" является стандартным в подобной ситуации. Рассмотрим обработку сообщения о перемещении мыши WM_MOUSEMOVE. Сначала мы должны определить, был ли "захвачен" один из углов параллелограмма. Если это не так, переменная Capture имеет значение false, и обработка сообщения пре- кращается. Если же угол параллелограмма "захвачен", считываем текущие коорди- наты мыши в соответствующий элемент массива pts (его индекс сохранился в ста- тической переменной k):
    
    pts[k].x = LOWORD(lParam); pts[k].y = HIWORD(lParam);
    				
    и инициируем перерисовку окна функцией InvalidateRect(). При сообщении об отпускании левой кнопки мыши WM_LBUTTONUP, если мышь была "захвачена", мы должны ее "освободить" функцией без параметров ReleaseCapture() и присвоить значение false логической переменной Capture. ПРИМЕЧАНИЕ Нужно очень осторожно обращаться с операцией "захвата мыши" и обязательно "ос- вобождать" мышь. Обычная практика такова — мышь "захватывается" по нажатию кнопки, а "освобождается" при ее отпускании. Осталось рассмотреть обработку сообщения WM_PAINT. Получаем контекст устрой- ства hdc и выводим изображение: PlgBlt(hdc, pts, memDC, 0, 0, bm.bmWidth, bm.bmHeight, 0, 0, 0); из контекста памяти memDC, начиная с левого верхнего угла (0,0), размером bm.bmWidth, bm.bmHeight, в параллелограмм с координатами, указанными в мас- сиве pts, на устройстве hdc. Маску изображения мы не использовали, поэтому три последних параметра функ- ции имеют нулевое значение. После чего выберем созданное ранее перо: SelectObject(hdc, (HPEN)hPen); и нарисуем три маленьких окружности радиуса 4 единицы вокруг углов параллело- грамма:
    
    for (i = 0; i < 3; i++)
    Ellipse(hdc, pts[i].x-4, pts[i].y-4, pts[i].x+4, pts[i].y+4);
    				
    Остальные сообщения в этой программе обрабатываются стандартным образом, нужно только не забыть освободить контекст в памяти memDC и перо hPen при за- вершении работы в сообщении WM_DESTROY. Функция MaskBlt() Таким же нововведением в Windows NT стала функция MaskBlt(), позволяющая маскировать одно изображение другим:
    
    BOOL WINAPI MaskBlt(HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, HBITMAP hbmMask, int xMask, int yMask, DWORD dwRop);
    				
    hdcDest — дескриптор контекста устройства приемника; nXDest, nYDest — x, y-координата левого верхнего угла приемника; nWidth, nHeight — ширина и высота изображения; hdcSrc — контекст устройства источника изображения; nXSrc, nYSrc — x, y-координата левого верхнего угла источника; hbmMask — дескриптор монохромного битового образа-маски; xMask, yMask — x, y-координаты левого верхнего угла маски; dwRop — код растровой операции, аналогичен функции BitBlt(). Полный набор растровых операций позволяет получить различные эффекты с ис- пользованием маскирующего изображения. Причем, в отличие от функции PlgBlt(), здесь изображение-маска накладывается на исходное изображение с соответст- вующим эффектом, определенным выбранной растровой операцией. ПРИМЕЧАНИЕ Функция MaskBlt() используется редко, поскольку практически все эффекты могут быть достигнуты в функции BitBlt(), к тому же, в ее реализации была сделана ошибка, и функция работает правильно только тогда, когда размер маски точно сов- падает с размером изображения. В качестве примера рассмотрим достаточно традиционную задачу (листинг 4.6 и рис. 4.5): отобразим в окне сигарету, а при нажатии левой кнопки мыши пере- черкнем изображение стандартным знаком Ø; при отпускании кнопки изображение должно восстанавливаться. Импортируем два изображения в ресурс приложения с идентификаторами IDB_SMOKES и IDB_NO. Листинг 4.6. Тест для функции MaskBlt()
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    static int x, y; static HDC memDC;
    static HBITMAP hSmokes, hNo; static BITMAP bm;
    switch (message)
    {
    case WM_CREATE:
    hNo = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_NO));
    hSmokes = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_SMOKES)); GetObject(hSmokes, sizeof(bm), &bm);
    hdc = GetDC(hWnd);
    memDC = CreateCompatibleDC(hdc);
    Растровая графика 179
    image
    
    SelectObject(memDC, hSmokes); ReleaseDC(hWnd, hdc);
    break; case WM_SIZE:
    x = (LOWORD(lParam) - bm.bmWidth)/2; y = (HIWORD(lParam) - bm.bmHeight)/2; break;
    case WM_LBUTTONDOWN:
    hdc = GetDC(hWnd); MaskBlt(hdc,x,y,bm.bmWidth,bm.bmHeight,memDC,0,0,hNo,0,0,SRCCOPY); ReleaseDC(hWnd, hdc);
    break;
    case WM_LBUTTONUP:
    InvalidateRect(hWnd, NULL, TRUE); break;
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, memDC, 0, 0, SRCCOPY); EndPaint(hWnd, &ps);
    break;
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    В оконной функции опишем переменные целого типа x,y для хранения координат вывода изображения по центру окна, контекст устройства в памяти memDC, а также дескрипторы изображения hSmokes и маски hNo. Переменная bm типа BITMAP необхо- дима для определения размеров загруженного изображения. При создании окна в сообщении WM_CREATE загрузим из ресурса изображение и мас- ку, получим их дескрипторы: hNo = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_NO)); hSmokes = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_SMOKES)); Получаем информацию об изображении: GetObject(hSmokes, sizeof(bm), (LPSTR)&bm); Создаем совместимый контекст устройства в памяти и выбираем для него загру- женное изображение:
    
    memDC = CreateCompatibleDC(hdc); SelectObject(memDC, hSmokes);
    				
    В сообщении WM_SIZE вычисляем координаты для вывода изображения:
    
    x = (LOWORD(lParam) - bm.bmWidth)/2; y = (HIWORD(lParam) - bm.bmHeight)/2;
    				
    Функцией BitBlt() прорисовываем изображение в сообщении WM_PAINT:
    
    BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, memDC, 0, 0, SRCCOPY);
    				
    А при нажатии левой кнопки мыши в сообщении WM_LBUTTONDOWN перерисуем изо- бражение, наложив на него маску (см. рис. 4.5):
    
    MaskBlt(hdc,x,y,bm.bmWidth,bm.bmHeight,memDC,0,0,hNo,0,0,SRCCOPY);
    				
    Здесь мы выбрали растровую операцию копирования. При отпускании кнопки мыши восстановим исходное изображение, перерисовывая окно.