BitImageTool — пиктограммы для кнопок и панелек приложений, закодированные в ASCII
Статья о том, как легко делать графические кнопки для панелей инструментов, не таская за приложением гору бинарных ресурсов с картинками. Этот метод платформонезависимый и может быть использован в различных языках и средах, позволяющих работать с графикой и растровыми изображениями. Ниже приводятся примеры для C# (WinForms / WPF), JavaScript, Python.
При разработке приложений и утилит мне неоднократно приходилось сталкиваться с проблемой создания множества пиктограмм для панелей инструментов и кнопок. Маленькое графическое изображение гораздо удобнее, чем громоздкий текст на кнопке, который лишь неоправданно раздувает её размер. Проблема в том, что разрабатывая небольшую утилиту, часто нет желания возиться с рисованием полноцветных изображений, а потом таскать их по папкам проекта, добавлять в ресурсы, искать их в ресурсах, чтобы обновить или заменить.
Тогда возникла идея — рисовать одноцветные изображения, кодировать их в строку и хранить прямо в свойствах объекта (например — кнопки). Самый простой вид кодировки — когда шесть точек изображения превращаются в шестибитный код, соответствующий некоторому символу из ASCII таблицы с заданным базовым смещением.
пример такой панельки с одноцветными пиктограммами на кнопочках
Казалось бы, для этих целей подойдёт Base64. Но это табличный алгоритм кодирования, что делает его использование не таким компактным и удобным, как прямое кодирование в код ASCII. А хотелось иметь простой и универсальный алгоритм расшифровки в несколько строк для всех платформ.
Кодировка выглядит максимально просто: 0-ой байт — код базового смещения, 1 — ширина изображения в пикселях + код смещения, 2 — высота + код смещения, 3 — первые шесть пикселей изображения + код смещения, 4 — следующие шесть пикселей + код, и так далее…
В общем, хотелось сделать бесхитростную кодировку для того, чтобы извлекать значение точки можно было одним выражением и иметь предельно короткий код.
Для рисования самих изображений пришлось сделать небольшую утилиту — BitImageTool. Программа крайне простая — в клетчатом поле рисуется мышкой нужное изображение, а параметры поля задаются ползунками в панели свойств с правой стороны окна. Также присутствуют самые необходимые функции — очистка, инверсия, сдвиг и так далее. Результат преобразования изображения в строку сразу же отражается в панели вывода (внизу окна).
BitImageTool для рисования битовых изображений
Функций «сохранить» и «загрузить» нет, потому как они и не нужны, просто копируем строку из поля результата, или наоборот, вставляем её туда, и изображение в поле редактора обновляется автоматически.
Следует обратить внимание на базовый код — он нужен для того, чтобы избежать в ряде случаев проблем с использованием недопустимых символов внутри строк. Например, такие символы как »<" и ">» могут привести к проблемам внутри XAML строк в WPF, и чтобы их не было в результирующей строке, можно выставить базу, к примеру, на 63. Для каждого изображения этот код свой, он присутствует в строке на нулевой позиции.
Для декодирования строки в изображение можно применить простой алгоритм:
Bitmap stringToBitmap(Color pen, string s)
{
byte basecode = s[0];
byte w = s[1] - basecode;
byte h = s[2] - basecode;
Bitmap bmp = new Bitmap(w, h);
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
bmp.SetPixel(x, y, ((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0 ? pen : Color.Transparent);
return bmp;
}
где «pen» — это параметр цвета получаемого изображения, а «s» — сами данные изображения, закодированные в строку.
Конечно, удобнее всего завернуть этот код внутрь компонента (или контрола) и получить готовый класс, объекты которого будут иметь пару этих параметров как свойства.
Вот несколько примеров использования:
JavaScript:
Этот код достаточно сохранить в файл и запустить в браузере. Главное, чтобы было разрешено исполнение скриптов. Тогда на экране появится несколько разноцветных изображений. Сами изображения заданы свойством image-data тэгов canvas. Их цвет задан свойством image-color.
результат в браузере
Следующий пример для WPF:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace EXLib
{
public class TextEncodedImage : Image
{
WriteableBitmap stringToBitmap(SolidColorBrush pen, string s)
{
try
{
uint ppen = ((uint)pen.Color.A << 24) + ((uint)pen.Color.B << 16) + ((uint)pen.Color.G << 8) + pen.Color.R;
int basecode = s[0];
int w = s[1] - basecode;
int h = s[2] - basecode;
var bmp = new WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null);
uint[,] data = new uint[h, w];
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
if (((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0) data[y, x] = ppen;
bmp.WritePixels(new System.Windows.Int32Rect(0, 0, w, h), data, bmp.Format.BitsPerPixel * w / 8, 0);
return bmp;
}
catch { return null; }
}
public static readonly DependencyProperty EncodedImageProperty = DependencyProperty.Register("EncodedImage", typeof(string), typeof(TextEncodedImage), new PropertyMetadata(null, propertyChangedCallback));
public string EncodedImage
{
get { return (string)GetValue(EncodedImageProperty); }
set { SetValue(EncodedImageProperty, value); }
}
public static readonly DependencyProperty EncodedImageColorProperty = DependencyProperty.Register("EncodedImageColor", typeof(SolidColorBrush), typeof(TextEncodedImage), new PropertyMetadata(null, propertyChangedCallback));
public SolidColorBrush EncodedImageColor
{
get { return (SolidColorBrush)GetValue(EncodedImageColorProperty); }
set { SetValue(EncodedImageColorProperty, value); }
}
private static void propertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
TextEncodedImage o = (TextEncodedImage)obj;
o.Source = o.stringToBitmap((SolidColorBrush)o.GetValue(EncodedImageColorProperty) ?? Brushes.White, (string)o.GetValue(EncodedImageProperty));
}
}
}
Здесь создаётся класс TextEncodedImage, с двумя свойствами (типа DependencyProperty), чтобы можно было их задавать через XAML.
Вот пример использования этого контрола в одном из моих проектов:
...
...
...
где xmlns: local — это «clr-namespace: пространство имён проекта».
Самое удобное здесь то, что если тема приложения меняется, например, со светлой на тёмную, то нет необходимости хранить дополнительный набор изображений других цветов, свойства динамически обновятся из нового словаря ресурсов и изображения будут пересозданы автоматически в новых, индивидуально заданных для каждого цветах.
пример панельки — тёмная тема
пример панельки — светлая тема
Следующий пример для C# WinForms:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace TextEncodedImage
{
public enum ToolButtonType { Button, Check, Radio };
public class ToolButton : Control
{
bool mouseOver = false;
bool mouseDown = false;
SolidBrush backBrush = new SolidBrush(Color.FromArgb(0xE0, 0xE0, 0xE0));
Pen borderPen = new Pen(Color.FromArgb(0xD0, 0xD0, 0xD0));
SolidBrush mouseOverBrush = new SolidBrush(Color.FromArgb(0xF0, 0xF0, 0xF0));
SolidBrush mouseDownBrush = new SolidBrush(Color.FromArgb(0xFF, 0xFF, 0xFF));
SolidBrush checkedBrush = new SolidBrush(Color.FromArgb(0xC0, 0xC0, 0xFF));
SolidBrush mouseOverCheckedBrush = new SolidBrush(Color.FromArgb(0xD0, 0xD0, 0xFF));
public ToolButton() : base()
{
this.Paint += ToolButton_Paint;
this.MouseEnter += (sender, e) => { mouseOver = true; Refresh(); };
this.MouseLeave += (sender, e) => { mouseOver = false; Refresh(); };
this.MouseDown += (sender, e) => { mouseDown = true; check(); Refresh(); };
this.MouseUp += (sender, e) => { mouseDown = false; Refresh(); };
Refresh();
}
Bitmap stringToBitmap(Color pen, string s)
{
try
{
byte basecode = s[0];
byte w = s[1] - basecode;
byte h = s[2] - basecode;
Bitmap bmp = new Bitmap(w, h);
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
bmp.SetPixel(x, y, ((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0 ? pen : Color.Transparent);
return bmp;
}
catch { return null; }
}
private void ToolButton_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
SolidBrush brush;
if (mouseDown) brush = mouseDownBrush;
else if (isChecked && mouseOver && buttonType != ToolButtonType.Radio) brush = mouseOverCheckedBrush;
else if (IsChecked) brush = checkedBrush;
else if (mouseOver) brush = mouseOverBrush;
else brush = backBrush;
g.FillRectangle(brush, 0, 0, this.Width, this.Height);
SizeF textSize = g.MeasureString(text, Font);
g.DrawString(text, Font, new SolidBrush(this.ForeColor), (Width - textSize.Width) / 2, (Height - textSize.Height) / 2);
if (encodedImageBitmap == null && !string.IsNullOrWhiteSpace(EncodedImage))
encodedImageBitmap = stringToBitmap(EncodedImagePen, EncodedImage);
if (encodedImageBitmap != null) g.DrawImage(encodedImageBitmap, EncodedImageLocation);
g.DrawRectangle(borderPen, 0, 0, this.Width - 1, this.Height - 1);
}
private void check()
{
if (buttonType == ToolButtonType.Button) isChecked = false;
if (buttonType == ToolButtonType.Check) isChecked = !isChecked;
if (buttonType == ToolButtonType.Radio)
{
foreach (var c in Parent.Controls) if (c is ToolButton tb && tb.ButtonType == ToolButtonType.Radio && tb.RadioGroup == this.RadioGroup) tb.IsChecked = false;
isChecked = true;
}
}
ToolButtonType buttonType = ToolButtonType.Button;
[DefaultValue(ToolButtonType.Button)]
public ToolButtonType ButtonType
{
get { return buttonType; }
set { buttonType = value; Refresh(); }
}
bool isChecked = false;
public bool IsChecked
{
get { return isChecked; }
set { isChecked = value; Refresh(); }
}
public int RadioGroup { get; set; }
string text = "";
public override string Text
{
get { return text; }
set { text = value; Refresh(); }
}
Bitmap encodedImageBitmap;
string encodedImage = "";
public string EncodedImage
{
get { return encodedImage; }
set { encodedImage = value; encodedImageBitmap = null; Refresh(); }
}
Color encodedImagePen = Color.Black;
public Color EncodedImagePen
{
get { return encodedImagePen; }
set { encodedImagePen = value; encodedImageBitmap = null; Refresh(); }
}
public Point EncodedImageLocation { get; set; }
}
}
Это код контрола для кнопочек из, собственно, описанной выше утилиты BitImageTool, кнопка тут бывает трёх типов ToolButtonType — обычная (Button), выключатель (Check), переключатель в группе (Radio). И, конечно, содержит изображение, декодированное из текста в свойстве EncodedImage, цвет в свойстве EncodedImagePen.
Не трудно догадаться, что изображения на кнопочках в BitImageTool нарисованы в самом BitImageTool.
И, ещё простой пример на питоне (установите pillow перед запуском):
from PIL import Image
s = "0@@00080060ncWolI6HV0V1PI0H6@V1VigOnm706001000"
pen = (255, 0, 0)
basecode = ord(s[0])
w = ord(s[1]) - basecode
h = ord(s[2]) - basecode
img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
for y in range(w):
for x in range(h):
if ((ord(s[3 + (y * w + x) // 6]) - basecode) & (1 << (y * w + x) % 6)) > 0:
img.putpixel((x, y), pen)
img.save('test.png', 'PNG')
этот код просто создаёт изображение из строки «s», цветом pen, и сохраняет в файл test.png
Итоги
Преимущества такого способа:
одноцветные изображения можно легко и быстро рисовать
также их легко и быстро можно переносить в код
нет нужды возиться с ресурсами приложения
не требуется подгружать их с диска или из сети
можно менять цвет на лету, просто изменяя один параметр (или свойство)
Недостатки:
требуются аппаратные ресурсы, чтобы создавать изображение каждый раз при инициализации приложения, однако, алгоритм столь примитивен, что на его фоне загрузка и декодирование одного из стандартных графических форматов изображений может потребовать не меньше
одноцветные изображения порой выглядят грубо, особенно в областях, где есть наклонные или изогнутые линии, но эта проблема легко решается методами алгоритмического сглаживания
Главное, чего мне хотелось добиться — это иметь возможность получать нужные пиктограммы для кнопочек и панелей инструментов быстро и необременительно, не имея навыков рисования и знания продвинутых графических редакторов. В BitImageTool можно нарисовать нужную картинку за пару минут, а иной раз и быстрее, скопировать сгенерированный «текст» в IDE и продолжить работу над кодом, не утопая в раздумьях над тяготами графического оформления приложения.
Скачать BitImageTool можно на гитхабе