Шрифты и вывод текста в графические ресурсы

Помимо отрисовки отдельных пикселей в массиве графического ресурса нам доступны встроенные функции для вывода текста. Функции позволяют изменять текущий шрифт и его характеристики (TextSetFont), получать размеры прямоугольника, в который может быть вписана заданная строка (TextGetSize), а также непосредственно вставлять надпись в генерируемое изображение (TextOut).

bool TextSetFont(const string name, int size, uint flags, int orientation = 0)

Функция устанавливает шрифт и его характеристики для последующего рисования текста в буфере изображения с помощью функции TextOut (см. далее). Параметр name может содержать имя встроенного шрифта Windows или файл ttf-шрифта (TrueType Font), подключенный директивой ресурс (если имя начинается с "::").

Размер (size) может задаваться в пунктах (типографская единица измерения) или пикселях (точках экрана). Положительные значения означают, что единицей измерения является пиксель, отрицательные — измеряются в десятых долях пункта. Высота в пикселях будет по-разному выглядеть у пользователей в зависимости от технических возможностей и настроек их мониторов. Высота в пунктах будет примерно ("на глаз") одинаковой у всех.

Типографский пункт — это физическая мера длины, равная традиционно 1/72-ой части дюйма. Следовательно, 1 пункт равен 0.352778 миллиметра. Пиксель на экране является виртуальной мерой длины. Его физический размер зависит от аппаратной разрешающей способности экрана. Например, при плотности экрана 96 DPI (точек на дюйм) 1 пиксель займет 0.264583 миллиметра, или 0.75 пункта. Однако большинство современных дисплеев имеют гораздо большие значения DPI и, соответственно, более мелкие пиксели. Из-за этого в операционных системах, в том числе и Windows, давно имеются настройки для увеличения видимого масштаба интерфейсных элементов. Таким образом, при указании размера в пунктах (отрицательные значения) размер текста в пикселях будет зависеть от дисплея и настроек масштаба в операционной системе (например, "стандартный" 100%, "средний" 125% или "крупный" 150%).
 
Увеличение масштаба приводит к тому, что размер отображаемых пикселей искусственно увеличивается системой. Это эквивалентно уменьшению размера экрана в пикселях и для получения того же физического размера система применяет так называемый эффективный DPI. Если масштабирование включено, то именно эффективный DPI сообщается программам, в том числе терминалу и затем MQL-программам. Узнать DPI экрана при необходимости можно из свойства TERMINAL_SCREEN_DPI (см. Характеристики экрана). Но на самом деле, задавая размер шрифта в пунктах, мы избавлены от необходимости пересчитывать его размер в зависимости от DPI, так как система сделает это за нас.

По умолчанию используется шрифт Arial и размер -120 (12 pt). Элементы управления, в частности, встроенные в объекты на графиках также оперируют размерами шрифтов в пунктах. Например, если в MQL-программе требуется нарисовать текст такого же размера, как текст в объекте OBJ_LABEL, в котором установлен размер 10 пунктов, следует использовать параметр size, равный -100.

В параметре flags задается комбинация флагов, описывающих стиль шрифт. Комбинация составляется как битовая маска, с помощью оператора побитового ИЛИ ('|'). Флаги делятся на две группы: флаги стиля и флаги жирности.

В следующей таблице приведены флаги стиля. Их можно смешивать.

Флаг

Описание

FONT_ITALIC

Курсив

FONT_UNDERLINE

Подчёркивание

FONT_STRIKEOUT

Перечёркивание

Флаги жирности имеют соответствующие им относительные весовые коэффициенты (приведены для возможности сравнить ожидаемые эффекты).

Флаг

Описание

FW_DONTCARE

0 (будет применено системное значение по умолчанию)

FW_THIN

100

FW_EXTRALIGHT, FW_ULTRALIGHT

200

FW_LIGHT

300

FW_NORMAL, FW_REGULAR

400

FW_MEDIUM

500

FW_SEMIBOLD, FW_DEMIBOLD

600

FW_BOLD

700

FW_EXTRABOLD, FW_ULTRABOLD

800

FW_HEAVY, FW_BLACK

900

В комбинации флагов используйте только одно из этих значений.

