Статья Изображения в ассортименте

Admin

Администратор
// I. Что такое GDI+

Я решил рассказать тебе о том, как удобно обрабатывать изображения в Windows. Например, нужно тебе нарисовать изображение из файла. Я думаю, не самый лучший способ убить время - кодить жуткую последовательность из CreateFile, CreateFileMapping, MapViewOfFile, CreateBitmapIndirect и SelectObject. К тому же, запутаться будет проще простого. Есть более простое решение - библиотека GDI+ от нашего старого знакомого дяди Билла. Она экспортирует классы (точнее, не совсем классы - экспортирует она так называемые Flat API, а в хидерах описаны классы, все функции которых строятся на этих flat API. Весьма интересный способ), с помощью которых легко можно загрузить/отобразить/модифицировать/сохранить изображение. А самое приятное - GDI+ входит в стандартную поставку Windows 98/Me, Windows XP, Windows 2000, Windows NT 4.0 SP6 (открой %WINDIR% и поищи поиском файлик gdiplus.dll). Заголовки и библиотека импорта для нее есть в Microsoft Visual Studio 2005, а в VC++ 6.0 нет (у меня, по крайней мере :)), поэтому я прилагаю к статье все хидеры и библиотеку импорта. Все это добро скармливается компилеру и линкеру VC++ 6.0 (правда, перед включением хидеров нужно будет написать typedef ULONG *ULONG_PTR).

// II. С чем его едят

А едят его очень просто :). Сначала нужно инициализировать GDI+ следующими строками кода:
Код:
// переменные для работы с GDI+
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR           gdiplusToken;
// Инициализируем GDI+
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

В конце нужно его отрубить такой строкой:
Код:
GdiplusShutdown(gdiplusToken);

Ну а работать с GDI проще простого. Сначала создаем объект класса Graphics (это некий прототип HDC из GDI. Как написано в MSDN, класс Graphics предоставляет методы для рисования линий, кривых, фигур, изображений и текста):
Код:
Graphics graph(hdc); // создаем объект на базе контекста устройства, чей хендл hdc

Хотим нарисовать закрашенный прямоугольник - нет проблем:
Код:
SolibBrush redbrush(Color(255, 0, 0));
graph.FillRectangle(&redbrush, 0, 0, 100, 100);

Хотим картинку загрузить - пожалуйста:
Код:
Image img(L"E:\\Windows\\winnt.bmp");
graph.DrawImage(&img, 0, 0); // последние два параметра - координаты X,Y точки, с которой следует вывести заветное изображение

Оказывается, его так же просто можно и сохранить. Для этого есть метод Save класса Image, которому можно передать 3 аргумента - имя файла (в юникоде, GDI+, к сожалению, не понимает ANSI), Class ID требуемого кодека (ведь можно загрузить файл в одном формате,а сохранять в другом) и параметры кодека (например, для jpeg можно указать степень сжатия). Получить ClSID кодека по его Mime-типу (например, image/png) можно с помощью функции GetEncoderClassid, код которой есть в исходнике к статье и который я честно переписал из MSDN (он получает список всех кодеков вызовом GetImageEncoders и просто ищет в нем нужный).

// III. Вьювер изображений

Чтобы просто так не играться с GDI+, напишем что-нибудь полезное, например, вьювер изображений. Конечно, до ACDSee или Ifran View мы не дотянем, да и у нас нет такой цели.
Писать будем на чистом API, поклонников MFC просьба удалиться (я считаю, что научиться вызывать GetDlgItemText вместо UpdateData и DialogBoxParam вместо DoModal несложно, зато пользы от этого будет немало в виде быстродействия и сокращения размеров экзешника).
Примерный скелет программы на API с показом окошка, я думаю, тебе знаком:
Код:
WinMain()
{
готовим WNDCLASSEX
вызываем CreateWindow
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}

WndProc()
{
обрабатываем сообщения от Windows
}


Нам понядобятся глобальные переменные для работы GDI+:
[CODE]GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR           gdiplusToken;


Перед вызовом CreateWindow стартуем GDI+, а после получения WM_QUIT - завершаем:

