Как стать автором
Обновить

BitImageTool — пиктограммы для кнопок и панелек приложений, закодированные в ASCII

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров1.4K

Статья о том, как легко делать графические кнопки для панелей инструментов, не таская за приложением гору бинарных ресурсов с картинками. Этот метод платформонезависимый и может быть использован в различных языках и средах, позволяющих работать с графикой и растровыми изображениями. Ниже приводятся примеры для C# (WinForms / WPF), JavaScript, Python.

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

Тогда возникла идея - рисовать одноцветные изображения, кодировать их в строку и хранить прямо в свойствах объекта (например - кнопки). Самый простой вид кодировки - когда шесть точек изображения превращаются в шестибитный код, соответствующий некоторому символу из ASCII таблицы с заданным базовым смещением.

пример такой панельки с одноцветными пиктограммами на кнопочках
пример такой панельки с одноцветными пиктограммами на кнопочках

Казалось бы, для этих целей подойдёт Base64. Но это табличный алгоритм кодирования, что делает его использование не таким компактным и удобным, как прямое кодирование в код ASCII. А хотелось иметь простой и универсальный алгоритм расшифровки в несколько строк для всех платформ.

Кодировка выглядит максимально просто: 0-ой байт - код базового смещения, 1 - ширина изображения в пикселях + код смещения, 2 - высота + код смещения, 3 - первые шесть пикселей изображения + код смещения, 4 - следующие шесть пикселей + код, и так далее...

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

Для рисования самих изображений пришлось сделать небольшую утилиту - BitImageTool. Программа крайне простая - в клетчатом поле рисуется мышкой нужное изображение, а параметры поля задаются ползунками в панели свойств с правой стороны окна. Также присутствуют самые необходимые функции - очистка, инверсия, сдвиг и так далее. Результат преобразования изображения в строку сразу же отражается в панели вывода (внизу окна).

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:

<html>
    <body style="background-color: #333">
        <canvas class="textimage" image-data="0@@00080060ncWolI6HV0V1PI0H6@V1VigOnm706001000" image-color="0xFF4040FF"></canvas>
        <canvas class="textimage" image-data="0@@00Po18@0boP488iOB2TW019@h34R0QhOn0070P00000" image-color="0x40FF40FF"></canvas>
        <canvas class="textimage" image-data="0@@00084033h`1>LPW7`o0h70d00;0@30\00=0`30H0000" image-color="0x4040FFFF"></canvas>
        
        <script>
            let canvases = document.getElementsByClassName("textimage");
            Array.from(canvases).forEach(function(canvas)
            {
                let ctx = canvas.getContext("2d");
                let s = canvas.getAttribute("image-data");
                let pen = parseInt(canvas.getAttribute("image-color"));
                let penr = pen >> 24 & 0xFF;
                let peng = pen >> 16 & 0xFF;
                let penb = pen >> 8 & 0xFF;
                let pena = pen & 0xFF;
                
                let basecode = s.charCodeAt(0);
                let w = s.charCodeAt(1) - basecode;
                let h = s.charCodeAt(2) - basecode;
                canvas.width = w;
                canvas.height = h;
                let idata = ctx.getImageData(0, 0, w, h);
                let data = idata.data;
                for (let y = 0; y < h; y++)
                    for (let x = 0; x < w; x++)
                        if (((s.charCodeAt(3 + (y * w + x) / 6) - basecode) & (1 << (y * w + x) % 6)) > 0)
                        {
                            let p = (y * w + x) * 4;
                            data[p] = penr;
                            data[p + 1] = peng;
                            data[p + 2] = penb;
                            data[p + 3] = pena;
                        }
                ctx.putImageData(idata, 0, 0);
            });
        </script>
    </body>
</html>

Этот код достаточно сохранить в файл и запустить в браузере. Главное, чтобы было разрешено исполнение скриптов. Тогда на экране появится несколько разноцветных изображений. Сами изображения заданы свойством 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.

Вот пример использования этого контрола в одном из моих проектов:

