Pull to refresh

Compact Framework: адаптируем графику приложения под текущую цветовую схему

Reading time5 min
Views1K

Вступление


Как известно, на 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;
}


* This source code was highlighted with Source Code Highlighter.


Про реальный смысл значения BaseHue можно почитать тут: HSL color space.

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

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

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


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

Original button
Рис.1 Оригинал

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

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

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

Button - hue applied, transparency restored
Рис.3 «Прозрачные» пиксели восстановлены

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

Button - hue applied, transparency restored, original check restored
Рис.4 Восстановлена зона чек-бокса

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

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

А вот пример того, если попробовать не использовать порог, а вырезать оригинал «в лоб»:
Button - hue applied, transparency restored, original check restored w/o level
Рис.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;
}


* This source code was highlighted with Source Code Highlighter.

Работающий пример можно скачать здесь.

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

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

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

Articles