Код:
// Инициализируем GDI+
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

// Создаем окно
if(!(hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL)))
{
MessageBox(0, "Unable to create window", 0, MB_ICONERROR);
return FALSE;
}

< ... >

while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

GdiplusShutdown(gdiplusToken);

return (int)msg.wParam;

В самом вьювере реализуем следующие фичи:
- полноэкранный режим. Где ты видел нормальный вьювер без полноэкранного режима? :)
- авто-сжатие изображения до размеров окна (опционально). Тоже необходимая штука
- zoom и сохранение зум-множителя при открытии нового изображения (опционально)
- поворот изображения (90/180/270 градусов (не алкогольных, а угловых:)))
- переход к следующему/предыдущему файлу при повороте колеса мыши. Очень удобная вешь
- Drag & Drop
- анимация при показе/уничтожении окна. Так красивее :)

Разберем каждую фичу отдельно.
1) Полноэкранный режим.
Реализовывать будем самым банальным образом, как это обычно делается в Windows - создается окно со стилями (или ставятся существующему) WS_EX_TOPMOST, WS_POPUP|WS_VISIBLE (я еще добавил WS_EX_APPWINDOW и WS_SYSMENU), ему ставятся размеры, равные размерам экрана и оно закрашивается нужным цветом фона. Например, если дизассемблировать стандартную виндовую заставку logon.scr, можно увидеть следующий код (я перевел на С++ для удобства):
Код:
WNDCLASS wc;
.....
RegisterClassW(&wc);
/* вот это нам интересно */
CreateWindowExW(WS_EX_TOPMOST, lpClassName, lpWindowName, WS_POPUP|WS_VISIBLE, ..... )
Поскольку у нас окно уже есть, будем делать так:
Код:
/* вход в фулскрин */
// Ставим стили
SetWindowLong(hWnd, GWL_STYLE, WS_POPUP|WS_VISIBLE|WS_SYSMENU);
SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_TOPMOST|WS_EX_APPWINDOW);
// Сохраняем положение
GetWindowRect(GetDesktopWindow(), &rt);
GetWindowPlacement(hWnd, &oldPlacement);
// Развертываем на весь экран
MoveWindow(hWnd, rt.left, rt.top, rt.right-rt.left, rt.bottom-rt.top, 1);

/* выход из фулскрина */
// Восстанавливаем стили
// WINDOW_STYLE и WINDOW_EXSTYLE - определенные мной константы со стилями окна. Они же задаются и в CreateWindowEx
SetWindowLong(hWnd, GWL_STYLE, WINDOW_STYLE|WS_VISIBLE); // WS_VISIBLE для того, чтобы винда правильно перерисовала назлежащие окна (иначе, на них так и останутся части изображения и будет очень некрасиво)
SetWindowLong(hWnd, GWL_EXSTYLE, WINDOW_EXSTYLE);
// Восстанавливаем положение окна
SetWindowPlacement(hWnd, &oldPlacement);
Еще я добавил в меню (и обработку клавиши M на клаве) пункт "Скрывать меню в полноэкранном режиме". Думаю, его действия поянсять не надо :)

