В этот раз статья Андрея Коновалова о работе с графикой в .NET Compact Framework.

Вступление

Как известно, на Windows Mobile устройствах существует возможность смены цветовой схемы. В случае, если приложение не использует графические элементы, достаточно воспользоваться набором цветов, предоставляемых классом SystemColors, чтобы приложение соответствовало текущей схеме. Из наиболее часто используемых имеет смысл отметить ActiveCaption, ActiveCaptionText, InactiveCaption, InactiveCaptionText, WindowText и.т.д. Также не стоит забывать про класс SystemBrushes, в котором представлены готовые для работы кисти — нет необходимости вызывать конструкторы и т.д.

Но что делать, когда есть набор изображений, которые должны соответствовать текущей цветовой схеме? Неужели делать набор картинок под все основные цвета?

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

Итак, что же остаётся? Очевидно, необходимо каким-то способом трансформировать базовое изображение «на лету». Известно, что самая главная компонента цветовой схемы содержится в реестре по адресу HKLM\Software\Microsoft\Color, в DWORD переменной BaseHue. В случае, если значение находится в диапазоне от 0 до 255, то у нас градации серого. От 256 до 510 — основная радуга 🙂 Опытным путём было установлено, что различные темы частенько кладут в эту переменную «что попало», т.е. значение, существенно превышающее диапазон 0..510. В итоге, чтобы получить честный BaseHue, воспользуемся следующей функцией:

private const String BASEHUE_PATH = “HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Color”;

public static int GetBaseHue()
{
object baseHue = Registry.GetValue(BASEHUE_PATH, “BaseHue”, 0);
int bh = baseHue == null ? 0: (int)baseHue;

if (bh < 255) return bh; else return (bh & 0xFF) + 255; } [/sourcecode] Про реальный смысл значения BaseHue можно почитать тут: HSL color space.

Если коротко, то при значении BaseHue от 0 до 255 у нас градации серого, значит saturation должен быть 0 (т.е. гарантированно grayscale изображение). В случае с диапазоном от 256 до 510, saturation уже идёт на наше усмотрение, по желанию. Меня устраивает и 255, т.е. максимально цветное изображение. Сейчас поясню, причём тут saturation.

Всё дело в том, что изображение у нас хранится в RGB модели, а BaseHue к RGB никакого отношения не имеет. В итоге получается, что есть необходимость произвести RGB -> HSL преобразование для получения возможности «раскраски» базового изображения, а потом обратное HSL -> RGB преобразование, чтобы получить уже реальные цвета для пикселей.

Применение на примере кнопки с чекбоксом

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

Рис. 1 - Оригинал
Рис. 1 - Оригинал

Первая трансформация — просто сделаем так, чтобы вся кнопка ушла в указанный BaseHue (в моём случае 391).

Рис. 2 - Произведено преобразование
Рис. 2 - Произведено преобразование

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

Рис. 3 - «Прозрачные» пиксели восстановлены
Рис. 3 - «Прозрачные» пиксели восстановлены

Да, с прозрачностью теперь всё хорошо, но больно уж некрасивыми остались галочка и бокс под ней. Добавим ещё справедливости:

Рис. 4 - Восстановлена зона чек-бокса
Рис. 4 - Восстановлена зона чек-бокса

Вот этот проход, пожалуй, не такой простой, как предыдущий. Как же это было сделано?

Если приглядеться, то совершенно очевидно, что изображение кнопки, свободное от чекбокса, в общем-то, повторяется (кроме угловых скруглений). И ровно такая же подложка находится под чек-боксом. Какой вывод? У нас есть возможность сравнивать «пустой» фон с частью, где поверх этого фона лежит чек-бокс и в случае, если расхождение в R, G или B более чем некая константа (путём простого перебора мне подошло число 25), то в раскрашенной картинке можно заменить пиксель на пиксель из оригинала.

А вот пример того, если попробовать не использовать порог, а вырезать оригинал «в лоб»:

Рис. 5 - Восстановлена зона чек-бокса без учёта порога
Рис. 5 - Восстановлена зона чек-бокса без учёта порога

