English
preview
Торговые инструменты на MQL5 (Часть 16): Улучшенное сглаживание методом суперсэмплинга (SSAA) и рендеринг в высоком разрешении

Торговые инструменты на MQL5 (Часть 16): Улучшенное сглаживание методом суперсэмплинга (SSAA) и рендеринг в высоком разрешении

MetaTrader 5Торговые системы |
57 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 15) мы разработали панель на базе canvas в MetaQuotes Language 5 (MQL5), которая включает в себя эффекты размытия, рендеринг теней и плавную прокрутку колеса мыши для интерактивного мониторинга рынка, включая визуальные панели и настраиваемые темы оформления. В Части 16 мы улучшим панель с помощью методов сглаживания и рендеринга с высоким разрешением, используя суперсэмплинг для получения более плавной графики, рамок и элементов. Это усовершенствование включает в себя области canvas с высоким разрешением для статистики и текстовые панели, точные функции рисования округлых форм и улучшенную интерактивность для улучшения визуального качества. В статье рассмотрим следующие темы:

  1. Изучение методов сглаживания и рендеринга с высоким разрешением
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

К концу статьи у вас будет обновленная панель на MQL5 с улучшенным рендерингом для более четкой и профессиональной визуализации рынка. Давайте приступим!


Изучение методов сглаживания и рендеринга с высоким разрешением

Фреймворк сглаживания и рендеринга с высоким разрешением устраняет визуальные артефакты в цифровой графике, такие как рваные края или "зубчатость изображения", возникающие, когда непрерывные фигуры отображаются на дискретной пиксельной сетке. Зубчатость проявляется в виде ступенчатых паттернов на линиях, кривых или рамках, снижая четкость и профессионализм отображения, например, на наших торговых панелях. Для смягчения такого эффекта, такие методы, как суперсэмплинг, позволяют визуализировать сцены с более высоким разрешением — обычно кратным целевому размеру, — а затем понижать дискретизацию путем усреднения значений пикселей, сглаживания краев с помощью смешивания цветов и создания более естественного внешнего вида. Ниже приведен пример процесса суперсэмплинга.

SUPER-SAMPLING

С другой стороны, рендеринг с высоким разрешением дополняет сглаживание, используя большие canvas для захвата более мелких деталей перед уменьшением дискретизации, повышая общее качество изображения без увеличения конечного размера. Это особенно важно в приложениях на MQL5, где точная визуализация рыночных данных, таких как графики и статистика, улучшает понимание пользователем информации и принятие решений за счет минимизации искажений. Например, бикубическая интерполяция, сложный метод повторной выборки, вычисляет новые значения пикселей, используя средневзвешенное значение окружающих пикселей, сохраняя плавность во время операций масштабирования и способствуя превосходному сглаживанию результатов.

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

ENHANCEMENT OBJECTIVES


Реализация средствами MQL5

Чтобы усовершенствовать программу на MQL5, нам нужно будет добавить новые определения, глобальные переменные и входные параметры для управления новыми улучшениями, такими как показаны ниже, для рендеринга в высоком разрешении и возможности суперсэмплинга.

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART4.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

// Added two new canvas objects for high-resolution rendering
//+------------------------------------------------------------------+
//| Canvas objects                                                   |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                    //--- Declare graph canvas object
CCanvas canvasStats;                    //--- Declare stats canvas object
CCanvas canvasStatsHighRes;             //--- Declare stats high-res canvas object
CCanvas canvasHeader;                   //--- Declare header canvas object
CCanvas canvasText;                     //--- Declare text canvas object
CCanvas canvasTextHighRes;              //--- Declare text high-res canvas object

// Added names for the new high-resolution canvases
//+------------------------------------------------------------------+
//| Canvas names                                                     |
//+------------------------------------------------------------------+
string canvasGraphName = "GraphCanvas"; //--- Set graph canvas name
string canvasStatsName = "StatsCanvas"; //--- Set stats canvas name
string canvasStatsHighResName = "StatsCanvasHighRes"; //--- Set stats high-res name
string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name
string canvasTextName = "TextCanvas";   //--- Set text canvas name
string canvasTextHighResName = "TextCanvasHighRes"; //--- Set text high-res name

// New group
sinput group "=== TEXT PANEL SETTINGS ==="
input int TriangleRoundRadius         = 1;                              // Triangle Round Radius
input double TriangleBaseWidthPercent = 65.0;                           // Triangle Base Width Percent (of button size)
input double TriangleHeightPercent    = 70.0;                           // Triangle Height Percent (of base width)
input bool ShowUpDownButtons          = false;                          // Show Up/Down Buttons
input int TextFontSize = 17;                                            // Text Font Size

// Added a new global variable for supersampling factor
const int smoothness_factor = 10;  // Higher = smoother drag (e.g., 20 for more)
const int supersamplingFactor = 4;      // Supersampling for smooth rounds

Сначала мы расширяем объекты canvas, объявляя дополнительные версии с высоким разрешением для панелей статистики и текстовых панелей, в частности "canvasStatsHighRes" и "canvasTextHighRes", наряду с существующими "canvasGraph", "canvasStats", "canvasHeader" и "canvasText" для поддержки рендеринга с использованием суперсэмплирования. Затем мы также определяем соответствующие имена для этих новых canvas с высоким разрешением, добавляя для "canvasStatsHighResName" значение "StatsCanvasHighRes", а для "canvasTextHighResName" значение "TextCanvasHighRes", которые будут использоваться для идентификации и создания в программе. Для большей ясности мы выделили конкретные изменения.

В новой группе ввода с меткой "=== TEXT PANEL SETTINGS ===" мы вводим настраиваемые пользователем параметры, включая "TriangleRoundRadius" для управления округлением углов стрелок, "TriangleBaseWidthPercent" и "TriangleHeightPercent" для настройки пропорций стрелок относительно размера кнопки, "ShowUpDownButtons" для переключения видимости кнопок прокрутки, и "TextFontSize" для настройки размера отображаемого текста. В частности, мы хотим обновить полосу прокрутки, чтобы она соответствовала новой версии Windows 11, последней версии от 2026 года. Наконец, мы определяем две константы: "smoothness_factor", значение которых инициализируется равным 10 для более плавного взаимодействия при прокрутке, и "supersamplingFactor", значение которого устанавливается равным 4 для обеспечения рендеринга с высоким разрешением путем умножения размеров пикселей для сглаживания перед понижением дискретизации.

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

if (EnableStatsPanel) {                                   //--- Check stats panel enabled
   int statsX = currentCanvasX + currentWidth + PanelGap; //--- Compute stats X
   if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats canvas
      Print("Failed to create Stats Canvas");             //--- Log creation failure
   }
   if (!canvasStatsHighRes.Create(canvasStatsHighResName, (currentWidth / 2) * supersamplingFactor, currentHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats high-res
      Print("Failed to create Stats High-Res Canvas");    //--- Log creation failure
   }
   statsCreated = true;                                   //--- Set stats created flag
}

if (EnableTextPanel) {                                    //--- Check text panel enabled
   int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y
   int text_width = inner_header_width;                   //--- Set text width
   int text_height = TextPanelHeight;                     //--- Set text height
   if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, text_width, text_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text canvas
      Print("Failed to create Text Canvas");              //--- Log creation failure
   }
   if (!canvasTextHighRes.Create(canvasTextHighResName, text_width * supersamplingFactor, text_height * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text high-res
      Print("Failed to create Text High-Res Canvas");     //--- Log creation failure
   }
   textCreated = true;                                    //--- Set text created flag
}

В обработчике OnInit мы проверяем, включена ли панель статистики с помощью входного параметра "EnableStatsPanel", и если да, мы вычисляем положение X для canvas статистики, добавляя текущее значение X canvas, ширину и зазор между панелями. Затем мы создаем стандартный canvas статистики, используя метод "CreateBitmapLabel" в объекте "canvasStats", указывая идентификатор графика, подокно, название, положение, размеры и цветовой формат COLOR_FORMAT_ARGB_NORMALIZE, при этом выводим сообщение об ошибке, если создание завершается неудачно, точно так же, как мы делали с предыдущей версией. Затем мы создаем canvas статистики с высоким разрешением с помощью метода Create для объекта "canvasStatsHighRes", используя имя с высоким разрешением, масштабированную ширину путём умножения половины текущей ширины на "supersamplingFactor", масштабированную высоту аналогичным образом и тот же цветовой формат, снова выводя ошибку, прежде чем установить флаг "statsCreated" в значение true.