2) и 3) Авто-сжатие до размеров окна. Позволим несчастному пользователю самому включать/выключать эту опцию :). Для этого в меню Options сделаем пункт Auto-shrink to fit, при нажатии на которой будем инвертировать галочку перед ним, а при перерисовке будем его проверять. Подробно останавливаться на галочке не буду :). А авто-сжатие реализуем простой проверкой - вмещается ли изображение или нет и соотвественно будем сжимать его. Чтобы его сжать, воспользуемся одним из перегруженных методов Graphics::DrawImage(Image*,Point*,int), который принимает в качестве параметров указатель на объект Image (что, собственно, будет и отображать), массив точек и длину массива - это координаты трех вершин параллелограмма, в котором нужно нарисовать изображение. Причем не каких попало, а именно левого верхнего, правого верхнего и левого нижнего углов. (число вершин должно быть равно трем, иначе метод вернет ошибку).
Код:
/* часть обработчика WM_PAINT */
int bAutoShrink =  GetMenuState(hOptionsMenu, IDM_AUTOSHRINK, MF_BYCOMMAND) & MF_CHECKED;
// Если картинка помещается на экране или юзеру не нужно авто-сжатие, отображаем как есть
if(
((signed)img->GetWidth() < rt.right && (signed)img->GetHeight() < rt.bottom) // картинка помещается
|| !bAutoShrink // не нужно сжатие
)
{
// Рисуем обычное изображение (с учетом зума)
Point pts[3] = { // три координаты параллелограма для растягивания изображения
Point((int)(rt.right/2 - img->GetWidth()*mult/2), (int)(rt.bottom/2 - img->GetHeight()*mult/2)),  // левыйверхний угол
Point((int)(rt.right/2 + img->GetWidth()*mult/2), (int)(rt.bottom/2 - img->GetHeight()*mult/2)),  // правый верхний угол
Point((int)(rt.right/2 - img->GetWidth()*mult/2), (int)(rt.bottom/2 + img->GetHeight()*mult/2))   // левыйнижний угол
};
if(gr->DrawImage(img, pts, 3)!=Ok)
DrawErrorString((bFullScreenMode)?hScreenDC:hdc, hWnd, 20, "Times New Roman", L"An error ocurred while displaying image");
}
else
{
// Вычисляем параметры картинки, чтобы она уместилась на экране полностью
double x_coeff = (double)img->GetWidth() / (double)rt.right;
double y_coeff = (double)img->GetHeight() / (double)rt.bottom;
double max_coeff = (x_coeff>y_coeff)?x_coeff:y_coeff; // выбираем наибольший коеффициент
// Вычисляем новую ширину и высоту изображения
int new_width  = (int) ((double)img->GetWidth() * mult / max_coeff);
int new_height = (int) ((double)img->GetHeight() * mult / max_coeff);
// Три координаты паралеллограма для рисования изображения
Point destination[3] = {
Point(rt.right/2 - new_width/2, rt.bottom/2 - new_height/2), // левый верхний угол
Point(rt.right/2 + new_width/2, rt.bottom/2 - new_height/2), // правый верхний угол
Point(rt.right/2 - new_width/2, rt.bottom/2 + new_height/2)  // левый нижний угол
};
// Рисуем сжатое изображение
if(gr->DrawImage(img, destination, 3)!=Ok)
DrawErrorString((bFullScreenMode)?hScreenDC:hdc, hWnd, 20, "Times New Roman", L"An error ocurred while displaying image");
}
Функция DrawErrorString(HDC hdc, HWND hWnd, int fontHeight, char* fontFace, LPWSTR error) (ее код найдешь в исходнике к статье, если хорошо поищешь :)) выводит ругань об ошибке на экран, если не удалось показать изображение (совсем забыл сказать, что Graphics::DrawImage() возвращает значение типа enum Gdiplus::Status, которое равно Ok, если отображение удалось :))

Заметь, что здесь уже реализован и зум - ширина и высота изображения умножается на множитель mult (который объявлен как double mult=1.0), который увеличивается/уменьшается при нажатии на клавиши + и -:
Код:
/* часть обработчика WM_CHAR */
else if(message == WM_CHAR)
{
char ch = (char)wParam;
// Зум
bool bRepaint = false;
if(ch == '+')
{
bRepaint = true;
mult *= zoom;
}
else if(ch == '-')
{
bRepaint = true;
mult /= zoom;
}

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

4) Поворот изображения. Для поворота нужно вызвать метод Image::RotateFlip и передать ему единственный параметр - значение типа enum Gdiplus::RotateFlipType, которое может быть таким: Rotate90FlipNone - поворот на 90 градусов по часовой стрелке, Rotate180FlipNone - поворот на 180 градусов или Rotate270FlipNone - поворот на 270 градусов по часовой стрелке. Я добавил и пункты в меню и обработку клавиш A,S,D - 270,180,90 градусов соответственно:
Код:
/* часть обработчика WM_CHAR */
// Поворот
else if(ch == 'a' || ch == 'A')
{
if(!img) // картинка не загружена
return MessageBox(hWnd, "Image is not loaded", "Image viewer", MB_ICONEXCLAMATION)?0:0; /*в любом случае возвращаем ноль*/
img->RotateFlip(Rotate270FlipNone);
bRepaint = true;
}
else if(ch == 's' || ch == 'S')
{
if(!img) // картинка не загружена
return MessageBox(hWnd, "Image is not loaded", "Image viewer", MB_ICONEXCLAMATION)?0:0; /*в любом случае возвращаем ноль*/
img->RotateFlip(Rotate180FlipNone);
bRepaint = true;
}
else if(ch == 'd' || ch == 'D')
{
if(!img) // картинка не загружена
return MessageBox(hWnd, "Image is not loaded", "Image viewer", MB_ICONEXCLAMATION)?0:0; /*в любом случае возвращаем ноль*/
img->RotateFlip(Rotate90FlipNone);
bRepaint = true;
}