Параметр orientation задает угол наклона текста по отношению к горизонтали, в десятых долях градуса. Например, 0 означает обычный вывод текста, а orientation = 450 приведет к наклону в 45 градусов (против часовой стрелки).

Обратите внимание, что установки, сделанные одним вызовом TextSetFont, будут влиять на все последующие вызовы TextOut, пока (или если) не будут изменены.

Функция возвращает true в случае успеха или false при возникновении проблем (например, если не найден шрифт).

Пример использования этой, а также двух остальных функций мы рассмотрим после описания всех их.

bool TextGetSize(const string text, uint &width, uint &height)

Функция возвращает ширину и высоту строки при текущих настройках шрифта (это может быть шрифт по умолчанию или заданный в предыдущем вызове TextSetFont).

В параметре text передается строка, для которой требуется получить длину и ширину в пикселях. Значения размеров записываются функцией по ссылкам в параметрах width и height.

Следует отметить, что поворот (наклон) выводимого текста, заданный параметром orientation при вызове TextSetFont, никак не влияет на оценку размеров. Иными словами, если текст предполагается повернуть на 45 градусов, то вычислить минимальный квадрат, в который текст может быть вписан, должна сама MQL-программа. Функция TextGetSize рассчитывает размер текста в стандартном (горизонтальном) положении.

bool TextOut(const string text, int x, int y, uint anchor, uint &data[], uint width, uint height, uint color, ENUM_COLOR_FORMAT color_format)

Функция рисует текст в графическом буфере по указанным координатам и с учетом цвета, формата и предыдущих настроек (шрифта, стиля и ориентации).

Текст передается в параметре text и должен представлять собой одну строку.

Координаты x и y, заданные в пикселях, определяют точку в графическом буфере, где выводится текст. Какое именно место генерируемой надписи окажется в точке (x,y), зависит от способа привязки в параметре anchor (см. далее).

Буфер представлен массивом data, и хотя массив одномерный, в нем хранится двумерный "холст" размерами width на height точек. Этот массив может быть получен из функции ResourceReadImage или распределен MQL-программой. После завершения всех операций редактирования, включая вывод текста, следует создать новый ресурс на основе этого буфера или применить его к уже существующему ресурсу — в обоих случаях подразумевается вызов ResourceCreate.

Цвет текста и способ обработки цвета задаются параметрами color и color_format (см. ENUM_COLOR_FORMAT). Обратите внимание, что для цвета используется тип uint, то есть для передачи цвета (color) следует конвертировать его с помощью ColorToARGB.

Способ привязки, задаваемый параметром anchor, является комбинацией двух флагов расположения текста: по вертикали и по горизонтали.

Флаги расположения текста по горизонтали:

  • TA_LEFT – привязка к левой стороне ограничивающего прямоугольника
  • TA_CENTER – привязка к середине между левой и правой стороной прямоугольника
  • TA_RIGHT – привязка к правой стороне ограничивающего прямоугольника

Флаги расположения текста по вертикали:

  • TA_TOP – привязка к верхней стороне ограничивающего прямоугольника
  • TA_VCENTER – привязка к середине между верхней и нижней стороной прямоугольника
  • TA_BOTTOM – привязка к нижней стороне ограничивающего прямоугольника

Итого существует 9 допустимых сочетаний флагов для описания способа привязки.

Положение выводимого текста относительно точки привязки

Положение выводимого текста относительно точки привязки

Здесь в центре картинки утрированно большим размером нанесена точка в генерируемом изображении с координатами (x,y). В зависимости от флагов, текст появляется относительных этой точки в указанных позициях (содержимое текста соответствует примененному способу привязки).

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

Не следует путать данные флаги с выравниванием текста. Ограничивающий прямоугольник всегда подогнан под размеры текста, и его позиция относительно точки привязки, в некотором смысле, противоположна названиям флагов.

Рассмотрим несколько примеров с использованием трех функций.

Для начала проверим самые простые возможности — установку жирности и начертания шрифтов. Скрипт ResourceText.mq5 позволяет выбрать во входных переменных название шрифта, его размер, а также цвета фона и текста. Надписи будут демонстрироваться на графике заданное количество секунд.