Аналогично, если текстовая панель включена с помощью параметра "EnableTextPanel", мы вычисляем положение по оси Y для текстового canvas, складывая текущее значение по оси Y класса canvas, высоту заголовка, отступы, текущую высоту и отступ панели, а затем устанавливаем ширину текста в соответствии с шириной и высотой внутреннего заголовка, равными значению параметра "TextPanelHeight". Мы создаём стандартный текстовый canvas с помощью функции "CreateBitmapLabel" для объекта "canvasText", обеспечивая необходимые параметры и выводя сообщения об ошибке. Затем мы создаём текстовый canvas высокого разрешения, используя функцию "Create" в "canvasTextHighRes" с масштабированными размерами на основе параметра «supersamplingFactor», регистрируем в лог любые проблемы, а затем устанавливаем флаг «textCreated» в значение true. Поскольку мы создали новые объекты canvas, необходимо рассмотреть возможность их уничтожения, когда они не нужны в соответствующих местах, так же, как и в случае с обычными объектами. Например, при деинициализации мы используем следующую логику.

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {           // Deinitialize expert advisor
   canvasHeader.Destroy();                  //--- Destroy header canvas
   if (graphCreated) canvasGraph.Destroy(); //--- Destroy graph if created
   if (statsCreated) {
      canvasStats.Destroy();                //--- Destroy stats if created
      canvasStatsHighRes.Destroy();         //--- Destroy stats high-res
   }
   if (textCreated) {
      canvasText.Destroy();                 //--- Destroy text if created
      canvasTextHighRes.Destroy();          //--- Destroy text high-res
   }
   ChartRedraw();                           //--- Redraw chart
}

Мы уничтожаем объекты canvas с высоким разрешением только тогда, когда они не нужны. Мы используем тот же формат в функциях "ToggleMinimize" и "CloseDashboard". Далее нам нужно будет определить логику рисования новых причудливых закругленных прямоугольников для заголовков статистики и закругленных треугольников для кнопок полосы прокрутки, поскольку мы хотим отказаться от статичных и нарисовать свои собственные. Мы используем следующую логику.

//+------------------------------------------------------------------+
//| Draw rectangle corner arc with exact boundaries                  |
//+------------------------------------------------------------------+
void DrawRectCornerArcPrecise(CCanvas &cvs, int centerX, int centerY, int radius, int thickness, uint borderARGB,
                              double startAngle, double endAngle) {
   int halfThickness = thickness / 2;
   double outerRadius = (double)radius + halfThickness;
   double innerRadius = (double)radius - halfThickness;
   if(innerRadius < 0) innerRadius = 0;

   int pixelRange = (int)(outerRadius + 2);

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) {
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) {
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY);
         if(distance < innerRadius || distance > outerRadius) continue;

         double angle = MathArctan2((double)deltaY, (double)deltaX);
         
         if(IsAngleBetween(angle, startAngle, endAngle))
            cvs.PixelSet(centerX + deltaX, centerY + deltaY, borderARGB);
      }
   }
}

//+------------------------------------------------------------------+
//| Fill quadrilateral                                               |
//+------------------------------------------------------------------+
void FillQuadrilateral(CCanvas &cvs, double &verticesX[], double &verticesY[], uint fillColor) {
   double minY = verticesY[0], maxY = verticesY[0];
   for(int i = 1; i < 4; i++) {
      if(verticesY[i] < minY) minY = verticesY[i];
      if(verticesY[i] > maxY) maxY = verticesY[i];
   }

   int yStart = (int)MathCeil(minY);
   int yEnd = (int)MathFloor(maxY);

   for(int y = yStart; y <= yEnd; y++) {
      double scanlineY = (double)y + 0.5;
      double xIntersections[8];
      int intersectionCount = 0;

      for(int i = 0; i < 4; i++) {
         int nextIndex = (i + 1) % 4;
         double x0 = verticesX[i], y0 = verticesY[i];
         double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex];

         double edgeMinY = (y0 < y1) ? y0 : y1;
         double edgeMaxY = (y0 > y1) ? y0 : y1;

         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         if(MathAbs(y1 - y0) < 1e-12) continue;

         double interpolationFactor = (scanlineY - y0) / (y1 - y0);
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         xIntersections[intersectionCount++] = x0 + interpolationFactor * (x1 - x0);
      }

      for(int a = 0; a < intersectionCount - 1; a++)
         for(int b = a + 1; b < intersectionCount; b++)
            if(xIntersections[a] > xIntersections[b]) {
               double temp = xIntersections[a];
               xIntersections[a] = xIntersections[b];
               xIntersections[b] = temp;
            }

      for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) {
         int xLeft = (int)MathCeil(xIntersections[pairIndex]);
         int xRight = (int)MathFloor(xIntersections[pairIndex + 1]);
         for(int x = xLeft; x <= xRight; x++)
            cvs.PixelSet(x, y, fillColor);
      }
   }
}

//+------------------------------------------------------------------+
//| Normalize angle                                                  |
//+------------------------------------------------------------------+
double NormalizeAngle(double angle) {
   double twoPi = 2.0 * M_PI;
   angle = MathMod(angle, twoPi);
   if(angle < 0) angle += twoPi;
   return angle;
}

//+------------------------------------------------------------------+
//| Is angle between                                                 |
//+------------------------------------------------------------------+
bool IsAngleBetween(double angle, double startAngle, double endAngle) {
   angle = NormalizeAngle(angle);
   startAngle = NormalizeAngle(startAngle);
   endAngle = NormalizeAngle(endAngle);
   
   double span = NormalizeAngle(endAngle - startAngle);
   double relativeAngle = NormalizeAngle(angle - startAngle);
   
   return relativeAngle <= span;
}

Здесь мы создаем функцию "DrawRectCornerArcPrecise" для отображения точных угловых дуг для закругленных рамок прямоугольника на объекте canvas с высоким разрешением, вычисляем половину толщины, чтобы определить внутренний и внешний радиусы, затем выполняем перебор по диапазону пикселей вокруг центра, чтобы проверить расстояния и углы, устанавливая пиксели только в том случае, если они попадают в пределы сегмента дуги, определяемого начальным и конечным углами. Мы реализуем функцию "FillQuadrilateral" для заполнения произвольных четырехугольных форм, используя алгоритм сканирования строк, сначала определяя минимальные и максимальные границы Y от вершин, затем для каждой строки сканирования по оси Y вычисляя пересечения ребер, сортируя их и заполняя горизонтальные промежутки между парами пересечений, чтобы обеспечить полное покрытие без пробелов.

Мы определяем функцию "NormalizeAngle" для стандартизации углов в диапазоне от 0 до 2 * pi с помощью операции взятия остатка от деления и корректировки отрицательных значений. Эта стандартизация обеспечивает единообразное сравнение углов в круговых геометрических формах, таких как дуги. Мы добавляем функцию "IsAngleBetween", чтобы определить, находится ли данный угол между начальным и конечным углами. Мы нормализуем все входные параметры, вычисляем линейную оболочку и проверяем относительное положение, поддерживая движение по часовой или против часовой стрелки для гибкого отображения дуг. Теперь можно использовать эти функции для рисования скругленного прямоугольника, как показано ниже.

//+------------------------------------------------------------------+
//| Fill rounded rectangle                                           |
//+------------------------------------------------------------------+
void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color) { // Render rounded fill
   if (radius <= 0) {                                                           //--- Check zero radius
      cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color);                //--- Fill rectangle
      return;                                                                   //--- Exit function
   }
   radius = MathMin(radius, MathMin(w / 2, h / 2));                             //--- Adjust radius

   cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc
   cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc
   cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc
   cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc

   cvs.FillCircle(x + radius, y + radius, radius, argb_color);                  //--- Fill top-left circle
   cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color);          //--- Fill top-right circle
   cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color);  //--- Fill bottom-right circle
   cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color); //--- Fill bottom-left circle

   cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal body

   cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical body
}

//+------------------------------------------------------------------+
//| Draw rounded rectangle border                                    |
//+------------------------------------------------------------------+
void DrawRoundedRectangleBorderHiRes(CCanvas &cvs, int positionX, int positionY, int width, int height, int radius, uint borderColorARGB, int thickness) {
   int scaledThickness = thickness;

   DrawRectStraightEdge(cvs, positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB);

   DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, 
                            M_PI, M_PI * 1.5);
   DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 1.5, M_PI * 2.0);
   DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 0.5, M_PI);
   DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            0.0, M_PI * 0.5);
}