Немного кода

Теперь о тонкостях реализации. В Compact Framework нет ни слова про RGB <-> HSL. Гугление достаточно быстро решило вопрос с преобразованиями — RGB <-> HSL. Но не сразу решило вопрос со скоростью преобразования. Как известно, managed код небыстр при работе с графикой, т.к. GetPixel жутко тормозит. Но и для этого решение было найдено. В MSDN-блоге про Windows Mobile обнаружился отличный пост про UnsafeBitmap для оперативных манипуляций с пикселями.

Ниже представлена функция, которая достаточно быстро раскрашивает изображение по указанным hue, saturation, brigtness, используя UnsafeBitmap:

public static Bitmap ApplyHueSaturation(Bitmap input, int hue, int sat, int brightness)
{
if (input == null)
return null;

ColorHandler.RGB rgb;
ColorHandler.HSV hsv;
UnsafeBitmap ibmp = new UnsafeBitmap(input);
UnsafeBitmap obmp = new UnsafeBitmap(new Bitmap(input.Width, input.Height));

ibmp.LockBitmap();
obmp.LockBitmap();

for (int y = 0; y < input.Height; y++) { for (int x = 0; x < input.Width; x++) { UnsafeBitmap.PixelData c = ibmp.GetPixel(x, y); rgb.Red = c.red; rgb.Blue = c.blue; rgb.Green = c.green; hsv = ColorHandler.RGBtoHSV(rgb); hsv.Hue = hue; hsv.Saturation = sat; hsv.value += brightness; if (hsv.value > 255)
hsv.value = 255;
if (hsv.value < 0) hsv.value = 0; ColorHandler.RGB r = ColorHandler.HSVtoRGB(hsv); obmp.SetPixel(x, y, (byte)r.Red, (byte)r.Green, (byte)r.Blue); } } obmp.UnlockBitmap(); ibmp.UnlockBitmap(); return obmp.Bitmap; } [/sourcecode] Работающий пример можно скачать здесь.

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

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

UPD: Недостаточно адаптировать изображения под текущую схему, необходимо также правильно их вывести. Скруглённые края могут остаться с фиолетовыми углами! 🙂 Читайте продолжение цикла про работу с графикой в Compact Framework.

Оригинал на Хабре.

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

Leave a Reply

Your email address will not be published. Required fields are marked *

M.

Mobile Application Blocks new Community Drop (05-21-2009) доступен для скачивания

На CodePlex доступна новая версия библиотеки Mobile Application Blocks.

Из нововведений:

  • Поддержка SQL Server CE 3.5 SP1
  • Утилита ConfigSectionEncrypt для шифровки конфигов
  • Проекты для .NET CF 3.5 и 2.0
  • Улучшения в ContainerModel Block
К.

Как получить изображение с камеры в Windows Mobile

Итак, камера… Windows Mobile версии 5 и выше предоставляет API для работы с камерой мобильного телефона. Что значит “для работы”? А значит это то, что кроме возможности получения изображения, которая будет рассмотрена в этом посте, предоставляемое API позволяет, также, осуществить захват видео любого формата, поддерживаемого Windows Media Player. Все эти радости жизни доступны посредством функции

HRESULT SHCameraCapture (PSHCAMERACAPTURE pshcc);


Документация по функции SHCameraCapture доступна в MSDN здесь, а описание структуры SHCAMERACAPTURE, которая передается в параметр функции, доступно здесь.
А сейчас мы рассмотрим небольшой пример использования выше указанной функции для получения изображения в формате JPG с камеры мобильного устройства. Ниже представлен код функции, которая отображает диалог для работы с камерой, который позволяет сохранить изображение с камеры в файл. После успешного заврешения работы диалога параметр result будет содержать путь к файлу с изображением.