5) Переход к соседнему файлу при повороте колеса мыши. Я думаю, ничего комментировать тут не надо, разве что подмечу, что при открытии файла мы записываем в глобальные переменные его имя и путь к содержащему его каталогу в переменные filename и path соответственно.
Код:
/* обработчик WM_MOUSEWHEEL */
// поворот колеса мыши. При повороте колеса будем проходить по каталогу вверх/вниз, как в продвинутых вьюверах
else if(message == WM_MOUSEWHEEL)
{
// Коэффициент поворота колеса. Число, кратное WHEEL_DELTA (120). Если оно >0 - поворот вверх, <0 - поворот вниз
int zDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA;
if(!img) // картинка не загружена
return 0;

// Имя предыдущего файла (если поворот вверх - мы открываем предыдущий файл)
char* prev = NULL;
// Флаг, указывающий на что, что предыдущий файл был текущим открытым файлом
//  (если поворот вниз и флаг установлен - открываем текущий файл)
bool bPrevIsOur = false;
// Устанавливаем текущий каталог, в котором лежит открытый файл
SetCurrentDirectory(path);
// Ищем первый файл
WIN32_FIND_DATA wfd = {0};
HANDLE hSearch = FindFirstFile("*.*",  &wfd);
if(hSearch == (HANDLE)-1)
return 0;
// Выбран ли пункт меню Options->Don't clear image zoom multiplier - сбрасываем в 1 множитель при открытии нового файла или нет
bool bSaveMult =  (bool)(GetMenuState(hOptionsMenu, IDM_SAVEMULT, MF_BYCOMMAND) & MF_CHECKED);
do
{
// Сравниваем расширение файла с заданными расширениями графических файлов
char* ext = strrchr(wfd.cFileName, '.');
if(!ext)
continue;
ext++;
// Не изображение - переходим к следующему файлу
if(!(!lstrcmpi(ext, "bmp") || !lstrcmpi(ext, "jpeg") || !lstrcmpi(ext, "jpg") || !lstrcmpi(ext, "gif") || !lstrcmpi(ext, "tiff")
|| !lstrcmpi(ext, "png")))
continue;
// Файл совпал с открытым
if(!lstrcmpi(wfd.cFileName, filename))
{
// Устанавливаем флаг
bPrevIsOur = true;
// Если хотят предыдущий файл - открываем его, обновляем окно и корректируем заголовок
if(prev && zDelta>0)
{
// открываем
if(!bSaveMult)
mult = 1.0;
delete img;
wchar_t wfile[1024];
MultiByteToWideChar(CP_ACP, 0, prev, -1, wfile, 1023);
img = new Image(wfile);
// обновляем окно
GetClientRect(hWnd, &rt);
InvalidateRect(hWnd, &rt, TRUE);
// ставим правильный заголовок окна
filename = prev;
char title[1024];
wsprintf(title, "%s\\%s - Image viewer", path, filename);
SetWindowText(hWnd, title);
return 0;
}
}
// Если предыдущий файл совпал с открытым и хотят следующий - открываем, обновляем окно и корректируем заголовок
else if(bPrevIsOur && zDelta<0)
{
if(!bSaveMult)
mult = 1.0;
delete img;
wchar_t wfile[1024];
MultiByteToWideChar(CP_ACP, 0, wfd.cFileName, -1, wfile, 1023);
img = new Image(wfile);
GetClientRect(hWnd, &rt);
InvalidateRect(hWnd, &rt, TRUE);
filename = strdup(wfd.cFileName);
char title[1024];
wsprintf(title, "%s\\%s - Image viewer", path, filename);
SetWindowText(hWnd, title);
return 0;
}
// Записываем новое значение имени предыдущего файла
if(prev)
free(prev);
prev = strdup(wfd.cFileName);
}
while(FindNextFile(hSearch, &wfd)); // пока есть необработанные файлы в каталоге
}
Вот такой вот некислый код наколбасил я почти с первого раза :).