//+------------------------------------------------------------------+
//| Draw straight edge for rectangle border                          |
//+------------------------------------------------------------------+
void DrawRectStraightEdge(CCanvas &cvs, double startX, double startY, double endX, double endY, int thickness, uint borderARGB) {
   double deltaX = endX - startX;
   double deltaY = endY - startY;
   double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY);
   if(edgeLength < 1e-6) return;

   double perpendicularX = -deltaY / edgeLength;
   double perpendicularY = deltaX / edgeLength;

   double edgeDirectionX = deltaX / edgeLength;
   double edgeDirectionY = deltaY / edgeLength;

   double halfThickness = (double)thickness / 2.0;
   
   double extensionLength = 1.5;
   double extendedStartX = startX - edgeDirectionX * extensionLength;
   double extendedStartY = startY - edgeDirectionY * extensionLength;
   double extendedEndX = endX + edgeDirectionX * extensionLength;
   double extendedEndY = endY + edgeDirectionY * extensionLength;

   double verticesX[4], verticesY[4];
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;  verticesY[0] = extendedStartY - perpendicularY * halfThickness;
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;  verticesY[1] = extendedStartY + perpendicularY * halfThickness;
   verticesX[2] = extendedEndX + perpendicularX * halfThickness;  verticesY[2] = extendedEndY + perpendicularY * halfThickness;
   verticesX[3] = extendedEndX - perpendicularX * halfThickness;  verticesY[3] = extendedEndY - perpendicularY * halfThickness;

   FillQuadrilateral(cvs, verticesX, verticesY, borderARGB);
}

Чтобы создать скругленный прямоугольник, мы сначала реализуем функцию "FillRoundedRectangle" для отображения закругленных прямоугольников с заливкой на объекте canvas, сначала обрабатывая случаи с нулевым радиусом, заполняя стандартный прямоугольник, затем регулируя радиус так, чтобы он соответствовал половине ширины или высоты, рисуя дуги в четверть дуги в каждом углу, используя метод Arc с преобразованием градусов в радианы, заливая полные круги в углах для полного покрытия и, наконец, заливая горизонтальные и вертикальные участки тела, чтобы плавно соединить закругленные части.

Затем мы создаём функцию "DrawRoundedRectangleBorderHiRes" для высокоточной отрисовки рамок вокруг скругленных прямоугольников, масштабируем толщину, вызываем функцию "DrawRectStraightEdge" для отрисовки четырёх прямых сторон и используем функцию "DrawRectCornerArcPrecise" для каждой угловой дуги с заданными начальными и конечными углами в радианах (например, от M_PI до M_PI * 1,5 для верхнего левого угла), обеспечивая точные, сглаженные контуры без наложений.

Наконец, в функции "DrawRectStraightEdge" мы вычисляем дельты и нормализуем векторы для направления и перпендикуляра ребер, вычисляем половину толщины для ширины рамки, слегка удлиняем линию за пределы конечных точек, чтобы обеспечить плавные соединения углов, определяем четыре вершины для четырехугольной полосы вдоль ребра и передаем их в функцию "FillQuadrilateral" для заполнения сегмента рамки, создавая непрерывную, высококачественную прямую линию с помощью векторной геометрии. Это функции, которые мы будем вызывать для создания желаемого скругленного прямоугольника. Теперь можем определить логику округленного треугольника. Сначала нам понадобятся вспомогательные функции.

//+------------------------------------------------------------------+
//| Precompute triangle geometry                                     |
//+------------------------------------------------------------------+
void PrecomputeTriangleGeometry(double &sharpX[], double &sharpY[], int radius, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) {
   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {
      int previousIndex = (cornerIndex + 2) % 3;
      int nextIndex = (cornerIndex + 1) % 3;

      double edgeA_X = sharpX[cornerIndex] - sharpX[previousIndex], edgeA_Y = sharpY[cornerIndex] - sharpY[previousIndex];
      double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y);
      edgeA_X /= edgeA_Length; edgeA_Y /= edgeA_Length;

      double edgeB_X = sharpX[nextIndex] - sharpX[cornerIndex], edgeB_Y = sharpY[nextIndex] - sharpY[cornerIndex];
      double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y);
      edgeB_X /= edgeB_Length; edgeB_Y /= edgeB_Length;

      double normalA_X = edgeA_Y, normalA_Y = -edgeA_X;
      double normalB_X = edgeB_Y, normalB_Y = -edgeB_X;

      double bisectorX = normalA_X + normalB_X, bisectorY = normalA_Y + normalB_Y;
      double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY);
      if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); }
      bisectorX /= bisectorLength; bisectorY /= bisectorLength;

      double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y;
      if(cosInteriorAngle > 1.0) cosInteriorAngle = 1.0;
      if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0;
      double halfAngle = MathArccos(cosInteriorAngle) / 2.0;
      double sinHalfAngle = MathSin(halfAngle);
      if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12;

      double distanceToCenter = radius / sinHalfAngle;
      arcCentersX[cornerIndex] = sharpX[cornerIndex] + bisectorX * distanceToCenter;
      arcCentersY[cornerIndex] = sharpY[cornerIndex] + bisectorY * distanceToCenter;

      double deltaX_A = sharpX[cornerIndex] - sharpX[previousIndex], deltaY_A = sharpY[cornerIndex] - sharpY[previousIndex];
      double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A;
      double interpolationFactor_A = ((arcCentersX[cornerIndex] - sharpX[previousIndex])*deltaX_A + (arcCentersY[cornerIndex] - sharpY[previousIndex])*deltaY_A) / lengthSquared_A;
      tangentX[cornerIndex][1] = sharpX[previousIndex] + interpolationFactor_A * deltaX_A;
      tangentY[cornerIndex][1] = sharpY[previousIndex] + interpolationFactor_A * deltaY_A;

      double deltaX_B = sharpX[nextIndex] - sharpX[cornerIndex], deltaY_B = sharpY[nextIndex] - sharpY[cornerIndex];
      double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B;
      double interpolationFactor_B = ((arcCentersX[cornerIndex] - sharpX[cornerIndex])*deltaX_B + (arcCentersY[cornerIndex] - sharpY[cornerIndex])*deltaY_B) / lengthSquared_B;
      tangentX[cornerIndex][0] = sharpX[cornerIndex] + interpolationFactor_B * deltaX_B;
      tangentY[cornerIndex][0] = sharpY[cornerIndex] + interpolationFactor_B * deltaY_B;

      startAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][1] - arcCentersY[cornerIndex], tangentX[cornerIndex][1] - arcCentersX[cornerIndex]);
      endAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][0] - arcCentersY[cornerIndex], tangentX[cornerIndex][0] - arcCentersX[cornerIndex]);
   }
}

//+------------------------------------------------------------------+
//| Angle in arc sweep for triangle                                  |
//+------------------------------------------------------------------+
bool TriangleAngleInArcSweep(double startAngle, double endAngle, double angle) {
   double twoPi = 2.0 * M_PI;
   double startAngleMod = MathMod(startAngle + twoPi, twoPi);
   double endAngleMod = MathMod(endAngle + twoPi, twoPi);
   angle = MathMod(angle + twoPi, twoPi);

   double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi);

   if(ccwSpan <= M_PI) {
      double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi);
      return(relativeAngle <= ccwSpan + 1e-6);
   } else {
      double cwSpan = twoPi - ccwSpan;
      double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi);
      return(relativeAngle <= cwSpan + 1e-6);
   }
}

Мы определяем функцию "PrecomputeTriangleGeometry" для вычисления необходимых геометрических элементов для отображения скругленных треугольников, таких как те, которые мы будем использовать в наших пользовательских значках стрелок, путем перебора каждого из трех углов с помощью цикла с параметром "cornerIndex" от 0 до 2. Для каждого угла мы определяем предыдущий и следующий индексы по модулю 3 для циклического доступа, вычисляем нормализованные векторы ребер от острых вершин "sharpX" и "sharpY" до смежных точек и выводим внешние нормали, поворачивая эти векторы на 90 градусов. Затем формируем биссектрису угла, суммируя нормали, нормализуя ее после обработки случаев с почти нулевой длиной, и вычисляем косинус внутреннего угла с помощью скалярного произведения отрицательных векторов ребер, ограничивая его значение диапазоном от -1 до 1, прежде чем найти половинный угол и его синус, чтобы избежать деления на ноль.

