Еще одна интересная статья от Андрея Коновалова. В статье рассмотрены особенности реализации отрисовки изображений с прозрачностью при использовании .NET Compact Framework.

Вступление

К большому сожалению разработчиков, Compact Framework, да и native-функции тоже, не поддерживают альфа-канал с разной прозрачностью у индивидуальных пикселей. Это означает, что нет возможности создавать красоту неописуемую с плавными переходами между изображениями. Однако, что же делать, если хочется иметь хотя бы подобие “полного” альфа-канала, а именно, выводить полупрозрачные изображения, у которых есть ещё и полностью прозрачные участки?

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

Способ №1. Фиксированый цвет является прозрачным

public static void DrawImageTransparent(Graphics g, 
  Bitmap b, Point location, Color transColor)
{
  if (b == null || g == null)
    return;

  ImageAttributes attrib = new ImageAttributes();
  attrib.SetColorKey(transColor, transColor);

  Rectangle destRect = new Rectangle(location.X, location.Y, b.Width, b.Height);
 
  g.DrawImage(b, destRect, 0, 0, b.Width, b.Height, GraphicsUnit.Pixel, attrib);
}

Стоит заметить, что только эта хитрая разновидность DrawImage позволяет выводить изображение с указанным ColorKey, по которому определяется, какие пиксели не рисовать. Шикарный набор параметров, не находите? 🙂 Куда рисовать, мы задаём через Rectange, а откуда — через 4 параметра. Ну это я так, лирическое отступление в сторону Microsoft.

Собственно, именно DrawImageTransparent и есть основной способ рисования изображений с прозрачными пикселями. Однако минус этого способа очевиден, состояния прозрачности всего два: полностью прозрачно и совсем непрозрачно.

Пример:

Compact Framework - Грани прозрачности
На самом деле, вполне неплохо, можно на этом и остановиться. Но хочется-то большего 🙂

Способ №2. У всего изображения фиксированный коэффициент непрозрачности

В этом случае без DllImport уже не обойтись, приготовим всё, что для этого необходимо:

public struct BlendFunction
{
  public byte BlendOp;
  public byte BlendFlags;
  public byte SourceConstantAlpha;
  public byte AlphaFormat;
}

public enum BlendOperation : byte
{
  AC_SRC_OVER = 0x00
}

public enum BlendFlags : byte
{
  Zero = 0x00
}

public enum SourceConstantAlpha : byte
{
  Transparent = 0x00,
  Opaque = 0xFF
}

public enum AlphaFormat : byte
{
  AC_SRC_ALPHA = 0x01
}

public class PlatformAPI
{
  [DllImport("coredll.dll")]
  extern public static Int32 AlphaBlend(IntPtr hdcDest, 
    Int32 xDest, Int32 yDest, Int32 cxDest, Int32 cyDest, 
    IntPtr hdcSrc, Int32 xSrc, Int32 ySrc, Int32 cxSrc, 
    Int32 cySrc, BlendFunction blendFunction);        
}

Как видно, обрезано всё, что только можно обрезать — в enum-ах по одному параметру и т.д. Но тем не менее, продолжаем. Собственно, наша функция:

public static void DrawAlpha(Graphics g, Bitmap b, Point location, byte opacity)
{
  if (b == null || g == null)
    return;

  using (Graphics gxSrc = Graphics.FromImage(g))
  {
    IntPtr hdcDst = g.GetHdc();
    IntPtr hdcSrc = gxSrc.GetHdc();
    BlendFunction blendFunction = new BlendFunction();
    blendFunction.BlendOp = (byte)BlendOperation.AC_SRC_OVER;
    blendFunction.BlendFlags = (byte)BlendFlags.Zero;
    blendFunction.SourceConstantAlpha = opacity;
    blendFunction.AlphaFormat = (byte)0;    
    PlatformAPI.AlphaBlend(hdcDst, location.X, location.Y, 
      b.Width, b.Height, hdcSrc, 0, 0, b.Width, b.Height, blendFunction);
    g.ReleaseHdc(hdcDst);
    gxSrc.ReleaseHdc(hdcSrc);
  }
}

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

Пример:
Compact Framework - грани прозрачности
Жутковато, да? Противные фиолетовые пиксели никуда не делись и тоже стали немного прозрачными 🙁

Комбинированное использование обоих способов

Вариантов комбинирования у нас, к сожалению, немного. На первый взгляд их совсем нет 🙂 Но есть всё-таки один способ.

Итак, решение следующее. Раз мы не можем одновременно задать ColorKey и вызвать AlphaBlend, будем использовать их по очереди. Сначала нарисуем фон стандартным спосбом без изысков, затем кнопку первым спосбом, а в конце… вторым спосбом нарисуем поверх фон с небольшим коэффициентом непрозрачности!

g.DrawImage(background, 0, 0);
DrawImageTransparent(g, button, new Point(10, 10), Color.FromArgb(255, 0, 255));
DrawAlpha(g, background, new Point(0, 0), 75);

Результат:
Compact Framework - Грани прозрачности

Описанный выше способ вполне жизнеспособен. Я им пользуюсь и вполне удовлетворён скоростью работы — на отрисовку всех элементов интерфейса в подобном стиле уходит в среднем от 60 до 80 миллисекунд (проверялось на разнообразных устройствах). Для создания приложения в таком стиле, безусловно, стандартные контролы не подойдут, но а кто обещал, что будет легко? В любом случае, для создания неописуемой красоты без собственного фреймворка рендеринга графических элементов не обойтись.