static bool DoCaptureImage(LPCTSTR captureDir, LPCTSTR defaultFileName, 
						   LPTSTR result, DWORD resultSize, HWND parent)
{
	do
	{
		HRESULT         hResult;
		SHCAMERACAPTURE shcc;

		// Заполняем поля структуры SHCAMERACAPTURE.
		ZeroMemory(&shcc, sizeof(shcc));
		shcc.cbSize				= sizeof(shcc);
		shcc.hwndOwner			= parent;
		shcc.pszInitialDir		= captureDir;
		shcc.pszDefaultFileName	= defaultFileName;
		shcc.pszTitle			= TEXT("Capture Image");
		shcc.nResolutionWidth	= 0;
		shcc.nResolutionHeight	= 0;
		shcc.nVideoTimeLimit	= 0;
		shcc.Mode				= CAMERACAPTURE_MODE_STILL;
		shcc.StillQuality		= CAMERACAPTURE_STILLQUALITY_HIGH;

		// Отображаем диалог для работы с камерой
		hResult = SHCameraCapture(&shcc);
		if(hResult != S_OK) break;
		StringCchCopy(result, resultSize, shcc.szFile);
		return true;
	}
	while(false);
	return false;
}

Идем дальше… Теперь рассмотрим, как все описанное выше можно поиспользовать с wxWidgets и получить изображение в виде переменной wxImage:

static wxImage CaptureImage(WXWidget parent = NULL)
{
	do
	{
		// Создаем временное имя файла
		wxString tempFileName = wxFileName::CreateTempFileName(wxT("capture_"));
		// Т.к. wxWidgets по умолчанию добавляет расширение .tmp, 
		// а нам надо бы .jpg, то добавляем расширение руками
		tempFileName += wxT(".jpg");

		// Копируем путь к каталогу с временным файлом в буфер 
		TCHAR captureDir[MAX_PATH];
		StringCchCopy(captureDir, MAX_PATH, wxPathOnly(tempFileName).GetData());

		// Копируем имя файла в буфер
		TCHAR defaultFileName[MAX_PATH];
		StringCchCopy(defaultFileName, MAX_PATH, wxFileName(tempFileName).GetFullName());

		TCHAR fileNameBuffer[MAX_PATH];

		// Если ошибка то выходим...
		if (!DoCaptureImage(captureDir, defaultFileName, 
			fileNameBuffer, MAX_PATH, (HWND)parent)) break;
		// Получаем имя файла в переменную wxString
		wxString resultFileName(fileNameBuffer);
		// Загружаем отснятое изображение
		wxImage result(resultFileName, wxBITMAP_TYPE_ANY);
		// Удаляем временный файл
		if(wxFileExists(resultFileName)) wxRemoveFile(resultFileName);
		// Если изображение было загружено неудачно, то возвращаем false
		if(!result.IsOk()) break;
		// Если все нормально, то возвращаем true
		return result;
	}
	while(false);
	return wxNullImage;
}

Вот, такой нехитрый финт ушами позволяет нам получить изображение в виде переменной wxImage, которую можно использовать в GUI-приложении, например как-то так:

void wxCameraTestWMMainFrame::OnOPENClick( wxCommandEvent& event )
{
	wxImage capturedImage = 
		wxWMCameraCapture::CaptureImage(GetHandle());
	if(capturedImage.IsOk())
	{
		double controlScale = 
			(double)m_StaticBitmap->GetClientSize().GetHeight()/
			(double)m_StaticBitmap->GetClientSize().GetWidth();
		double imageScale = 
			(double)capturedImage.GetHeight() / 
			(double)capturedImage.GetWidth();
		double zoomLevel(1.0);
		if(imageScale > controlScale)
		{
			zoomLevel = (double)m_StaticBitmap->GetSize().GetHeight() / (double)capturedImage.GetHeight();
		}
		else
		{
			zoomLevel = (double)m_StaticBitmap->GetSize().GetWidth() / (double)capturedImage.GetWidth();
		}
		wxImage scaledImage = capturedImage.Scale(capturedImage.GetWidth() * zoomLevel,
			capturedImage.GetHeight() * zoomLevel);
		m_StaticBitmap->SetBitmap(wxBitmap(scaledImage));
	}
}

The конец.

Исходный код примера и проект для Visual Studio 2008 можно скачать здесь:
Пример получения изображения с камеры под Windows Mobile