6) Drag & Drop. Реализация проста как арбуз: если при вызове CreateWindowEx установить расширенный флаг WS_EX_ACCEPTFILES, то в окно можно будет перетаскивать файлы из эксплорера. Мы будем принимать файлы только поштучно (мы же не можем открыть 5 файлов сразу, правда?). При перетаскивании файлов винда посылает нашему окну сообщение WM_DROPFILES, передавая в параметрах хендл "упавших" файлов (я не виноват в таком обороте, Билл сам назвал эту операцию Drag & Drop - перетащить и уронить :)). По этому хендлу мы можем узнать о всех файлах, перетащенных в окно вызовом DragQueryFile(HDROP hDrop, int index, LPCTSTR file, int filelen). Если ей передать index=-1, то она вернет число файлов. Потом можно передать ей index=0 и указатель на строку, куда будет записано имя файла (включая путь к нему). Вот так это реализовано у меня:
Код:
/* обработчик WM_DROPFILES */
// обрабатываем Drag & Drop
else if(message == WM_DROPFILES)
{
HDROP hDrop = (HDROP)wParam;
// Получаем количество файлов
UINT nFiles = DragQueryFile(hDrop, -1, 0, 0);
if(nFiles == 1)
{
char file[1024];
// Информация о первом файле
DragQueryFile(hDrop, 0, file, 1023);
if(img)
delete img;
wchar_t wfile[1024];
MultiByteToWideChar(CP_ACP, 0, file, -1, wfile, 1023);
// Открываем
img = new Image(wfile);
GetClientRect(hWnd, &rt);
// обновляем вид
InvalidateRect(hWnd, &rt, TRUE);
filename = strdup(file);
filename = strrchr(file, '\\')+1;
path = strdup(file);
*strrchr(path, '\\')=0;
// изменяем заголовок окна
char title[1024];
wsprintf(title, "%s - Image viewer", file);
SetWindowText(hWnd, title);
return 0;
}
DragFinish(hDrop);
}

7) Ну и последний штрих - анимация окна. Реализуется вызовом AnimateWindow(HWND hwnd, DWORD dwTime, DWORD dwFlags) с передачей ей хендла окна, времени анимации в миллисекундах (я использую стандартные 200мс) и флагов, которые могут быть соответственно равны либо AW_BLEND|AW_ACTIVATE для показа окна или AW_BLEND|AW_SPOILER для скрытия.

Еще, при большом желании, в сорце можно найти код для сохранения файла.

// III. Outro
Я и так уже накатал на 19 килобайт, поэтому в заключении буду краток :). Эта статья далеко не претендует на полное описание возможностей GDI+, виндовых функций (я не маразматик, чтобы переписывать MSDN :)). Далее, не надо писать отзывы типа "все равно Ifran View лучше" - я не старался создать шедевр. А вообще, получилось не так уж и плохо, не правда ли?
И еще пара слов. Поскольку мы писали на чистом WinAPI, у меня размер экзешника составил 17,4 Кб (учитывая, что в нем есть ресурсы). Я указал оптимальные опции линкера (например, совмещение секций - /MERGE), но, надеюсь, у тебя получится еще меньше. Дерзай :)