Оригинал статьи на Хабре.

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

This post has 2 Comments

2
  1. а можете уточнить пожалуйста что имело ввиду под этими словами “собственного фреймворка рендеринга графических элементов”?? что для красивого интерфейса не стоит использовать написаные вручную элементы управления? то есть лучше иметь один элемент управления и в нем все отрисовывать ручками?то есть не используя кучу специальных классических пользовательских элементов управления?

  2. Я думаю, здесь имелось в виду owner-drawn’ая версия каждого контрола. Хотя в принципе вариант когда одним контролом эмулируется работа всего интерфейса пользователя тоже не так и плох, для простых приложений это было бы нормально. В OpenGL-играх ведь тоже те же меню рендерятся в одной сцене на одном контроле.

Leave a Reply

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

C.

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

В этот раз статья Андрея Коновалова о работе с графикой в .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.

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

D.

DialogBlocks – Странный, но очень удобный способ создания интерфейса пользователя для Windows Mobile

Как и обещалось ранее в предыдущем посте о создании простейшего приложения для Windows Mobile, в этот раз речь пойдет о том, как быстро создать пользовательский интерфейс приложения для Windows Mobile с помощью DialogBlocks. Описывать подробно все аспекты пользования дизайнером пользовательских интерфейсов DialogBlocks я не буду, зато опишу последовательность действий, которая поможет начать пользоваться этим инструментом.

Итак, запускаем DialogBlocks и создаем новый проект (File -> New Project…)

В мастере указываем название проекта и каталог, в котором будет создан файл проекта.

На вкладке настройки параметров класса приложения указываем имя класса приложения, а также задаем имена файлов, в которые будет записан код класса приложения.

На вкладке настройки кодировки проекта указываем кодировки для файла проекта, файлов исходного кода и файлов ресурсов. Лучше всего указать <System>.

Отлично, проект создан и в него добавлен класс приложения. Теперь необходимо создать главное окно приложения. Для этого на панели инструментов жмем кнопку Element и в выпадающем меню выбираем пункт Add Frame.

В окне настройки параметров новой формы указываем имя класса формы и задаем имена файлов исходного кода для новой формы.

Теперь нам необходимо указать классу приложения, какая форма является главной. Для этого в дереве структуры проекта выбираем класс приложения и в окне настройки параметров в свойстве Main window указываем созданную ранее форму.

Теперь можно приступить к добавлению компонентов на форму. Добавляем вертикальный сайзер, в него панель, на нее еще вертикальный сайзер, в него многострочное текстовое поле. Затем на форму добавляем строку меню, на нее меню, в меню пункт меню Open, разделитель и пункт меню Exit

После того как компоненты добавлены, можно приступить к настройке их параметров.

В дереве структуры проекта выбираем панель, в окне свойств панели переходим на вкладку Styles и выставляем флаг wxNO_BORDER.

Переходим на вкладку Sizer, выставляем значение свойства Stretch factor в 1, поле Border в 0 и убираем флажки напротив свойств wxLEFT, wxRIGHT, wxTOP, wxBOTTOM. После всех этих манипуляций наша панель будет занимать все свободное пространство на форме.

В дереве структуры проекта выбираем текстовое поле, в окне свойств текстового поля переходим на вкладку Main и свойству Member variable name задаем значение m_MyTextCtrl.

Переходим на вкладку Sizer и свойство Stretch factor выставляем в 1. Этим мы указываем что текстовое поле должно занимать все свободное место на панели.

В дереве структуры проекта по очереди выбираем пункты меню Open и Exit и для каждого из них на вкладке Event handlers добавляем обработчик события wxEVT_COMMAND_MENU_SELECTED. Это событие выбора пункта меню.

Далее переходим на вкладку cpp и пишем код обработчиков событий для пунктов меню.

void MyMainFrame::OnEXITClick( wxCommandEvent & event )
{
    Close();
}

void MyMainFrame::OnOPENClick( wxCommandEvent & event )
{
    wxFileDialog dlg(this);
    if(dlg.ShowModal() == wxID_OK)
    {
        m_MyTextCtrl->LoadFile(dlg.GetPath());
    }
}

На этом работа непосредственно в DialogBlocks для нас закончена. Сохраняем все и открываем Visual Studio. Создаем новый проект (о том как это сделать я писал ранее) и добавляем в него файлы с исходным кодом, которые были созданы с помощью DialogBlocks.

Настраиваем свойства проекта и все, можно собирать.

Да, тут вот еще какая штука. DialogBlocks отображает создаваемые окна так, как они выглядят в ОС, в которой ведется работа. Т.е. если вы работаете в десктопной версии Windows, то, например, строка меню будет находиться вверху формы, в то время как в Windows Mobile строка меню будет размещаться внизу. Это своего рода неудобство, но со временем на него перестаешь обращать внимание, т.к. строка меню не мешает размещению остальных компонентов на форме и никак на них не влияет.

Вот так выглядит созданная нами форма в дизайнере DialogBlocks.

А вот так она выглядит на устройстве.


Ну вот, собственно, и все.