Используя радиус, деленный на этот синус, мы располагаем центр дуги вдоль биссектрисы от угла, обеспечивая равномерное скругление; затем проецируем центр дуги на ребра, чтобы найти точки касания, хранящиеся в "tangentX" и "tangentY", и вычисляем начальный и конечный углы с помощью MathArctan2 для определения направления и угла движения дуги в этом углу.

Этот предварительный расчет имеет решающее значение для точного рендеринга округлых форм со сглаживанием, поскольку он преобразует острые вершины треугольников в плавные кривые, определяя центры дуг "arcCentersX" и "arcCentersY", касательные и углы "startAngles" и "endAngles", которые позволяют выполнять векторную заливку без пиксельных искажений, что значительно улучшает визуальное качество элементов пользовательского интерфейса, таких как стрелки прокрутки, благодаря возможности точной проверки границ во время итерации пикселей.

Затем реализуем функцию "TriangleAngleInArcSweep", чтобы проверить, попадает ли данный угол в дугу, определяемую начальным и конечным углами, предварительно нормализуя все углы до значений от 0 до 2 * pi с помощью MathMod и добавляя 2 * pi для положительных значений. Мы вычисляем хорду против часовой стрелки и, если она равна pi или меньше, проверяем относительный угол от начала; в противном случае используем хорду по часовой стрелке и проверяем от конца, добавляя небольшую эпсилон для точности с плавающей запятой, которая поддерживает оба направления дуги и обеспечивает корректное включение при заполнении методом сканирования строк или тестировании пикселей во время отрисовки закругленных треугольников. В принципе, если вам интересно, что это за алгоритм сканирования строк, мы немного объясним, что это такое, чтобы у вас было понимание.

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

SCANLINE ALGORITHM

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

//+------------------------------------------------------------------+
//| Fill rounded triangle                                            |
//+------------------------------------------------------------------+
void FillRoundedTriangle(CCanvas &cvs, double &sharpX[], double &sharpY[], int radius, uint fillColor, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) {
   double minY = sharpY[0], maxY = sharpY[0];
   for(int i = 1; i < 3; i++) {
      if(sharpY[i] < minY) minY = sharpY[i];
      if(sharpY[i] > maxY) maxY = sharpY[i];
   }

   int yStart = (int)MathCeil(minY);
   int yEnd = (int)MathFloor(maxY);

   for(int y = yStart; y <= yEnd; y++) {
      double scanlineY = (double)y + 0.5;

      double xIntersections[12];
      int intersectionCount = 0;

      for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
         int nextIndex = (edgeIndex + 1) % 3;
         double startX = tangentX[edgeIndex][0], startY = tangentY[edgeIndex][0];
         double endX = tangentX[nextIndex][1], endY = tangentY[nextIndex][1];

         double edgeMinY = (startY < endY) ? startY : endY;
         double edgeMaxY = (startY > endY) ? startY : endY;

         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         if(MathAbs(endY - startY) < 1e-12) continue;

         double interpolationFactor = (scanlineY - startY) / (endY - startY);
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX);
      }

      for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {
         double centerX = arcCentersX[cornerIndex], centerY = arcCentersY[cornerIndex];
         double deltaY = scanlineY - centerY;

         if(MathAbs(deltaY) > radius) continue;

         double deltaX = MathSqrt(radius*radius - deltaY*deltaY);

         double candidates[2];
         candidates[0] = centerX - deltaX;
         candidates[1] = centerX + deltaX;

         for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) {
            double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX);
            if(TriangleAngleInArcSweep(startAngles[cornerIndex], endAngles[cornerIndex], angle))
               xIntersections[intersectionCount++] = candidates[candidateIndex];
         }
      }

      for(int a = 0; a < intersectionCount - 1; a++)
         for(int b = a + 1; b < intersectionCount; b++)
            if(xIntersections[a] > xIntersections[b]) {
               double temp = xIntersections[a];
               xIntersections[a] = xIntersections[b];
               xIntersections[b] = temp;
            }

      for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) {
         int xLeft = (int)MathCeil(xIntersections[pairIndex]);
         int xRight = (int)MathFloor(xIntersections[pairIndex + 1]);
         for(int x = xLeft; x <= xRight; x++)
            cvs.PixelSet(x, y, fillColor);
      }
   }
}

//+------------------------------------------------------------------+
//| Draw rounded triangle arrow                                      |
//+------------------------------------------------------------------+
void DrawRoundedTriangleArrow(CCanvas &cvs, int baseX, int baseY, int tri_base_width, int tri_height, bool isUp, uint fillColor) {
   int radius = TriangleRoundRadius * supersamplingFactor;

   double sharpX[3], sharpY[3];

   if (isUp) {
      sharpX[0] = baseX;
      sharpY[0] = baseY;
      sharpX[1] = baseX - tri_base_width / 2.0;
      sharpY[1] = baseY + tri_height;
      sharpX[2] = baseX + tri_base_width / 2.0;
      sharpY[2] = baseY + tri_height;
   } else {
      sharpX[0] = baseX;
      sharpY[0] = baseY + tri_height;
      sharpX[1] = baseX + tri_base_width / 2.0;  // Swapped for consistent winding
      sharpY[1] = baseY;
      sharpX[2] = baseX - tri_base_width / 2.0;
      sharpY[2] = baseY;
   }

   double arcCentersX[3], arcCentersY[3];
   double tangentX[3][2], tangentY[3][2];
   double startAngles[3], endAngles[3];

   PrecomputeTriangleGeometry(sharpX, sharpY, radius, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles);

   FillRoundedTriangle(cvs, sharpX, sharpY, radius, fillColor, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles);
}

Мы реализуем функцию "FillRoundedTriangle" для визуализации заполненных закругленных треугольников с использованием подхода, основанного на сканировании строк для точного векторного заполнения, сначала определяя минимальные и максимальные координаты Y от острых вершин "sharpY", чтобы определить вертикальные границы, затем выполняя перебор каждого целого числа y от вершины minY до минимума maxY со смещением на половину пикселя "scanlineY" для лучшего сглаживания за счет субпиксельной точности. Для каждой сканируемой строки мы вычисляем до 12 x-пересечений: сначала для трех прямых касательных ребер путем линейной интерполяции между точками касания "tangentX" и "tangentY", если сканируемая строка попадает в диапазон Y ребра, а затем для каждого из трех углов, проверяя, пересекает ли сканируемая строка дугу решая для deltaX из уравнения окружности в центре дуги "arcCentersX" и "arcCentersY" с заданным радиусом, добавляя возможные значения x только в том случае, если их углы проходят проверку "TriangleAngleInArcSweep", чтобы убедиться, что они лежат в пределах размаха дуги, определенного "startAngles" и "endAngles".

Мы сортируем накопленные x-пересечения в порядке возрастания, используя простую пузырьковую сортировку для повышения эффективности работы с небольшими массивами, затем заполняем горизонтальные промежутки между каждой парой пересечений, устанавливая пиксели от вершины слева по оси x до минимума справа по оси x с помощью предусмотренного "fillColor", что приводит к получению сглаженного, не содержащего артефакта, закругленного треугольника, который использует предварительно вычисленную геометрию для высококачественного рендеринга элементов пользовательского интерфейса, таких как стрелки. Этот метод сканирования строк важен тем, что он позволяет с пиксельной точностью заполнять сложные криволинейные формы, не полагаясь на аппроксимацию, обеспечивая сглаживание краев за счет точного определения пересечений границ, что имеет решающее значение для сохранения четкости изображения в условиях масштабирования или высокого разрешения, когда традиционное заполнение полигонов может привести к появлению «зубцов» или разрывов.

Мы создаем функцию "DrawRoundedTriangleArrow", чтобы рисовать закругленные треугольные стрелки вверх или вниз для наших кнопок прокрутки, масштабируем "TriangleRoundRadius" с помощью "supersamplingFactor" в соответствии с рендерингом в высоком разрешении, определяем три острые вершины "sharpX" и "sharpY" на основе положения основания, ширину основания треугольника "tri_base_width", высоту "tri_height" и направление "isUp" — для стрелок "вверх", располагая точку вверху, а основание - внизу, а для стрелок "вниз" - инвертирование с заменой базовых точек для обеспечения единообразного порядка обхода, чтобы избежать искажений заполнения. Затем мы вызываем функцию "PrecomputeTriangleGeometry" для вычисления центров дуг, касательных и углов относительно этих вершин и радиуса, сохраняя их в массивах "arcCentersX", "arcCentersY", "tangentX", "tangentY", "startAngles" и "endAngles", после чего вызываем функцию "FillRoundedTriangle" с этими параметрами и значением "fillColor" для фактического рендеринга, что позволяет получить плавные, закругленные стрелки, которые органично вписываются в полосу прокрутки, улучшая эстетику и удобство использования пользовательского интерфейса. Мы выполнили повышение дискретизации; пришло время выполнить даунсэмплинг или понижение дискретизации для целевого, финального рендеринга объекта canvas.

//+------------------------------------------------------------------+
//| Downsample canvas (average for AA)                               |
//+------------------------------------------------------------------+
void DownsampleCanvas(CCanvas &targetCanvas, CCanvas &highResCanvas) {
   int targetWidth  = targetCanvas.Width();
   int targetHeight = targetCanvas.Height();

   for(int pixelY = 0; pixelY < targetHeight; pixelY++) {
      for(int pixelX = 0; pixelX < targetWidth; pixelX++) {
         double sourceX = pixelX * supersamplingFactor;
         double sourceY = pixelY * supersamplingFactor;

         double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0;
         double weightSum = 0;

         for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) {
            for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) {
               int sourcePixelX = (int)(sourceX + deltaX);
               int sourcePixelY = (int)(sourceY + deltaY);

               if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) {
                  uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY);

                  uchar alpha = (uchar)((pixelValue >> 24) & 0xFF);
                  uchar red = (uchar)((pixelValue >> 16) & 0xFF);
                  uchar green = (uchar)((pixelValue >> 8)  & 0xFF);
                  uchar blue = (uchar)(pixelValue         & 0xFF);

                  double weight = 1.0;
                  sumAlpha += alpha * weight;
                  sumRed += red * weight;
                  sumGreen += green * weight;
                  sumBlue += blue * weight;
                  weightSum += weight;
               }
            }
         }

         if(weightSum > 0) {
            uchar finalAlpha = (uchar)(sumAlpha / weightSum);
            uchar finalRed = (uchar)(sumRed / weightSum);
            uchar finalGreen = (uchar)(sumGreen / weightSum);
            uchar finalBlue = (uchar)(sumBlue / weightSum);

            uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) |
                              ((uint)finalGreen << 8)  | (uint)finalBlue;
            targetCanvas.PixelSet(pixelX, pixelY, finalColor);
         }
      }
   }
}

Мы определяем функцию "DownsampleCanvas" для выполнения сглаживания путем понижения высокого разрешения canvas до целевого размера путем усреднения по пикселям, извлекая целевые размеры из "targetCanvas.Width()" и "targetCanvas.Height()", затем выполняем перебор каждого целевого пикселя с помощью вложенных циклов для "pixelY" и "pixelX". Для каждого целевого пикселя мы сопоставляем его с исходной областью, умножая координаты на "supersamplingFactor", чтобы получить "sourceX" и "sourceY", инициализируя суммы для альфа-канала, красного, зеленого, синего цветов и счетчика "weightSum", после чего выполняем вложенные циклы по "deltaY" и "deltaX" от 0 до "supersamplingFactor" - 1 и отбираем соответствующие пиксели высокого разрешения.

Мы рассчитываем позиции исходных пикселей, проверяем границы внутри "highResCanvas.Width()" и "highResCanvas.Height()", извлекаем компоненты ARGB с помощью PixelGet и битовых сдвигов, добавляем взвешенные вклады (с равномерным весом 1.0) к суммам, и если значение weightSum положительно, усредняем каждый компонент для вычисления окончательных значений uchar, объединяем их в finalColor со сдвигами и устанавливаем его на целевом изображении с помощью метода  PixelSet. Этот процесс понижения дискретизации необходим для получения плавных визуальных эффектов со сглаживанием за счет смешивания нескольких изображений высокого разрешения на целевой пиксель, уменьшения «рваных» краев в визуализируемых элементах, таких как границы и фигуры, и, таким образом, повышения общего качества графики на панели без больших вычислительных затрат. Теперь у нас есть все необходимые вспомогательные функции, так что мы можем приступать к работе. Начнём с обновления canvas для статистики, который теперь включает в себя рисование на большом canvas с высоким разрешением, а затем уменьшим его до обычного размера.

//+------------------------------------------------------------------+
//| Update stats on canvas                                           |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas() {            // Render stats elements
   canvasStatsHighRes.Erase(0);         //--- Clear high-res stats canvas

   int statsWidthHighRes = (currentWidth / 2) * supersamplingFactor; //--- Scaled width
   int heightHighRes = currentHeight * supersamplingFactor;          //--- Scaled height

   if (UseBackground && ArraySize(bg_pixels_stats) == (currentWidth / 2) * currentHeight) { //--- Check background valid
      uint bg_pixels_stats_high[];                                   //--- High-res bg
      ArrayResize(bg_pixels_stats_high, statsWidthHighRes * heightHighRes); //--- Resize
      ScaleImage(bg_pixels_stats_high, currentWidth / 2, currentHeight, statsWidthHighRes, heightHighRes); //--- Scale bg
      for (int y = 0; y < heightHighRes; y++) {                      //--- Loop Y
         for (int x = 0; x < statsWidthHighRes; x++) {               //--- Loop X
            canvasStatsHighRes.PixelSet(x, y, bg_pixels_stats_high[y * statsWidthHighRes + x]); //--- Set pixel
         }
      }
   }

   if (StatsBackgroundMode != NoColor) {                             //--- Check background mode
      for (int y = 0; y < heightHighRes; y++) {                      //--- Loop rows
         double factor = (double)y / (heightHighRes - 1);            //--- Compute factor
         color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color
         uchar alpha = (uchar)(255 * BackgroundOpacity);             //--- Compute alpha
         uint argbFill = ColorToARGB(currentColor, alpha);           //--- Convert to ARGB

         for (int x = 0; x < statsWidthHighRes; x++) {               //--- Loop columns
            uint currentPixel = canvasStatsHighRes.PixelGet(x, y);   //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels
            canvasStatsHighRes.PixelSet(x, y, blendedPixel);         //--- Set blended pixel
         }
      }
   }

   if (StatsBackgroundMode != NoColor) {                              //--- Check background mode for borders
      double reduction = BorderOpacityPercentReduction / 100.0;       //--- Compute reduction
      double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity
      uchar alpha = (uchar)(255 * opacity);                           //--- Set alpha
      double darkenReduction = BorderDarkenPercent / 100.0;           //--- Compute darken reduction
      double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Compute darken factor

      for (int y = 0; y < heightHighRes; y++) {                       //--- Loop vertical borders
         double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (heightHighRes - 1); //--- Get factor
         color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base color
         color darkColor = DarkenColor(baseColor, darkenFactor);      //--- Darken color
         uint argb = ColorToARGB(darkColor, alpha);                   //--- Convert to ARGB

         canvasStatsHighRes.PixelSet(0, y, argb);                     //--- Set left border pixel
         canvasStatsHighRes.PixelSet(1, y, argb);                     //--- Set inner left pixel

         canvasStatsHighRes.PixelSet(statsWidthHighRes - 1, y, argb); //--- Set right border pixel
         canvasStatsHighRes.PixelSet(statsWidthHighRes - 2, y, argb); //--- Set inner right pixel
      }

      double factorTop = 0.0;                                         //--- Set top factor
      color baseTop = GetTopColor();                                  //--- Get top base
      color darkTop = DarkenColor(baseTop, darkenFactor);             //--- Darken top
      uint argbTop = ColorToARGB(darkTop, alpha);                     //--- Convert top to ARGB
      for (int x = 0; x < statsWidthHighRes; x++) {                   //--- Loop top borders
         canvasStatsHighRes.PixelSet(x, 0, argbTop);                  //--- Set top pixel
         canvasStatsHighRes.PixelSet(x, 1, argbTop);                  //--- Set inner top pixel
      }

      double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor
      color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base
      color darkBot = DarkenColor(baseBot, darkenFactor);                  //--- Darken bottom
      uint argbBot = ColorToARGB(darkBot, alpha);                          //--- Convert bottom to ARGB
      for (int x = 0; x < statsWidthHighRes; x++) {                        //--- Loop bottom borders
         canvasStatsHighRes.PixelSet(x, heightHighRes - 1, argbBot);       //--- Set bottom pixel
         canvasStatsHighRes.PixelSet(x, heightHighRes - 2, argbBot);       //--- Set inner bottom pixel
      }
   } else {                                                                //--- Handle no background
      uint argbBorder = ColorToARGB(GetBorderColor(), 255);                //--- Convert border to ARGB
      canvasStatsHighRes.Line(0, 0, statsWidthHighRes - 1, 0, argbBorder); //--- Draw top border
      canvasStatsHighRes.Line(statsWidthHighRes - 1, 0, statsWidthHighRes - 1, heightHighRes - 1, argbBorder); //--- Draw right border
      canvasStatsHighRes.Line(statsWidthHighRes - 1, heightHighRes - 1, 0, heightHighRes - 1, argbBorder); //--- Draw bottom border
      canvasStatsHighRes.Line(0, heightHighRes - 1, 0, 0, argbBorder);     //--- Draw left border
      canvasStatsHighRes.Line(1, 1, statsWidthHighRes - 2, 1, argbBorder); //--- Draw inner top
      canvasStatsHighRes.Line(statsWidthHighRes - 2, 1, statsWidthHighRes - 2, heightHighRes - 2, argbBorder); //--- Draw inner right
      canvasStatsHighRes.Line(statsWidthHighRes - 2, heightHighRes - 2, 1, heightHighRes - 2, argbBorder); //--- Draw inner bottom
      canvasStatsHighRes.Line(1, heightHighRes - 2, 1, 1, argbBorder);     //--- Draw inner left
   }

   color labelColor = GetStatsLabelColor();   //--- Get label color
   color valueColor = GetStatsValueColor();   //--- Get value color
   color headerColor = GetStatsHeaderColor(); //--- Get header color

   int yPos = 20 * supersamplingFactor;       //--- Scaled Y position for first header
   string headerText = "Account Stats";       //--- Set header text
   int fontSizeHigh = StatsHeaderFontSize * supersamplingFactor; //--- Scaled font
   canvasStatsHighRes.FontSet("Arial Bold", fontSizeHigh);       //--- Set header font
   int textW = canvasStatsHighRes.TextWidth(headerText);         //--- Get text width
   int textH = canvasStatsHighRes.TextHeight(headerText);        //--- Get text height
   int pad = 5 * supersamplingFactor;                            //--- Scaled padding
   int rectX = (statsWidthHighRes - textW) / 2 - pad;            //--- Compute rect X
   int rectY = yPos - pad / 2;              //--- Compute rect Y
   int rectW = textW + 2 * pad;             //--- Compute rect width
   int rectH = textH + pad;                 //--- Compute rect height
   uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Compute alpha
   uint argbHeaderBg = ColorToARGB(GetStatsHeaderColor(), alpha);      //--- Convert bg to ARGB

   color header_border_color = GetStatsHeaderColor();                  //--- Significant color
   uint argbHeaderBorder = ColorToARGB(header_border_color, 255);      //--- Convert border to ARGB
   FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect
   DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 1 * supersamplingFactor); //--- Draw border

   int yPosSecond = 120 * supersamplingFactor;        //--- Scaled Y for second header
   headerText = "Current Bar Stats";                  //--- Set bar header
   textW = canvasStatsHighRes.TextWidth(headerText);  //--- Get width
   textH = canvasStatsHighRes.TextHeight(headerText); //--- Get height
   rectX = (statsWidthHighRes - textW) / 2 - pad;     //--- Compute rect X
   rectY = yPosSecond - pad / 2;                      //--- Compute rect Y
   rectW = textW + 2 * pad;                           //--- Compute rect width
   rectH = textH + pad;                               //--- Compute rect height

   FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect
   DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 2 * supersamplingFactor); //--- Draw border

   DownsampleCanvas(canvasStats, canvasStatsHighRes); //--- Downsample

   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize); //--- Set header font
   uint argbHeader = ColorToARGB(headerColor, 255); //--- Convert header to ARGB
   canvasStats.TextOut((currentWidth / 2) / 2, 20, "Account Stats", argbHeader, TA_CENTER); //--- Draw first header text

   canvasStats.TextOut((currentWidth / 2) / 2, 120, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw second header text

   canvasStats.FontSet("Arial Bold", StatsFontSize); //--- Set stats font
   uint argbLabel = ColorToARGB(labelColor, 255); //--- Convert label to ARGB
   uint argbValue = ColorToARGB(valueColor, 255); //--- Convert value to ARGB

   int yPosDisplay = 50;                // Name at 50
   canvasStats.TextOut(10, yPosDisplay, "Name:", argbLabel, TA_LEFT); //--- Draw name label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value
   yPosDisplay += 20;                   // Balance at 70

   canvasStats.TextOut(10, yPosDisplay, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value
   yPosDisplay += 20;                   // Equity at 90

   canvasStats.TextOut(10, yPosDisplay, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value
   yPosDisplay += 30;                   // Second header at 120, then labels

   yPosDisplay = 150;                   // Open at 150 (120 +30)
   canvasStats.TextOut(10, yPosDisplay, "Open:", argbLabel, TA_LEFT); //--- Draw open label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iOpen(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw open value
   yPosDisplay += 20;                   // High at 170

   canvasStats.TextOut(10, yPosDisplay, "High:", argbLabel, TA_LEFT); //--- Draw high label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iHigh(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw high value
   yPosDisplay += 20;                   // Low at 190

   canvasStats.TextOut(10, yPosDisplay, "Low:", argbLabel, TA_LEFT); //--- Draw low label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iLow(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw low value
   yPosDisplay += 20;                   // Close at 210

   canvasStats.TextOut(10, yPosDisplay, "Close:", argbLabel, TA_LEFT); //--- Draw close label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iClose(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw close value

   canvasStats.Update();                //--- Update stats canvas
}

На панели статистики мы начинаем с очистки canvas статистики высокого разрешения с помощью метода Erase,  установленного на 0, подготавливая его к новой отрисовке и вычисляя масштабированные размеры "statsWidthHighRes" и "heightHighRes", путем умножения исходной ширины статистики (половина "currentWidth") и высоты на "supersamplingFactor" для повышения детализации. Если фон включен с помощью функции "UseBackground", а массив "bg_pixels_stats" соответствует исходному размеру, мы объявляем массив фона высокого разрешения "bg_pixels_stats_high", изменяем его размер в соответствии с масштабированными размерами, масштабируем исходные пиксели с помощью "ScaleImage", которая использует бикубическую интерполяцию для сохранения плавности при увеличении, а затем перебираем в цикле каждый пиксель, чтобы установить его на canvas высокого разрешения с помощью метода PixelSet

Если параметр "StatsBackgroundMode" не равен "NoColor", мы применяем вертикальный градиент или одноцветный фон, перебирая строки с высоким разрешением, вычисляя коэффициент смешивания на основе позиции по оси Y, определяя текущий цвет с помощью "InterpolateColor", если используется градиентный режим, преобразуя в ARGB с учетом непрозрачности из "BackgroundOpacity", и смешивая каждый пиксель в строке с помощью "BlendPixels" после получения существующего значения с помощью PixelGet для достижения прозрачных оверлеев.

Для рамок в фоновых режимах мы вычисляем уменьшенную непрозрачность и коэффициент затемнения на основе входных данных, таких как "BorderOpacityPercentReduction" и "BorderDarkenPercent", затем для вертикальных рамок затемняем базовый цвет для каждой строки и устанавливаем пиксели ARGB на левом и правом краях, включая внутренние; аналогично, для верхних и нижних горизонтальных границ мы используем фиксированные коэффициенты для затемнения и заполнения целых строк, создавая тонкие, учитывающие градиент рамки без резких линий. В случаях отсутствия фона мы преобразуем цвет рамки в ARGB и рисуем внешние и внутренние линии, используя метод Line для верхней, правой, нижней и левой сторон на canvas высокого разрешения, чтобы обеспечить единообразие.

Мы получаем цвета, заданные в соответствии с темой, с помощью таких функций, как "GetStatsLabelColor", затем для заголовка "Account Stats" масштабируем положение по оси Y "yPos" и размер шрифта до высокого разрешения, устанавливаем жирный шрифт, вычисляем центрированные размеры прямоугольника с масштабированным отступом, заполняем фон с помощью "FillRoundedRectangle», используя низкую непрозрачность ARGB и рисуем его рамку с помощью функции "DrawRoundedRectangleBorderHiRes" с толщиной, равной 1, умноженной на суперсэмплинг; повторяем то же самое для заголовка "Current Bar Stats" с масштабированным значением "yPosSecond" и толщиной рамки, равной 2, для разнообразия.

После рендеринга в высоком разрешении мы вызываем функцию "DownsampleCanvas", чтобы усреднить пиксели до стандартного значения "canvasStats", достигая сглаживания, а затем на стандартном canvas устанавливаем жирный шрифт в исходных размерах для отрисовки центрированного текста заголовка с использованием цветов ARGB. Мы переключаемся на статистический шрифт, преобразуем цвета меток и значений в ARGB и отображаем информационные метки и значения торгового счета (имя, баланс, эквити) в позициях отображения с шагом "yPosDisplay", используя TextOut с выравниванием по левому и правому краю, получая данные через функции AccountInfoString и AccountInfoDouble. Продолжая анализ статистики баров, мы отображаем метки и значения OHLC (открытие, максимум, минимум, закрытие) в позициях "yPosDisplay" с дальнейшим увеличением, получая цены с помощью iOpen, "iHigh", iLow, "iClose" для текущего инструмента и периода, отформатированные в цифры с помощью DoubleToString функции. Наконец, мы вызываем функцию "Update" для "canvasStats", чтобы обновить отображение и убедиться, что все улучшения высокого разрешения преобразуются в плавные, профессиональные визуальные элементы на панели статистики. После компиляции получаем следующий результат.

ENHANCED, ROUNDED STATS HEADERS, ROUNDED RECTANGLES

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

//+------------------------------------------------------------------+
//| Update text on canvas                                            |
//+------------------------------------------------------------------+
void UpdateTextOnCanvas() {             // Render text elements
   canvasTextHighRes.Erase(0);          //--- Clear high-res text canvas

   int textWidthHighRes = canvasText.Width() * supersamplingFactor; //--- Scaled width
   int textHeightHighRes = canvasText.Height() * supersamplingFactor; //--- Scaled height

   color text_bg = is_dark_theme ? text_bg_dark : text_bg_light; //--- Get bg color
   uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert bg to ARGB
   canvasTextHighRes.FillRectangle(0, 0, textWidthHighRes - 1, textHeightHighRes - 1, argb_bg); //--- Fill background

   uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border to ARGB
   canvasTextHighRes.Line(0, 0, textWidthHighRes - 1, 0, argbBorder); //--- Draw top border
   canvasTextHighRes.Line(textWidthHighRes - 1, 0, textWidthHighRes - 1, textHeightHighRes - 1, argbBorder); //--- Draw right border
   canvasTextHighRes.Line(textWidthHighRes - 1, textHeightHighRes - 1, 0, textHeightHighRes - 1, argbBorder); //--- Draw bottom border
   canvasTextHighRes.Line(0, textHeightHighRes - 1, 0, 0, argbBorder); //--- Draw left border

   int padding = 10 * supersamplingFactor; //--- Scaled padding
   int textAreaX = padding;             //--- Set area X
   int textAreaY = 0;                   //--- Set area Y
   int textAreaWidth = textWidthHighRes - padding * 2; //--- Compute area width
   int textAreaHeight = textHeightHighRes; //--- Set area height
   string font = "Calibri";               //--- Set font
   int fontSize = TextFontSize * supersamplingFactor;  // Scaled font size
   canvasTextHighRes.FontSet(font, fontSize); //--- Apply font
   int lineHeight = canvasTextHighRes.TextHeight("A"); //--- Get line height
   text_adjustedLineHeight = (lineHeight + 3) / supersamplingFactor; //--- Adjust line height (display scale)

   text_visible_height = textAreaHeight / supersamplingFactor; //--- Visible height (display scale)
   static string wrappedLines[];        //--- Declare wrapped lines
   static color wrappedColors[];        //--- Declare wrapped colors
   static bool wrapped = false;         //--- Track wrapped state
   bool need_scroll = false;            //--- Set scroll need
   int reserved_width = 0;              //--- Set reserved width
   if (!wrapped) {                      //--- Check not wrapped
      WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Wrap at display scale
      wrapped = true;                   //--- Set wrapped flag
   }
   int numLines = ArraySize(wrappedLines); //--- Get line count
   text_total_height = numLines * text_adjustedLineHeight; //--- Compute total height
   need_scroll = text_total_height > text_visible_height; //--- Check need scroll
   if (need_scroll) {                   //--- Handle scroll needed
      reserved_width = text_track_width * supersamplingFactor; //--- Reserve scaled width
      textAreaWidth -= reserved_width;  //--- Adjust area width
      WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Rewrap text
      numLines = ArraySize(wrappedLines); //--- Update line count
      text_total_height = numLines * text_adjustedLineHeight; //--- Update total height
   }
   text_max_scroll = MathMax(0, text_total_height - text_visible_height) * smoothness_factor; //--- Compute max scroll
   text_scroll_visible = need_scroll;   //--- Set scroll visible
   text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp scroll pos
   if (text_scroll_visible) {           //--- Check scroll visible
      int scrollbar_y = 0;              //--- Set scrollbar Y
      int scrollbar_height = textAreaHeight; //--- Set scrollbar height
      int scroll_area_height = scrollbar_height - 2 * (text_button_size * supersamplingFactor); //--- Scaled area height
      text_slider_height = TextCalculateSliderHeight(); //--- Compute slider height
      int scrollbar_x = textWidthHighRes - (text_track_width * supersamplingFactor); //--- Scaled scrollbar X
      color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color
      uint argb_leader = ColorToARGB(leader_color, 255); //--- Convert to ARGB
      canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader

      int slider_y = scrollbar_y + (text_button_size * supersamplingFactor) + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - (text_slider_height * supersamplingFactor))); //--- Scaled slider Y

      if (text_scroll_area_hovered) {   //--- Check area hovered
         color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light; //--- Get button bg
         color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg
         color up_bg;
         if (ShowUpDownButtons) {
            up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg;
         } else {
            up_bg = leader_color; // Blend with track, no hover
         }
         uint argb_up_bg = ColorToARGB(up_bg, (uchar)255); //--- Convert up bg
         canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + (text_button_size * supersamplingFactor) - 1, argb_up_bg); //--- Fill up button
         color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light; //--- Get arrow color
         color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow
         color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow
         color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Get up arrow color
         uint argb_up_arrow = ColorToARGB(up_arrow, (uchar)255); //--- Convert up arrow

         // Draw up rounded triangle
         int arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X
         double base_width = text_button_size * (TriangleBaseWidthPercent / 100.0) * supersamplingFactor; //--- Compute base width
         int tri_height = (int)(base_width * (TriangleHeightPercent / 100.0)); //--- Compute height
         int arrow_y = scrollbar_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y
         DrawRoundedTriangleArrow(canvasTextHighRes, arrow_x, arrow_y, (int)base_width, tri_height, true, argb_up_arrow); //--- Up arrow

         int down_y = scrollbar_y + scrollbar_height - (text_button_size * supersamplingFactor); //--- Compute down Y
         color down_bg;
         if (ShowUpDownButtons) {
            down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg;
         } else {
            down_bg = leader_color; // Blend with track, no hover
         }
         uint argb_down_bg = ColorToARGB(down_bg, (uchar)255); //--- Convert down bg
         canvasTextHighRes.FillRectangle(scrollbar_x, down_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, down_y + (text_button_size * supersamplingFactor) - 1, argb_down_bg); //--- Fill down button
         color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Get down arrow color
         uint argb_down_arrow = ColorToARGB(down_arrow, (uchar)255); //--- Convert down arrow

         // Draw down rounded triangle
         int down_arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X
         int down_arrow_y = down_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y
         DrawRoundedTriangleArrow(canvasTextHighRes, down_arrow_x, down_arrow_y, (int)base_width, tri_height, false, argb_down_arrow); //--- Down arrow

         int slider_x = scrollbar_x + (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider X
         int slider_w = (text_track_width * supersamplingFactor) - 2 * (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider width
         int cap_radius = slider_w / 2; //--- Compute cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg
         color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg
         color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine slider bg
         uint argb_slider = ColorToARGB(slider_bg, (uchar)255); //--- Convert slider to ARGB

         canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc
         canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap

         canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc
         canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap

         canvasTextHighRes.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill slider body
      } else {                          //--- Handle thin scrollbar
         int thin_w = text_scrollbar_thin_width * supersamplingFactor; //--- Scaled thin width
         int thin_x = scrollbar_x + ((text_track_width * supersamplingFactor) - thin_w) / 2; //--- Compute thin X
         int cap_radius = thin_w / 2;   //--- Compute cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg
         uint argb_slider = ColorToARGB(slider_bg_color, (uchar)255); //--- Convert to ARGB

         canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc
         canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap

         canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc
         canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap

         canvasTextHighRes.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill thin body
      }
   }

   DownsampleCanvas(canvasText, canvasTextHighRes); //--- Downsample

   color text_base = is_dark_theme ? text_base_dark : text_base_light; //--- Get base color
   canvasText.FontSet("Calibri", TextFontSize);     //--- Normal font
   for (int line = 0; line < numLines; line++) { //--- Loop lines
      string lineText = wrappedLines[line]; //--- Get line text
      if (StringLen(lineText) == 0) continue; //--- Skip empty
      color lineColor = wrappedColors[line]; //--- Get line color
      if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5); //--- Adjust dark color
      else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7); //--- Adjust light color
      int line_y = (textAreaY / supersamplingFactor) + line * text_adjustedLineHeight - (text_scroll_pos/smoothness_factor); //--- Compute line Y (display scale)
      if (line_y + text_adjustedLineHeight < 0 || line_y > text_visible_height) continue; //--- Skip out of view
      if (IsHeading(lineText) ) {        //--- Check heading
         canvasText.FontSet("Calibri Bold", TextFontSize); //--- Set bold font
         lineColor = clrDodgerBlue;     //--- Set heading color
      } else canvasText.FontSet("Calibri", TextFontSize); //--- Set normal font
      uint argbText = ColorToARGB(lineColor, 255); //--- Convert to ARGB
      canvasText.TextOut(textAreaX / supersamplingFactor, line_y, lineText, argbText, TA_LEFT); //--- Draw line text
   }

   canvasText.Update();                 //--- Update text canvas
}

В функции текстового canvas мы начинаем с очистки текстового canvas высокого разрешения с помощью метода Erase, установленного на 0, и вычисляем масштабированные размеры "textWidthHighRes" и "textHeightHighRes", умножая стандартную ширину и высоту canvas на "supersamplingFactor" для детальной отрисовки. Мы выбираем цвет фона "text_bg" на основе темы "is_dark_theme", преобразуем его в ARGB с прозрачностью из параметра "TextBackgroundOpacityPercent" и заполняем весь прямоугольник высокого разрешения с помощью параметра "FillRectangle". Затем рисуем рамки с помощью метода "Line", используя ARGB из параметра "GetBorderColor" для верхнего, правого, нижнего и левого рёбер.

Мы масштабируем отступы до высокого разрешения с помощью параметра "supersamplingFactor", устанавливаем границы текстовой области "textAreaX", "textAreaY", "textAreaWidth" и "textAreaHeight", меняем шрифт на "Calibri" с масштабированным "fontSize" из "TextFontSize", измеряем высоту строки для "A" и корректируем "text_adjustedLineHeight" обратно до масштаба отображения, деля на "supersamplingFactor". Мы вычисляем "text_visible_height" как высоту масштабированной области, деленную на "supersamplingFactor", используем статические массивы "wrappedLines" и "wrappedColors" с флагом "wrapped" для кэширования переноса текста, вызывая функцию "WrapText", если перенос еще не произошел, с параметрами display-scale (размер и ширина шрифта, деленные на коэффициент), чтобы разбить "text_usage_text" на строки с цветами.

Мы определяем количество строк "numLines" и общую высоту "text_total_height" как количество строк, умноженное на скорректированную высоту, проверяем, нужна ли прокрутка "need_scroll", сравнивая ее с видимой высотой, и если да, резервируем масштабированную ширину "reserved_width" для полосы прокрутки, корректируем ширину области, еще раз переносим текст и обновляем счетчики и высоты. Мы вычисляем "text_max_scroll" как избыточную высоту, умноженную на "smoothness_factor" для более точного управления прокруткой, устанавливаем "text_scroll_visible" в соответствии с флагом need, ограничиваем "text_scroll_pos" значениями от 0 до max, и, если текст виден, располагаем полосу прокрутки справа с масштабированной шириной дорожки, заполняем прямоугольник подложки полосы прокрутки цветом "argb_leader" на основе темы из "leader_color".

Для позиции ползунка "slider_y" мы масштабируем размер кнопки и высоту области, вычисляем высоту с помощью "TextCalculateSliderHeight" (исходный масштаб), и если область находится под курсором "text_scroll_area_hovered", условно устанавливаем фон кнопок вверх и вниз на основе функции "ShowUpDownButtons" — используя цвета при наведении курсора, если они отображаются, в противном случае смешивая с цветом подложки — заполняем их прямоугольники, определяем цвета стрелок с учетом состояний отключения или наведения курсора и рисуем закругленные треугольные стрелки вверх и вниз с помощью функции "DrawRoundedTriangleArrow", используя вычисленную ширину основания из "TriangleBaseWidthPercent", высоту из "TriangleHeightPercent", центрированные позиции и направление "isUp" или false.

Затем мы рисуем ползунок с масштабированным отступом "slider_x", шириной "slider_w" и радиусом закругления, выбирая фон в зависимости от состояния наведения курсора или перемещения "text_scroll_slider_hovered" или "text_movingStateSlider", рисуя верхнюю и нижнюю дуги с помощью "Arc", заполняя закругления с помощью FillCircle, а тело с помощью FillRectangle; для тонкого режима без наведения курсора аналогично рисуем более узкий ползунок, центрированный в дорожке. После рендеринга в высоком разрешении мы понижаем дискретизацию до стандартного "canvasText" с помощью функции "DownsampleCanvas" для получения сглаженного изображения. Мы выбираем цвет текста на основе темы "text_base", устанавливаем шрифт "Calibri" в исходное значение "TextFontSize", проходим циклом по перенесенным строкам, пропуская пустые места, корректируем цвета для темы с помощью "LightenColor" или "DarkenColor", вычисляем положение по оси Y в масштабе отображения "line_y", вычитая сглаженную прокрутку "text_scroll_pos / smoothness_factor", пропускаем строки, выходящие за пределы видимой области, выделяем заголовки жирным шрифтом "Calibri Bold" и устанавливаем значение "clrDodgerBlue", преобразуем в ARGB и отображаем каждый из них с помощью TextOut в масштабированных значениях X и Y. Наконец, мы вызываем метод Update для объекта "canvasText", чтобы отобразить улучшенную, плавную текстовую панель с улучшенными прокруткой и визуальными элементами. После компиляции получаем следующий результат.

SUPER-SAMPLED SCROLLBAR

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


Тестирование на истории

Мы провели тестирование, а ниже приведена итоговая визуализация в формате Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

В заключение отметим, что мы улучшили панель canvas на MQL5 с помощью методов сглаживания и рендеринга с высоким разрешением, используя суперсэмплинг для получения более плавной графики и элементов. Мы добавили объекты canvase с высоким разрешением для статистических и текстовых панелей, а также точные функции рисования закругленных рамок, прямоугольников и стрелок, улучшив общее качество изображения. Благодаря улучшенному сглаживанию и рендерингу в высоком разрешении вы сможете создавать более четкие и профессиональные торговые панели, готовые к дальнейшим пользовательским настройкам в процессе разработки. Удачной торговли!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21252

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Разработка инструментария для анализа движения цен (Часть 24): Инструмент количественного анализа Price Action Разработка инструментария для анализа движения цен (Часть 24): Инструмент количественного анализа Price Action
Свечные паттерны дают ценную информацию о возможном движении рынка. Одни свечи сигнализируют о продолжении текущего тренда, а другие предвещают разворот – в зависимости от того, где именно они формируются в структуре движения цены. В этой статье представлен советник, который автоматически определяет четыре ключевые свечные формации. В разделах ниже вы узнаете, как этот инструмент может улучшить ваш анализ движения цены.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа движения цен (Часть 26): Инструмент для работы с несколькими паттернами – пин-баром, паттернами поглощения и дивергенцией RSI Разработка инструментария для анализа движения цен (Часть 26): Инструмент для работы с несколькими паттернами – пин-баром, паттернами поглощения и дивергенцией RSI
В соответствии с нашей целью – разрабатывать практические инструменты для анализа движения цены – в этой статье рассматривается создание советника, который выявляет пин-бары и паттерны поглощения и использует дивергенцию RSI для подтверждения перед формированием торговых сигналов.