input string Font = "Arial";             // Font Name
input int    Size = -240;                // Size
input color  Color = clrBlue;            // Font Color
input color  Background = clrNONE;       // Background Color
input uint   Seconds = 10;               // Demo Time (seconds)

Название каждой градации жирности будет выводиться в текст надписи, поэтому для упрощения процесса (за счет использования EnumToString) объявлено перечисление ENUM_FONT_WEIGHTS.

enum ENUM_FONT_WEIGHTS
{
   _DONTCARE = FW_DONTCARE,
   _THIN = FW_THIN,
   _EXTRALIGHT = FW_EXTRALIGHT,
   _LIGHT = FW_LIGHT,
   _NORMAL = FW_NORMAL,
   _MEDIUM = FW_MEDIUM,
   _SEMIBOLD = FW_SEMIBOLD,
   _BOLD = FW_BOLD,
   _EXTRABOLD = FW_EXTRABOLD,
   _HEAVY = FW_HEAVY,
};
 
const int nw = 10; // количество различных весов

Флаги начертания собраны в массиве rendering и из него выбираются случайные сочетания.

   const uint rendering[] =
   {
      FONT_ITALIC,
      FONT_UNDERLINE,
      FONT_STRIKEOUT
   };
   const int nr = sizeof(rendering) / sizeof(uint);

Для получения случайного числа в диапазоне имеется вспомогательная функция Random.

int Random(const int limit)
{
   return rand() % limit;
}

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

void OnStart()
{
   ...
   const string name = "FONT";
   const int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
   
   // объект для ресурса с картинкой на все окно
   ObjectCreate(0nameOBJ_BITMAP_LABEL000);
   ObjectSetInteger(0nameOBJPROP_XSIZEw);
   ObjectSetInteger(0nameOBJPROP_YSIZEh);
   ...

Далее мы выделяем память под буфер для картинки, закрашиваем его заданным фоновым цветом (или оставляем прозрачным, по умолчанию), создаем на основе буфера ресурс и привязываем его к объекту.

   uint data[];
   ArrayResize(dataw * h);
   ArrayInitialize(dataBackground == clrNONE ? 0 : ColorToARGB(Background));
   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_RAW);
   ObjectSetString(0nameOBJPROP_BMPFILE"::" + name);
   ...

На всякий случай обратите внимание, что мы можем задавать свойство OBJPROP_BMPFILE без модификатора (0 или 1) в вызове ObjectSetString, если не предполагается переключать объект между двумя состояниями.

Все веса шрифтов перечислены в массиве weights.

   const uint weights[] =
   {
      FW_DONTCARE,
      FW_THIN,
      FW_EXTRALIGHT// FW_ULTRALIGHT,
      FW_LIGHT,
      FW_NORMAL,     // FW_REGULAR,
      FW_MEDIUM,
      FW_SEMIBOLD,   // FW_DEMIBOLD,
      FW_BOLD,
      FW_EXTRABOLD,  // FW_ULTRABOLD,
      FW_HEAVY,      // FW_BLACK
   };
   const int nw = sizeof(weights) / sizeof(uint);

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

   const int step = h / (nw + 2);
   int cursor = 0;    // координата Y текущей "строки текста"
   
   for(int weight = 0weight < nw; ++weight)
   {
      // применяем случайный стиль
      const int r = Random(8);
      uint render = 0;
      for(int j = 0j < 3; ++j)
      {
         if((bool)(r & (1 << j))) render |= rendering[j];
      }
      TextSetFont(FontSizeweights[weight] | render);
      
      // генерируем описание шрифта
      const string text = Font + EnumToString((ENUM_FONT_WEIGHTS)weights[weight]);
      
      // рисуем текст на отдельной "строке"
      cursor += step;
      TextOut(textw / 2cursorTA_CENTER | TA_TOPdatawh,
         ColorToARGB(Color), COLOR_FORMAT_ARGB_RAW);
   }
   ...

Наконец, мы обновляем ресурс и график.

   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_RAW);
   ChartRedraw();
   ...

Пользователь может остановить демонстрацию заранее.

   const uint timeout = GetTickCount() + Seconds * 1000;
   while(!IsStopped() && GetTickCount() < timeout)
   {
      Sleep(1000);
   }

В завершении скрипт удаляет ресурс и объект.

   ObjectDelete(0name);
   ResourceFree("::" + name);
}

Результат работы скрипта приведен на следующем изображении.

Рисование текста разной жирности и стилей

Рисование текста разной жирности и стилей

Во втором примере ResourceFont.mq5 усложним задачу, подключив пользовательский шрифт в виде ресурса, а также задействуем поворот текста с шагом 90 градусов.

Файл шрифта находится рядом со скриптом.

#resource "a_LCDNova3DCmObl.ttf"

Сообщение можно поменять во входном параметре.

input string Message = "Hello world!";   // Message

На этот раз объект OBJ_BITMAP_LABEL не будет занимать все окно и потому центрируется по горизонтали и вертикали.

void OnStart()
{
   const string name = "FONT";
   const int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
   
   // объект для ресурса с картинкой
   ObjectCreate(0nameOBJ_BITMAP_LABEL000);
   ObjectSetInteger(0nameOBJPROP_XDISTANCEw / 2);
   ObjectSetInteger(0nameOBJPROP_YDISTANCEh / 2);
   ObjectSetInteger(0nameOBJPROP_ANCHORANCHOR_CENTER);
   ...

Буфер сначала выделяется минимального размера, просто чтобы выполнить создание ресурса. Позднее мы его расширим под габариты надписи: для них зарезервированы переменные width и height.

   uint data[], widthheight;
   ArrayResize(data1);
   ResourceCreate(namedata11001COLOR_FORMAT_ARGB_RAW);
   ObjectSetString(0nameOBJPROP_BMPFILE"::" + name);
   ...

В цикле с отсчетом времени теста нам потребуется менять ориентацию надписи, для чего заведена переменная angle (в ней будут прокручиваться градусы). Ориентация будет меняться раз в секунду, отсчет ведется в переменной remain.

   const uint timeout = GetTickCount() + Seconds * 1000;
   int angle = 0;
   int remain = 10;
   ...

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

   while(!IsStopped() && GetTickCount() < timeout)
   {
      // применяем новый угол
      TextSetFont("::a_LCDNova3DCmObl.ttf", -2400angle * 10);
      
      // формируем текст
      const string text = Message + " (" + (string)remain-- + ")";
      
      // получаем размеры текста, распределяем массив
      TextGetSize(textwidthheight);
      ArrayResize(datawidth * height);
      ArrayInitialize(data0);            // прозрачность
      
      // при вертикальной ориентации меняем местами размеры
      if((bool)(angle / 90 & 1))
      {
         const uint t = width;
         width = height;
         height = t;
      }
      
      // подстраиваем размеры объекта
      ObjectSetInteger(0nameOBJPROP_XSIZEwidth);
      ObjectSetInteger(0nameOBJPROP_YSIZEheight);
      
      // рисуем текст
      TextOut(textwidth / 2height / 2TA_CENTER | TA_VCENTERdatawidthheight,
         ColorToARGB(clrBlue), COLOR_FORMAT_ARGB_RAW);
      
      // обновляем ресурс и график
      ResourceCreate(namedatawidthheight00widthCOLOR_FORMAT_ARGB_RAW);
      ChartRedraw();
      
      // меняем угол
      angle += 90;
      
      Sleep(100);
   }
   ...

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

В конце также удаляем объект и ресурс.

   ObjectDelete(0name);
   ResourceFree("::" + name);
}

Один из моментов выполнения скрипта представлен на следующем скриншоте.

Надпись с пользовательским шрифтом

Надпись с пользовательским шрифтом

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

Скрипт генерирует заданное количество надписей (ExampleCount), используя указанный шрифт.

input string Font = "Arial";             // Font Name
input int    Size = -150;                // Size
input int    ExampleCount = 11;          // Number of examples

Точки привязки и повороты выбираются случайным образом.

Для указания названий точек привязки в надписях объявлено перечисление ENUM_TEXT_ANCHOR со всеми допустимыми вариантами, так что для любого случайно выбранного элемента достаточно будет вызвать EnumToString.

enum ENUM_TEXT_ANCHOR
{
   LEFT_TOP = TA_LEFT | TA_TOP,
   LEFT_VCENTER = TA_LEFT | TA_VCENTER,
   LEFT_BOTTOM = TA_LEFT | TA_BOTTOM,
   CENTER_TOP = TA_CENTER | TA_TOP,
   CENTER_VCENTER = TA_CENTER | TA_VCENTER,
   CENTER_BOTTOM = TA_CENTER | TA_BOTTOM,
   RIGHT_TOP = TA_RIGHT | TA_TOP,
   RIGHT_VCENTER = TA_RIGHT | TA_VCENTER,
   RIGHT_BOTTOM = TA_RIGHT | TA_BOTTOM,
};

В обработчике OnStart первым делом объявлен массив этих новых констант.

void OnStart()
{
   const ENUM_TEXT_ANCHOR anchors[] =
   {
      LEFT_TOP,
      LEFT_VCENTER,
      LEFT_BOTTOM,
      CENTER_TOP,
      CENTER_VCENTER,
      CENTER_BOTTOM,
      RIGHT_TOP,
      RIGHT_VCENTER,
      RIGHT_BOTTOM,
   };
   const int na = sizeof(anchors) / sizeof(uint);
   ...

Начальное создание объекта и ресурса аналогично примеру ResourceText.mq5, так что опустим их здесь. Самое интересное происходит в цикле.

   for(int i = 0i < ExampleCount; ++i)
   {
      // применяем случайный угол
      const int angle = Random(360);
      TextSetFont(FontSize0angle * 10);
      
      // берем случайные координаты и точку привязки
      const ENUM_TEXT_ANCHOR anchor = anchors[Random(na)];
      const int x = Random(w / 2) + w / 4;
      const int y = Random(h / 2) + h / 4;
      const color clr = ColorMix::HSVtoRGB(angle);
      
      // рисуем кружок непосредственно в том месте изображения,
      // куда попадает точка привязки
      TextOut(ShortToString(0x2022), xyTA_CENTER | TA_VCENTERdatawh,
         ColorToARGB(clr), COLOR_FORMAT_ARGB_NORMALIZE);
      
      // формируем текст с описанием типа привязки и угла
      const string text =  EnumToString(anchor) +
         "(" + (string)angle + CharToString(0xB0) + ")";
   
      // рисуем текст
      TextOut(textxyanchordatawh,
         ColorToARGB(clr), COLOR_FORMAT_ARGB_NORMALIZE);
   }
   ...

Остается лишь обновить картинку и график, а затем ждать команды пользователя и освободить ресурсы.

   ResourceCreate(namedatawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
   ChartRedraw();
   
   const uint timeout = GetTickCount() + Seconds * 1000;
   while(!IsStopped() && GetTickCount() < timeout)
   {
      Sleep(1000);
   }
   
   ObjectDelete(0name);
   ResourceFree("::" + name);
}

Вот что получилось в результате.

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

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

Дополнительно, для самостоятельного изучения к книге прилагается игрушечный графический редактор SimpleDrawing.mq5. Он выполнен как безбуферный индикатор и использует в своей работе рассмотренные ранее классы фигур (см. пример ResourceShapesDraw.mq5). Они практически без изменений вынесены в заголовочный файл ShapesDrawing.mqh. Но если ранее фигуры генерировал случайным образом скрипт, то теперь их может выбирать и наносить на график пользователь. Для этой цели реализован интерфейс с цветовой палитрой и панелью кнопок, по количеству зарегистрированных классов фигур. Весь интерфейс поддерживается классом SimpleDrawing (SimpleDrawing.mqh).

Простой графический редактор

Простой графический редактор

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

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

Когда один из типов фигур выбран в панели (одна из кнопок "активна"), щелчок мышью в рабочей области рисования (остальная часть графика, обозначенная затемнением) выводит в этом месте фигуру предопределенного размера. Кнопка при этом "отключается". В таком состоянии, когда все кнопки неактивны, можно двигать мышью фигуры по рабочей области. Если держать нажатой клавишу Ctrl, вместо перемещения производится изменение размера фигуры. "Горячая точка" находится в центре каждой фигуры (размер чувствительной области задан макросом в исходном коде и, вероятно, потребует увеличения для дисплеев с очень высоким DPI).

Обратите внимание, что редактор включает идентификатор графика (ChartID) в имена создаваемых ресурсов. Это позволяет запускать редактор параллельно на нескольких графиках.