...
<DockPanel>
    <Button DockPanel.Dock="Left" HorizontalAlignment="Left" ToolTip="Reload cache" Click="ReloadMenuItem_Click" Width="30" Height="30">
        <local:TextEncodedImage EncodedImage="0@@00080060ncWolI6HV0V1PI0H6@V1VigOnm706001000" Width="16" Height="16" EncodedImageColor="{DynamicResource MenuButton_Refresh}"></local:TextEncodedImage>
    </Button>
    
    <Rectangle Width="2" Fill="{DynamicResource PanelBack}"></Rectangle>
    <Button DockPanel.Dock="Left" HorizontalAlignment="Left" ToolTip="Grab frames for selected file/folder [F3]" Width="30" Height="30" Click="GrabFramesMenuItem_Click">
        <local:TextEncodedImage EncodedImage="0@@00Po18@0boP488iOB2TW019@h34R0QhOn0070P00000" Width="16" Height="16" EncodedImageColor="{DynamicResource MenuButton_GrabFrames}"></local:TextEncodedImage>
    </Button>
    
    <Rectangle Width="2" Fill="{DynamicResource PanelBack}"></Rectangle>
    
    <Button DockPanel.Dock="Left" HorizontalAlignment="Left" ToolTip="Purge Cache&#x0d;&#x0a;It will clean cache from empty links and ulinked frames&#x0d;&#x0a;Run it to keep cache consistency" Width="30" Height="30" Click="PurgeCacheMenuItem_Click">
        <local:TextEncodedImage EncodedImage="0@@000P10H0060P10H0060P10H0PO0l?0o3@E0D5PZ0000" Width="16" Height="16" EncodedImageColor="{DynamicResource MenuButton_PurgeCache}"></local:TextEncodedImage>
    </Button>
    ...
</DockPanel>
...

где xmlns:local - это "clr-namespace:пространство имён проекта".

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

пример панельки - тёмная тема
пример панельки - тёмная тема
пример панельки - светлая тема
пример панельки - светлая тема

Следующий пример для C# WinForms:

using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace BitImageTool
{
    public enum ToolButtonType { Button, Check, Radio };

    public class ToolButton : Control
    {
        bool mouseOver = false;
        bool mouseDown = false;

        Color backBrushColor = Color.FromArgb(0xE0, 0xE0, 0xE0);
        Color borderPenColor = Color.FromArgb(0xD0, 0xD0, 0xD0);
        Color mouseOverBrushColor = Color.FromArgb(0xF0, 0xF0, 0xF0);
        Color mouseDownBrushColor = Color.FromArgb(0xFF, 0xFF, 0xFF);
        Color checkedBrushColor = Color.FromArgb(0xC0, 0xC0, 0xFF);
        Color mouseOverCheckedBrushColor = 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();
        }

        ~ToolButton()
        {
            encodedImageBitmap?.Dispose();
        }

        Bitmap stringToBitmap(Color pen, string s)
        {
            if (pen == null || s == null) return null;
            try
            {
                int basecode = s[0];
                int w = s[1] - basecode;
                int 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)
        {
            SolidBrush brush = null;
           
            try
            {
                var g = e.Graphics;

                if (mouseDown) brush = new SolidBrush(mouseDownBrushColor);
                else if (isChecked && mouseOver && buttonType != ToolButtonType.Radio) brush = new SolidBrush(mouseOverCheckedBrushColor);
                else if (IsChecked) brush = new SolidBrush(checkedBrushColor);
                else if (mouseOver) brush = new SolidBrush(mouseOverBrushColor);
                else brush = new SolidBrush(backBrushColor);

                g.FillRectangle(brush, 0, 0, this.Width, this.Height);

                SizeF textSize = g.MeasureString(text, Font);
                using (Brush br = new SolidBrush(this.ForeColor))
                    g.DrawString(text, Font, br, (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);

                using (Pen borderPen = new Pen(borderPenColor))
                    g.DrawRectangle(borderPen, 0, 0, this.Width - 1, this.Height - 1);
            }
            finally
            {
                brush?.Dispose();
            }
        }

        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?.Dispose();
                encodedImageBitmap = null;
                Refresh();
            }
        }

        Color encodedImagePen;
        public Color EncodedImagePen
        {
            get { return encodedImagePen; }
            set
            {
                encodedImagePen = value;
                encodedImageBitmap?.Dispose();
                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 можно на гитхабе

Теги:
Хабы:
+12
Комментарии17

Публикации

Истории

Работа

.NET разработчик
57 вакансий
Data Scientist
85 вакансий
Python разработчик
133 вакансии
React разработчик
55 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн