Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки
Введение
В своей предыдущей статье (Часть 18) мы объединили векторные прямоугольники и треугольники для создания скругленных текстовых выносок с управлением ориентацией в MetaQuotes Language 5 (MQL5), что позволило добавить динамические элементы интерфейса в торговые приложения. В части 19 мы разрабатываем интерактивную палитру инструментов для рисования графиков, включающую перетаскиваемые панели, изменение размера, переключение тем и кнопки для таких инструментов, как перекрестия, линии и фигуры. Эта настраиваемая система расширяет возможности анализа благодаря взаимодействиям и инструкциям в режиме реального времени. В статье рассмотрим следующие темы:
- Разработка интерактивных палитр инструментов для графиков
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет функциональная палитра, готовая к расширению торговых сценариев работы. Перейдём к реализации!
Разработка интерактивных палитр инструментов для графиков
Интерактивная палитра инструментов для построения графиков объединяет кнопки для основных функций, таких как перекрестия, линии тренда, линии, прямоугольники, числа Фибоначчи, текст и стрелки. Это позволяет быстро выбирать и применять их на торговых графиках с обратной связью в реальном времени. Она поддерживает функции перетаскивания, изменения размера, переключения между темным и светлым режимами, а также сворачивание для оптимизации рабочего пространства, предоставляя при этом указания и обновления статуса для интуитивно понятного использования. Этот дизайн упрощает анализ, предлагая настраиваемый, адаптивный пользовательский интерфейс, который подстраивается под действия пользователя. При этом график не перегружается. Мы планируем использовать объекты типа Canvas для заголовка и панели, управлять событиями мыши для обеспечения интерактивности, определять перечисления для режимов изменения размера и инструментов, а также обрабатывать логику отрисовки с помощью создания объектов на графиках. Вкратце, ниже представлено наглядное представление наших целей.

Реализация средствами MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать во всей программе.
//+------------------------------------------------------------------+ //| Tools Palette.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 #include <Canvas/Canvas.mqh> //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CCanvas canvasHeader; //--- Declare header canvas CCanvas canvasTools; //--- Declare tools canvas string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name string canvasToolsName = "ToolsCanvas"; //--- Set tools canvas name input int CanvasX = 20; // Panel X position input int CanvasY = 30; // Panel Y position input int CanvasWidth = 350; // Panel width input int CanvasHeight = 200; // Panel height input double BackgroundOpacity = 0.8; // Background opacity bool is_dark_theme = true; //--- Set initial dark theme flag color DarkHeaderColor = C'60,60,60'; //--- Set dark header color color DarkHeaderHoverColor = clrRed; //--- Set dark header hover color color DarkHeaderDragColor = clrMediumBlue; //--- Set dark header drag color color DarkHeaderTextColor = clrWhite; //--- Set dark header text color color DarkBorderColor = clrBlack; //--- Set dark border color color DarkTopColor = clrBlack; //--- Set dark top color color DarkIconColor = clrWhite; //--- Set dark icon color color LightHeaderColor = clrSilver; //--- Set light header color color LightHeaderHoverColor = clrRed; //--- Set light header hover color color LightHeaderDragColor = clrMediumBlue; //--- Set light header drag color color LightHeaderTextColor = clrBlack; //--- Set light header text color color LightBorderColor = clrBlack; //--- Set light border color color LightTopColor = clrWhite; //--- Set light top color color LightIconColor = clrBlack; //--- Set light icon color color DarkInstructionColor = clrLime; //--- Set dark instruction color color LightInstructionColor = clrGreen; //--- Set light instruction color color DarkStatusColor = clrMagenta; //--- Set dark status color color LightStatusColor = clrPurple; //--- Set light status color color DarkActiveBtnColor = clrGreen; //--- Set dark active button color color LightActiveBtnColor = clrDarkGreen; //--- Set light active button color color DarkHoverBtnColor = clrDarkGray; //--- Set dark hover button color color LightHoverBtnColor = clrLightGray; //--- Set light hover button color int currentCanvasX = CanvasX; //--- Initialize current X position int currentCanvasY = CanvasY; //--- Initialize current Y position int currentWidth = CanvasWidth; //--- Initialize current width int currentHeight = CanvasHeight; //--- Initialize current height bool panel_dragging = false; //--- Initialize panel dragging flag int panel_drag_x = 0, panel_drag_y = 0; //--- Initialize drag coordinates int panel_start_x = 0, panel_start_y = 0; //--- Initialize start coordinates bool resizing = false; //--- Initialize resizing flag enum ENUM_RESIZE_MODE { NONE, // No resize BOTTOM, // Bottom resize RIGHT, // Right resize BOTTOM_RIGHT // Bottom-right resize }; ENUM_RESIZE_MODE resize_mode = NONE; //--- Initialize resize mode ENUM_RESIZE_MODE hover_mode = NONE; //--- Initialize hover mode int resize_start_x = 0, resize_start_y = 0; //--- Initialize resize start coordinates int start_width = 0, start_height = 0; //--- Initialize start dimensions const int resize_thickness = 5; //--- Set resize thickness const int min_width = 150; //--- Set minimum width const int min_height = 200; //--- Set minimum height int hover_mouse_local_x = 0; //--- Initialize local hover X int hover_mouse_local_y = 0; //--- Initialize local hover Y bool header_hovered = false; //--- Initialize header hover flag bool minimize_hovered = false; //--- Initialize minimize hover flag bool close_hovered = false; //--- Initialize close hover flag bool theme_hovered = false; //--- Initialize theme hover flag bool resize_hovered = false; //--- Initialize resize hover flag int prev_mouse_state = 0; //--- Initialize previous mouse state int header_height = 27; //--- Set header height int button_size = 25; //--- Set button size int theme_x_offset = -75; //--- Set theme X offset int minimize_x_offset = -50; //--- Set minimize X offset int close_x_offset = -25; //--- Set close X offset bool panel_minimized = false; //--- Initialize minimized flag enum TOOL_TYPE { TOOL_NONE, // No tool TOOL_CROSSHAIR, // Crosshair tool TOOL_TRENDLINE, // Trendline tool TOOL_HLINE, // Horizontal line tool TOOL_VLINE, // Vertical line tool TOOL_RECTANGLE, // Rectangle tool TOOL_FIBO, // Fibonacci tool TOOL_TEXT, // Text tool TOOL_ARROW // Arrow tool }; TOOL_TYPE active_tool = TOOL_NONE; //--- Initialize active tool bool drawing_first_click = false; //--- Initialize first click flag datetime draw_time1 = 0; //--- Initialize draw time 1 double draw_price1 = 0.0; //--- Initialize draw price 1 long local_chart_id = 0; //--- Initialize local chart ID string current_instruction = ""; //--- Initialize current instruction struct ToolButton { // Define tool button structure string symbol; // Store symbol string font; // Store font TOOL_TYPE type; // Store type string tooltip; // Store tooltip }; ToolButton buttons[8]; //--- Declare buttons array int hovered_tool_index = -1; //--- Initialize hovered tool index bool measuring = false; //--- Initialize measuring flag datetime fixed_time = 0; //--- Initialize fixed time double fixed_price = 0.0; //--- Initialize fixed price ulong last_click_time = 0; //--- Initialize last click time const int tools_y_offset = -1; //--- Set tools Y offset
Начинаем реализацию с включения библиотеки Canvas с помощью команды "#include <Canvas/Canvas.mqh>", чтобы получить доступ к основным графическим функциям для создания палитры. Далее мы объявляем глобальные объекты Canvas "canvasHeader" и "canvasTools" для заголовка и основной области панели, присваивая им имена вроде "HeaderCanvas" и "ToolsCanvas" для дальнейшего обращения к ним в коде, а также указываем пользовательские входные параметры положения, размера и прозрачности фона для возможности настройки. Для поддержки тем оформления мы инициализируем параметр "is_dark_theme" значением true и определяем цветовые константы для темного и светлого режимов, охватывающие заголовки, границы, значки, указания, состояние и кнопки, обеспечивая визуальную согласованность между переключателями.
Мы отслеживаем состояние панели с помощью переменных, определяющих текущее положение и размеры, флажки перетаскивания, режимы изменения размера с помощью перечисления "ENUM_RESIZE_MODE" (NONE, BOTTOM, RIGHT, BOTTOM_RIGHT), состояния при наведении курсора, а также константы, такие как минимальные размеры и толщина для ограничения удобства использования. Для инструментов мы определяем перечисление "TOOL_TYPE", перечисляющее параметры от NONE до ARROW, инициализируем "active_tool" значением NONE и управляем флагами рисования, такими как "drawing_first_click", а также структурой "ToolButton", содержащей символ, шрифт, тип и всплывающую подсказку, создавая массив "buttons[8]" для элементов палитры. Наконец, мы настроили переменные для режима измерения, фиксированных точек, времени клика, индексов при наведении курсора и смещения по оси Y для выравнивания инструментов, подготавливая таким образом интерактивные функции. Затем нам понадобятся некоторые вспомогательные функции, чтобы сделать код модульным.
//+------------------------------------------------------------------+ //| Get theme-aware colors | //+------------------------------------------------------------------+ color GetHeaderColor() { return is_dark_theme ? DarkHeaderColor : LightHeaderColor; } //--- Return header color based on theme color GetHeaderHoverColor() { return is_dark_theme ? DarkHeaderHoverColor : LightHeaderHoverColor; } //--- Return header hover color based on theme color GetHeaderDragColor() { return is_dark_theme ? DarkHeaderDragColor : LightHeaderDragColor; } //--- Return header drag color based on theme color GetHeaderTextColor() { return is_dark_theme ? DarkHeaderTextColor : LightHeaderTextColor; } //--- Return header text color based on theme color GetBorderColor() { return is_dark_theme ? DarkBorderColor : LightBorderColor; } //--- Return border color based on theme color GetTopColor() { return is_dark_theme ? DarkTopColor : LightTopColor; } //--- Return top color based on theme color GetIconColor() { return is_dark_theme ? DarkIconColor : LightIconColor; } //--- Return icon color based on theme color GetInstructionColor() { return is_dark_theme ? DarkInstructionColor : LightInstructionColor; } //--- Return instruction color based on theme color GetStatusColor() { return is_dark_theme ? DarkStatusColor : LightStatusColor; } //--- Return status color based on theme color GetActiveBtnColor() { return is_dark_theme ? DarkActiveBtnColor : LightActiveBtnColor; } //--- Return active button color based on theme color GetHoverBtnColor() { return is_dark_theme ? DarkHoverBtnColor : LightHoverBtnColor; } //--- Return hover button color based on theme //+------------------------------------------------------------------+ //| Get tool name from type | //+------------------------------------------------------------------+ string GetToolName(TOOL_TYPE t) { for (int i = 0; i < 8; i++) { //--- Loop over buttons if (buttons[i].type == t) { //--- Check if type matches return StringSubstr(buttons[i].tooltip, 0, StringFind(buttons[i].tooltip, " Tool")); //--- Return tool name } } return "None"; //--- Return default if no match } //+------------------------------------------------------------------+ //| Check if tool is single-click | //+------------------------------------------------------------------+ bool IsSingleClickTool(TOOL_TYPE t) { switch(t) { //--- Switch on tool type case TOOL_HLINE: //--- Handle horizontal line case TOOL_VLINE: //--- Handle vertical line case TOOL_TEXT: //--- Handle text case TOOL_ARROW: //--- Handle arrow return true; //--- Return true for single-click tools default: //--- Handle default return false; //--- Return false otherwise } }
Далее мы определяем ряд функций-геттеров, таких как "GetHeaderColor", для получения цветов, зависящих от темы оформления, каждая из которых возвращает темный или светлый вариант на основе флага "is_dark_theme", обеспечивая согласованное оформление заголовков, наведений курсора, перетаскиваемых элементов, текста, границ, значков, указаний, статуса, активных кнопок и наведений курсора без избыточных проверок. Далее функция "GetToolName" извлекает имя инструмента из его типа, проходя циклом по массиву "buttons", сопоставляя тип и выделяя подстроку всплывающей подсказки до "Tool" для получения корректного результата; по умолчанию, если совпадения нет, значение устанавливается на "None". Для классификации инструментов мы реализуем функцию "IsSingleClickTool", используя оператор switch для типа инструмента, возвращая true для горизонтальных/вертикальных линий, текста и стрелок, требующих только одного щелчка, и false в противном случае для точной обработки событий. После этого мы можем инициализировать создание заголовка и основной части палитры.
//+------------------------------------------------------------------+ //| Draw header | //+------------------------------------------------------------------+ void DrawHeader() { canvasHeader.Erase(0); //--- Erase header canvas color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Get header background color canvasHeader.FillRectangle(0, 0, currentWidth - 1, header_height - 1, ColorToARGB(header_bg, 255)); //--- Fill header rectangle uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Get border ARGB canvasHeader.Rectangle(0, 0, currentWidth - 1, header_height - 1, argbBorder); //--- Draw border rectangle canvasHeader.FontSet("Arial Bold", 13); //--- Set font for title canvasHeader.TextOut(10, (header_height - 13)/2, "Tools Palette", ColorToARGB(GetHeaderTextColor(), 255)); //--- Draw title text int button_y = (header_height - 20)/2; //--- Compute button Y position int theme_button_x = currentWidth + theme_x_offset; //--- Compute theme button X canvasHeader.FontSet("Wingdings", 20); //--- Set font for theme symbol string theme_symbol = CharToString(91); //--- Set theme symbol int theme_w = canvasHeader.TextWidth(theme_symbol); //--- Get theme symbol width int theme_x = theme_button_x + (button_size - theme_w) / 2; //--- Compute theme X position color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Get theme color canvasHeader.TextOut(theme_x, button_y, theme_symbol, ColorToARGB(theme_color, 255)); //--- Draw theme symbol int min_button_x = currentWidth + minimize_x_offset; //--- Compute minimize button X string min_symbol = panel_minimized ? CharToString(111) : CharToString(114); //--- Set minimize symbol int min_w = canvasHeader.TextWidth(min_symbol); //--- Get minimize symbol width int min_x = min_button_x + (button_size - min_w) / 2; //--- Compute minimize X position color min_color = minimize_hovered ? clrYellow : GetHeaderTextColor(); //--- Get minimize color canvasHeader.TextOut(min_x, button_y, min_symbol, ColorToARGB(min_color, 255)); //--- Draw minimize symbol int close_button_x = currentWidth + close_x_offset; //--- Compute close button X canvasHeader.FontSet("Webdings", 20); //--- Set font for close symbol string close_symbol = CharToString(114); //--- Set close symbol int close_w = canvasHeader.TextWidth(close_symbol); //--- Get close symbol width int close_x = close_button_x + (button_size - close_w) / 2; //--- Compute close X position color close_color = close_hovered ? clrRed : GetHeaderTextColor(); //--- Get close color canvasHeader.TextOut(close_x, button_y, close_symbol, ColorToARGB(close_color, 255)); //--- Draw close symbol canvasHeader.Update(); //--- Update header canvas } //+------------------------------------------------------------------+ //| Draw tools panel | //+------------------------------------------------------------------+ void DrawToolsPanel() { canvasTools.Erase(0); //--- Erase tools canvas uint argb_fill = ColorToARGB(GetTopColor(), (uchar)(255 * BackgroundOpacity)); //--- Get fill ARGB canvasTools.FillRectangle(0, 0, currentWidth - 1, currentHeight - 1, argb_fill); //--- Fill background rectangle uint argb_border = ColorToARGB(GetBorderColor(), 255); //--- Get border ARGB canvasTools.Rectangle(0, 0, currentWidth - 1, currentHeight - 1, argb_border); //--- Draw border rectangle int columns = 4; //--- Set number of columns int padding = 5; //--- Set padding int button_width = (currentWidth - (columns + 1) * padding) / columns; //--- Compute button width int button_height = 70; //--- Set button height int start_x = padding; //--- Set start X int start_y = padding; //--- Set start Y for (int i = 0; i < 8; i++) { //--- Loop over buttons int col = i % columns; //--- Compute column int row = i / columns; //--- Compute row int x = start_x + col * (button_width + padding); //--- Compute X position int y = start_y + row * (button_height + padding); //--- Compute Y position color btn_bg = (active_tool == buttons[i].type) ? GetActiveBtnColor() : ((i == hovered_tool_index) ? GetHoverBtnColor() : clrNONE); //--- Get button background if (btn_bg != clrNONE) canvasTools.FillRectangle(x, y, x + button_width - 1, y + button_height - 1, ColorToARGB(btn_bg, 255)); //--- Fill button rectangle if needed canvasTools.FontSet(buttons[i].font, 35); //--- Set font for icon int text_w = canvasTools.TextWidth(buttons[i].symbol); //--- Get icon width int text_h = canvasTools.TextHeight(buttons[i].symbol); //--- Get icon height int icon_x = x + (button_width - text_w) / 2; //--- Compute icon X int icon_y = y + 5; //--- Set icon Y canvasTools.TextOut(icon_x, icon_y, buttons[i].symbol, ColorToARGB(GetIconColor(), 255)); //--- Draw icon string tool_name = StringSubstr(buttons[i].tooltip, 0, StringFind(buttons[i].tooltip, " Tool")); //--- Get tool name canvasTools.FontSet("Arial Bold", 12); //--- Set font for name int name_w = canvasTools.TextWidth(tool_name); //--- Get name width canvasTools.TextOut(x + (button_width - name_w) / 2, y + button_height - 20, tool_name, ColorToARGB(GetIconColor(), 255)); //--- Draw tool name } if (StringLen(current_instruction) > 0) { //--- Check if instruction exists canvasTools.FontSet("Arial", 14); //--- Set font for instruction int instr_w = canvasTools.TextWidth(current_instruction); //--- Get instruction width int instr_x = (currentWidth - instr_w) / 2; //--- Compute instruction X canvasTools.TextOut(instr_x, currentHeight - 30, current_instruction, ColorToARGB(GetInstructionColor(), 255)); //--- Draw instruction } string status_text = "Active: " + GetToolName(active_tool); //--- Set status text canvasTools.FontSet("Arial Bold", 14); //--- Set font for status int status_w = canvasTools.TextWidth(status_text); //--- Get status width int status_x = (currentWidth - status_w) / 2; //--- Compute status X canvasTools.TextOut(status_x, currentHeight - 15, status_text, ColorToARGB(GetStatusColor(), 255)); //--- Draw status if (resize_hovered || resizing) { //--- Check if resize hovered or resizing string icon_font = "Wingdings 3"; //--- Set icon font int icon_size = 25; //--- Set icon size uchar icon_code = 0; //--- Initialize icon code int angle = 0; //--- Initialize angle switch (hover_mode) { //--- Switch on hover mode case BOTTOM: icon_code = (uchar)'2'; angle = 0; break; //--- Set for bottom case RIGHT: icon_code = (uchar)'1'; angle = 0; break; //--- Set for right case BOTTOM_RIGHT: icon_code = (uchar)'2'; angle = 450; break; //--- Set for bottom-right } if (icon_code != 0) { //--- Check if icon code set canvasTools.FontSet(icon_font, icon_size); //--- Set font for icon canvasTools.FontAngleSet(angle); //--- Set font angle color icon_color = (resize_hovered || resizing) ? clrRed : GetIconColor(); //--- Get icon color int icon_x = (hover_mode == BOTTOM) ? MathMin(MathMax(hover_mouse_local_x - icon_size/2, 0), currentWidth - icon_size) : currentWidth - icon_size - 2; //--- Compute icon X int icon_y = (hover_mode == RIGHT) ? MathMin(MathMax(hover_mouse_local_y - icon_size/2, 0), currentHeight - icon_size) : currentHeight - icon_size - 2; //--- Compute icon Y if (hover_mode == BOTTOM_RIGHT) { icon_x = currentWidth - icon_size - 10; icon_y = currentHeight - icon_size; } //--- Adjust for bottom-right canvasTools.TextOut(icon_x, icon_y, CharToString(icon_code), ColorToARGB(icon_color, 255)); //--- Draw icon canvasTools.FontAngleSet(0); //--- Reset font angle } } canvasTools.Update(); //--- Update tools canvas }
Для создания начального отображения мы реализуем функцию "DrawHeader", которая отображает заголовок палитры на объекте Canvas, начиная с его стирания с помощью Erase, установленного на 0, затем заполняя прямоугольник динамическим цветом фона, полученным из методов-получателей, таких как "GetHeaderDragColor" при перетаскивании, "GetHeaderHoverColor" при наведении курсора или стандартным цветом в противном случае, преобразованным в ARGB. Далее, рисуем контур, используя Rectangle с цветом ARGB из функции "GetBorderColor", устанавливаем жирный шрифт "Arial" для заголовка "Tools Palette" с помощью FontSet и TextOut в цвете текста темы. Для добавления элементов управления мы вычисляем центрирование кнопки по оси Y, затем для темы устанавливаем шрифт Wingdings, позиционируем и окрашиваем символ '[' в зависимости от состояния при наведения курсора и рисуем с помощью "TextOut". Аналогично для сворачивания (переключение символа 'o' или 'r') и закрытия ('X' в Webdings) настраиваем цвета для состояний при наведении курсора, например, желтый или красный. Наконец, для отображения изменений обновляем объект Canvas с помощью Update. При необходимости можно настроить значки, используя собственные символы. Смотрите набор символов шрифта, которые можно использовать.

В функции "DrawToolsPanel" стираем объект Canvas инструментов, заполняем его верхним цветом ARGB, скорректированным для параметра "BackgroundOpacity", и рисуем прямоугольник границы. Мы размещаем кнопки в 4 столбцах с отступами, проходим циклом для заполнения фона при активации или наведении курсора с помощью функций "GetActiveBtnColor" или "GetHoverBtnColor", устанавливаем пользовательские шрифты для значков, например, 'v' в формате "Wingdings", центрируем их и рисуем с помощью TextOut цветом значка, а затем добавляем жирным шрифтом названия инструментов ниже. Если в переменной "current_instruction" есть указание, мы отображаем его по центру внизу с помощью функции "GetInstructionColor"; аналогично добавляем статус активного инструмента. Для обозначения изменения размера, если установлено значение "resize_hovered" или "resizing", мы устанавливаем шрифт "Wingdings 3", определяем код стрелки и угол для каждого "hover_mode" (например, '2' на 450 для нижнего правого угла), динамически позиционируем, рисуем красным или цветом значка и сбрасываем угол. В завершение вызываем метод "Update" для обновления объекта Canvas. Теперь мы можем инициализировать объект, отрисовав его, используя следующую логику.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { currentCanvasX = CanvasX; //--- Set current X from input currentCanvasY = CanvasY; //--- Set current Y from input currentWidth = CanvasWidth; //--- Set current width from input currentHeight = CanvasHeight; //--- Set current height from input if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX, currentCanvasY, currentWidth, header_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create header canvas Print("Failed to create Header Canvas"); //--- Print failure message return(INIT_FAILED); //--- Return initialization failure } if (!canvasTools.CreateBitmapLabel(0, 0, canvasToolsName, currentCanvasX, currentCanvasY + header_height + tools_y_offset, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create tools canvas Print("Failed to create Tools Canvas"); //--- Print failure message return(INIT_FAILED); //--- Return initialization failure } buttons[0].symbol = CharToString('v'); buttons[0].font = "Wingdings"; buttons[0].type = TOOL_CROSSHAIR; buttons[0].tooltip = "Crosshair Tool"; //--- Initialize button 0 buttons[1].symbol = CharToString('.'); buttons[1].font = "Wingdings 3"; buttons[1].type = TOOL_TRENDLINE; buttons[1].tooltip = "Trendline Tool"; //--- Initialize button 1 buttons[2].symbol = CharToString('*'); buttons[2].font = "Wingdings 3"; buttons[2].type = TOOL_HLINE; buttons[2].tooltip = "Horizontal Line Tool"; //--- Initialize button 2 buttons[3].symbol = CharToString('+'); buttons[3].font = "Wingdings 3"; buttons[3].type = TOOL_VLINE; buttons[3].tooltip = "Vertical Line Tool"; //--- Initialize button 3 buttons[4].symbol = CharToString('c'); buttons[4].font = "Webdings"; buttons[4].type = TOOL_RECTANGLE; buttons[4].tooltip = "Rectangle Tool"; //--- Initialize button 4 buttons[5].symbol = CharToString('"'); buttons[5].font = "Webdings"; buttons[5].type = TOOL_FIBO; buttons[5].tooltip = "Fibonacci Tool"; //--- Initialize button 5 buttons[6].symbol = CharToString('>'); buttons[6].font = "Webdings"; buttons[6].type = TOOL_TEXT; buttons[6].tooltip = "Text Tool"; //--- Initialize button 6 buttons[7].symbol = CharToString(225); buttons[7].font = "Wingdings"; buttons[7].type = TOOL_ARROW; buttons[7].tooltip = "Arrow Tool"; //--- Initialize button 7 DrawHeader(); //--- Draw header DrawToolsPanel(); //--- Draw tools panel ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Enable mouse move events ChartRedraw(); //--- Redraw chart local_chart_id = ChartID(); //--- Set local chart ID return(INIT_SUCCEEDED); //--- Return initialization success }
В обработчике OnInit мы инициализируем текущие положения и размеры панелей на основе пользовательских входных данных, обеспечивая соответствие заданным значениям. Далее мы создаём объект Canvas заголовка с помощью CreateBitmapLabel с параметром ARGB normalize для обеспечения прозрачности, выводя сообщения об ошибках и возвращая INIT_FAILED в случае неудачи. Аналогично для объекта Canvas инструментов, расположенного под заголовком, со смещением по оси Y. Для заполнения палитры мы инициализируем элементы массива "buttons" определенными символами (например, 'v' для перекрестия), шрифтами, такими как Wingdings, типами инструментов из перечисления и всплывающими подсказками для информации при наведении курсора. Затем мы отрисовываем заголовок и панель инструментов с помощью соответствующих функций, включаем события перемещения мыши с помощью ChartSetInteger для захвата взаимодействий, перерисовываем график, сохраняем локальный идентификатор графика для управления объектами и возвращаем INIT_SUCCEEDED. После компиляции получаем следующий результат.

Мы видим, что панель инициализирована корректно. Теперь нам нужно вдохнуть жизнь в панель, чтобы она могла взаимодействовать с другими элементами и менять тему. Для упрощения этой задачи мы определим еще несколько вспомогательных функций.
//+------------------------------------------------------------------+ //| Toggle theme | //+------------------------------------------------------------------+ void ToggleTheme() { is_dark_theme = !is_dark_theme; //--- Toggle theme flag DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Toggle minimize | //+------------------------------------------------------------------+ void ToggleMinimize() { panel_minimized = !panel_minimized; //--- Toggle minimized flag if (panel_minimized) { //--- Check if minimized canvasTools.Destroy(); //--- Destroy tools canvas } else { //--- Handle restored canvasTools.CreateBitmapLabel(0, 0, canvasToolsName, currentCanvasX, currentCanvasY + header_height + tools_y_offset, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE); //--- Recreate tools canvas DrawToolsPanel(); //--- Redraw tools panel } DrawHeader(); //--- Redraw header ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Is mouse over header excluding buttons | //+------------------------------------------------------------------+ bool IsMouseOverHeader(int mx, int my) { if (mx < currentCanvasX || mx > currentCanvasX + currentWidth || my < currentCanvasY || my > currentCanvasY + header_height) return false; //--- Check outside header int btn_left = currentCanvasX + currentWidth + theme_x_offset; //--- Compute buttons left if (mx >= btn_left && mx <= btn_left + button_size * 3) return false; //--- Check over buttons return true; //--- Return true if over header } //+------------------------------------------------------------------+ //| Is mouse over specific button | //+------------------------------------------------------------------+ bool IsMouseOverButton(int mx, int my, int offset) { int btn_left = currentCanvasX + currentWidth + offset; //--- Compute button left return (mx >= btn_left && mx <= btn_left + button_size && my >= currentCanvasY && my <= currentCanvasY + header_height); //--- Return if over button } //+------------------------------------------------------------------+ //| Is mouse over resize area | //+------------------------------------------------------------------+ bool IsMouseOverResize(int mx, int my, ENUM_RESIZE_MODE &rm) { if (panel_minimized) return false; //--- Return false if minimized int tx = currentCanvasX; //--- Set tools X int ty = currentCanvasY + header_height + tools_y_offset; //--- Set tools Y int tr = tx + currentWidth; //--- Set right int tb = ty + currentHeight; //--- Set bottom bool over_right = mx >= tr - resize_thickness && mx <= tr && my >= ty && my <= tb; //--- Check over right bool over_bottom = my >= tb - resize_thickness && my <= tb && mx >= tx && mx <= tr; //--- Check over bottom if (over_bottom && over_right) { rm = BOTTOM_RIGHT; return true; } //--- Set and return bottom-right if (over_bottom) { rm = BOTTOM; return true; } //--- Set and return bottom if (over_right) { rm = RIGHT; return true; } //--- Set and return right return false; //--- Return false otherwise } //+------------------------------------------------------------------+ //| Get hovered tool | //+------------------------------------------------------------------+ int GetHoveredTool(int mx, int my) { int lx = mx - currentCanvasX; //--- Compute local X int ly = my - (currentCanvasY + header_height + tools_y_offset); //--- Compute local Y if (lx < 0 || lx > currentWidth || ly < 0 || ly > currentHeight) return -1; //--- Return -1 if outside int columns = 4; //--- Set columns int padding = 5; //--- Set padding int button_width = (currentWidth - (columns + 1) * padding) / columns; //--- Compute button width int button_height = 70; //--- Set button height for (int i = 0; i < 8; i++) { //--- Loop over buttons int col = i % columns; //--- Compute column int row = i / columns; //--- Compute row int x = padding + col * (button_width + padding); //--- Compute X int y = padding + row * (button_height + padding); //--- Compute Y if (lx >= x && lx < x + button_width && ly >= y && ly < y + button_height) return i; //--- Return index if over } return -1; //--- Return -1 if none } //+------------------------------------------------------------------+ //| Toggle tool activation | //+------------------------------------------------------------------+ void ToggleTool(TOOL_TYPE type) { if (active_tool == type) { //--- Check if already active if (active_tool == TOOL_CROSSHAIR) { //--- Handle crosshair deactivation DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = TOOL_NONE; //--- Deactivate tool drawing_first_click = false; //--- Reset first click current_instruction = ""; //--- Clear instruction } else { //--- Handle activation if (active_tool == TOOL_CROSSHAIR) { //--- Deactivate previous crosshair DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = type; //--- Set active tool drawing_first_click = false; //--- Reset first click if (active_tool == TOOL_CROSSHAIR) { //--- Handle crosshair instruction current_instruction = "Move mouse for crosshair. Double-click to start measuring."; //--- Set instruction } else { //--- Handle other tools current_instruction = "Click on chart to draw."; //--- Set instruction } } DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Deactivate current tool on Escape | //+------------------------------------------------------------------+ void DeactivateTool() { if (active_tool == TOOL_CROSSHAIR) { //--- Check if crosshair active DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll } active_tool = TOOL_NONE; //--- Deactivate tool drawing_first_click = false; //--- Reset first click current_instruction = ""; //--- Clear instruction DrawToolsPanel(); //--- Redraw tools panel ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Delete measure objects | //+------------------------------------------------------------------+ void DeleteMeasureObjects() { ObjectDelete(local_chart_id, "Cross_HF"); //--- Delete fixed horizontal ObjectDelete(local_chart_id, "Cross_VF"); //--- Delete fixed vertical ObjectDelete(local_chart_id, "Measure_Line"); //--- Delete measure line ObjectDelete(local_chart_id, "Measure_Label"); //--- Delete measure label } //+------------------------------------------------------------------+ //| Delete crosshair | //+------------------------------------------------------------------+ void DeleteCrosshair() { ObjectDelete(local_chart_id, "Cross_H"); //--- Delete moving horizontal ObjectDelete(local_chart_id, "Cross_V"); //--- Delete moving vertical DeleteMeasureObjects(); //--- Delete measure objects ChartRedraw(); //--- Redraw chart }
Здесь мы реализуем функцию "ToggleTheme" для переключения между темным и светлым режимами, инвертируя флаг "is_dark_theme", затем перерисовывая заголовок и панель инструментов для применения новых цветов и обновляя график для немедленной видимости. Далее мы создаём функцию "ToggleMinimize", которая будет чередовать состояние панели с "panel_minimized", уничтожая объект Canvas инструментов при сворачивании для экономии места или создавая его заново при восстановлении, после чего происходит перерисовка заголовка и обновление графика. Для обнаружения взаимодействий мы определяем параметр "IsMouseOverHeader", который проверяет, находится ли курсор мыши в пределах заголовка, но исключает область кнопки, начиная со смещения темы, и возвращает значение true только для областей, доступных для перетаскивания.
Аналогично, функция "IsMouseOverButton" проверяет положение курсора мыши над определенной кнопкой, вычисляя ее левый край на основе смещения и сравнивая его с размером кнопки и высотой заголовка. Для изменения размера функция "IsMouseOverResize" проверяет, находится ли курсор мыши вблизи нижнего, правого или углового края панели инструментов (игнорируя, если панель свернута), обновляя режим привязки, как в функции "BOTTOM_RIGHT", и возвращая значение true, если это обнаружено. Мы добавляем функцию "GetHoveredTool", которая идентифицирует кнопку под курсором мыши, преобразуя ее в локальные координаты, перебирая 4-колоночную сетку с отступами и возвращая индекс, если находится в пределах кнопки, либо -1 в противном случае.
Для управления инструментами функция "ToggleTool" активирует или деактивирует их на основе типа, обрабатывает очистку перекрестия при переключении, сбрасывает флаги рисования и устанавливает соответствующие указания, например, "Для рисования щелкните по графику", перед перерисовкой панели. Функция "DeactivateTool" полностью сбрасывает активный инструмент, в частности, очищает элементы перекрестия и включает прокрутку, затем обновляет указания и перерисовывает изображение. Наконец, мы выполняем очистку с помощью функции "DeleteMeasureObjects", удаляющей фиксированное перекрестие и линию/метку измерения, и функции "DeleteCrosshair", удаляющей движущееся перекрестие и вызывающей функцию удаления измерения, после чего происходит перерисовка графика. Теперь мы рассмотрим функции объектов и их создание.
//+------------------------------------------------------------------+ //| Handle chart drawing for tools | //+------------------------------------------------------------------+ void HandleDrawing(int mx, int my) { datetime time; //--- Declare time double price; //--- Declare price int sw; //--- Declare subwindow if (!ChartXYToTimePrice(local_chart_id, mx, my, sw, time, price)) return; //--- Convert XY to time/price or return string name = "Tool_" + TimeToString(TimeCurrent(), TIME_SECONDS); //--- Set object name color col = clrBlue; //--- Set color ENUM_OBJECT obj_type = OBJ_VLINE; //--- Initialize object type datetime time1 = time; //--- Set time1 double price1 = price; //--- Set price1 datetime time2 = 0; //--- Set time2 double price2 = 0.0; //--- Set price2 bool create = true; //--- Set create flag bool valid_obj = false; //--- Set valid object flag string tool_name = GetToolName(active_tool); //--- Get tool name if (IsSingleClickTool(active_tool)) { //--- Check if single-click tool switch (active_tool) { //--- Switch on tool case TOOL_HLINE: //--- Handle horizontal line obj_type = OBJ_HLINE; //--- Set type time1 = 0; //--- Set time1 price1 = price; //--- Set price1 time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_VLINE: //--- Handle vertical line obj_type = OBJ_VLINE; //--- Set type time1 = time; //--- Set time1 price1 = 0; //--- Set price1 time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_TEXT: //--- Handle text obj_type = OBJ_TEXT; //--- Set type time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break case TOOL_ARROW: //--- Handle arrow obj_type = OBJ_ARROW; //--- Set type time2 = 0; //--- Set time2 price2 = 0; //--- Set price2 valid_obj = true; //--- Set valid break; //--- Break } } else { //--- Handle two-click tools if (!drawing_first_click) { //--- Check if first click draw_time1 = time; //--- Set draw time1 draw_price1 = price; //--- Set draw price1 drawing_first_click = true; //--- Set first click flag create = false; //--- Set no create current_instruction = "Click second point for " + tool_name + "."; //--- Set instruction } else { //--- Handle second click ChartXYToTimePrice(local_chart_id, mx, my, sw, time2, price2); //--- Get time2/price2 drawing_first_click = false; //--- Reset first click time1 = draw_time1; //--- Set time1 price1 = draw_price1; //--- Set price1 current_instruction = "Click on chart to draw."; //--- Set instruction } switch (active_tool) { //--- Switch on tool case TOOL_TRENDLINE: obj_type = OBJ_TREND; valid_obj = true; break; //--- Set trendline case TOOL_RECTANGLE: obj_type = OBJ_RECTANGLE; valid_obj = true; break; //--- Set rectangle case TOOL_FIBO: obj_type = OBJ_FIBO; valid_obj = true; break; //--- Set fibo } } if (create && valid_obj) { //--- Check if create and valid if (ObjectCreate(local_chart_id, name, obj_type, sw, time1, price1, time2, price2)) { //--- Create object ObjectSetInteger(local_chart_id, name, OBJPROP_COLOR, col); //--- Set color ObjectSetInteger(local_chart_id, name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style ObjectSetInteger(local_chart_id, name, OBJPROP_WIDTH, 1); //--- Set width if (obj_type == OBJ_TEXT) ObjectSetString(local_chart_id, name, OBJPROP_TEXT, "Text"); //--- Set text if (obj_type == OBJ_ARROW) ObjectSetInteger(local_chart_id, name, OBJPROP_ARROWCODE, 233); //--- Set arrow code if (obj_type == OBJ_HLINE || obj_type == OBJ_VLINE) ObjectSetInteger(local_chart_id, name, OBJPROP_STYLE, STYLE_DASH); //--- Set dash for lines ObjectSetInteger(local_chart_id, name, OBJPROP_SELECTABLE, true); //--- Set selectable ObjectSetInteger(local_chart_id, name, OBJPROP_SELECTED, true); //--- Set selected ChartRedraw(local_chart_id); //--- Redraw chart active_tool = TOOL_NONE; //--- Deactivate tool current_instruction = ""; //--- Clear instruction } } DrawToolsPanel(); //--- Redraw tools panel }
Для обработки построения графиков мы реализуем функцию "HandleDrawing", которая управляет созданием объектов графика на основе активного инструмента и положения мыши, начиная с преобразования координат во время и цену с помощью функции ChartXYToTimePrice и возвращаясь к исходному состоянию в случае неудачи. Далее мы генерируем уникальное имя объекта, используя текущее время, устанавливаем синий цвет по умолчанию и тип вертикальной линии, затем выбираем вариант для инструментов с одним щелчком мыши с помощью параметра "IsSingleClickTool", переключаясь на настройку типов, таких как OBJ_HLINE для горизонтальной линии (сброс времени), OBJ_VLINE для вертикальной линии (сброс цен), OBJ_TEXT или OBJ_ARROW, помечаем их как действительные и устанавливаем флаг создания.
Для инструментов с двумя щелчками мыши мы захватываем первую точку, если "drawing_first_click" равно false, обновляя глобальные переменные и указания без создания; при втором щелчке получаем конечную точку, сбрасываем флаг и настраиваем типы, такие как OBJ_TREND для линии тренда, OBJ_RECTANGLE или OBJ_FIBO, проверяя объект. Если создание отмечено флагом и действительно, мы используем ObjectCreate с параметрами, устанавливаем такие свойства, как цвет, стиль, ширина, текст или код стрелки (если применимо), пунктир для линий, выбираемое и выбранное состояния, затем перерисовываем график с помощью ChartRedraw и деактивируем инструмент, удаляя указания. Наконец для отражения изменений мы обновляем панель инструментов с помощью "DrawToolsPanel". Теперь перейдём к перекрестию. С остальными всё просто.
//+------------------------------------------------------------------+ //| Update moving crosshair | //+------------------------------------------------------------------+ void UpdateMovingCrosshair(datetime time, double price) { string h_name = "Cross_H"; //--- Set horizontal name string v_name = "Cross_V"; //--- Set vertical name if (ObjectFind(local_chart_id, h_name) < 0) { //--- Check if horizontal exists ObjectCreate(local_chart_id, h_name, OBJ_HLINE, 0, 0, price); //--- Create horizontal line ObjectSetInteger(local_chart_id, h_name, OBJPROP_COLOR, clrRed); //--- Set color ObjectSetInteger(local_chart_id, h_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, h_name, 0, 0, price); //--- Move horizontal } if (ObjectFind(local_chart_id, v_name) < 0) { //--- Check if vertical exists ObjectCreate(local_chart_id, v_name, OBJ_VLINE, 0, time, 0); //--- Create vertical line ObjectSetInteger(local_chart_id, v_name, OBJPROP_COLOR, clrRed); //--- Set color ObjectSetInteger(local_chart_id, v_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, v_name, 0, time, 0); //--- Move vertical } } //+------------------------------------------------------------------+ //| Update fixed crosshair | //+------------------------------------------------------------------+ void UpdateFixedCrosshair() { string hf_name = "Cross_HF"; //--- Set fixed horizontal name string vf_name = "Cross_VF"; //--- Set fixed vertical name if (ObjectFind(local_chart_id, hf_name) < 0) { //--- Check if fixed horizontal exists ObjectCreate(local_chart_id, hf_name, OBJ_HLINE, 0, 0, fixed_price); //--- Create fixed horizontal ObjectSetInteger(local_chart_id, hf_name, OBJPROP_COLOR, clrGreen); //--- Set color ObjectSetInteger(local_chart_id, hf_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, hf_name, 0, 0, fixed_price); //--- Move fixed horizontal } if (ObjectFind(local_chart_id, vf_name) < 0) { //--- Check if fixed vertical exists ObjectCreate(local_chart_id, vf_name, OBJ_VLINE, 0, fixed_time, 0); //--- Create fixed vertical ObjectSetInteger(local_chart_id, vf_name, OBJPROP_COLOR, clrGreen); //--- Set color ObjectSetInteger(local_chart_id, vf_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle existing ObjectMove(local_chart_id, vf_name, 0, fixed_time, 0); //--- Move fixed vertical } }
Мы реализовали функцию "UpdateMovingCrosshair" для управления динамическим перекрестием на графике, задав имена для горизонтальных линий "Cross_H" и вертикальных линий "Cross_V", проверив их наличие с помощью ObjectFind (в противном случае возвращается отрицательное значение), создав их с помощью "ObjectCreate" как OBJ_HLINE или "OBJ_VLINE" красным цветом, сплошным стилем, если они отсутствуют, или переместив их с помощью ObjectMove к текущему времени и цене, если они присутствуют, обеспечивая отслеживание курсора в реальном времени. Далее мы создаём функцию "UpdateFixedCrosshair" для статического референсного перекрестия, используя имена "Cross_HF" и "Cross_VF", аналогично проверяя его существование, создавая его в сплошном стиле, зелёного цвета, в точке с параметрами fixed_time и fixed_price, или обновляя позиции, включая постоянные маркеры в режиме измерения для сравнений. Теперь обработаем её нажатие.
//+------------------------------------------------------------------+ //| Handle crosshair click | //+------------------------------------------------------------------+ void HandleCrosshairClick(datetime time, double price) { ulong now = GetMicrosecondCount(); //--- Get current microseconds if (now - last_click_time < 500000) { //--- Check for double-click if (!measuring) { //--- Check not measuring fixed_time = time; //--- Set fixed time fixed_price = price; //--- Set fixed price measuring = true; //--- Set measuring flag ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll current_instruction = "Measuring. Double-click to exit."; //--- Set instruction } else { //--- Handle exit measuring measuring = false; //--- Reset measuring DeleteMeasureObjects(); //--- Delete objects ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll current_instruction = "Move mouse for crosshair. Double-click to start measuring."; //--- Set instruction } DrawToolsPanel(); //--- Redraw tools panel last_click_time = 0; //--- Reset click time } else { //--- Handle single click last_click_time = now; //--- Update click time } }
Мы реализуем функцию "HandleCrosshairClick" для обработки кликов в режиме перекрестия, получая текущее значение в микросекундах с помощью функции GetMicrosecondCount для обнаружения двойных кликов в течение 500 мс после "last_click_time". При двойном щелчке переключается измерение: если режим не активный, устанавливается фиксированная точка на текущее время и цену, включается измерение, отключается прокрутка графика с помощью ChartSetInteger, обновляется указание; если режим активный, сбрасывается измерение, удаляются объекты с помощью "DeleteMeasureObjects", снова включается прокрутка и восстанавливается указание по умолчанию, затем перерисовывается панель инструментов и сбрасывается время клика. Для одиночных кликов мы обновляем значение параметра "last_click_time" до текущего момента, что позволяет определять последующие клики как двойные. Теперь давайте определим логику обновления измерительной линии между перекрестиями.
//+------------------------------------------------------------------+ //| Update measure line | //+------------------------------------------------------------------+ void UpdateMeasureLine(datetime time, double price) { string line_name = "Measure_Line"; //--- Set measure line name if (ObjectFind(local_chart_id, line_name) < 0) { //--- Check if line exists ObjectCreate(local_chart_id, line_name, OBJ_TREND, 0, fixed_time, fixed_price, time, price); //--- Create trend line ObjectSetInteger(local_chart_id, line_name, OBJPROP_COLOR, clrBlue); //--- Set color ObjectSetInteger(local_chart_id, line_name, OBJPROP_STYLE, STYLE_DASHDOT); //--- Set style ObjectSetInteger(local_chart_id, line_name, OBJPROP_WIDTH, 1); //--- Set width ObjectSetInteger(local_chart_id, line_name, OBJPROP_RAY, false); //--- Disable ray } else { //--- Handle existing ObjectMove(local_chart_id, line_name, 0, fixed_time, fixed_price); //--- Move point 0 ObjectMove(local_chart_id, line_name, 1, time, price); //--- Move point 1 } } //+------------------------------------------------------------------+ //| Update measure label | //+------------------------------------------------------------------+ void UpdateMeasureLabel(int mx, int my, datetime time, double price) { string label_name = "Measure_Label"; //--- Set measure label name long period_sec = PeriodSeconds(_Period); //--- Get period seconds long virtual_fixed = fixed_time / period_sec; //--- Compute virtual fixed long virtual_time = time / period_sec; //--- Compute virtual time int bars_diff = (int)MathAbs(virtual_fixed - virtual_time); //--- Compute bars difference double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); //--- Get point value long digits = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); //--- Get digits double pip_size = (digits == 3 || digits == 5) ? point * 10 : point; //--- Compute pip size double pips = MathAbs(price - fixed_price) / pip_size; //--- Compute pips string price_diff = DoubleToString(MathAbs(price - fixed_price), (int)digits); //--- Format price diff string text = StringFormat("%d bars, %.1f pips, Price Diff: %s", bars_diff, pips, price_diff); //--- Format text if (ObjectFind(local_chart_id, label_name) < 0) { //--- Check if label exists ObjectCreate(local_chart_id, label_name, OBJ_LABEL, 0, 0, 0); //--- Create label ObjectSetInteger(local_chart_id, label_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(local_chart_id, label_name, OBJPROP_XDISTANCE, mx + 20); //--- Set X distance ObjectSetInteger(local_chart_id, label_name, OBJPROP_YDISTANCE, my); //--- Set Y distance ObjectSetString(local_chart_id, label_name, OBJPROP_TEXT, text); //--- Set text ObjectSetInteger(local_chart_id, label_name, OBJPROP_FONTSIZE, 9); //--- Set font size ObjectSetString(local_chart_id, label_name, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetInteger(local_chart_id, label_name, OBJPROP_COLOR, clrBlue); //--- Set color } else { //--- Handle existing ObjectSetInteger(local_chart_id, label_name, OBJPROP_XDISTANCE, mx + 20); //--- Update X distance ObjectSetInteger(local_chart_id, label_name, OBJPROP_YDISTANCE, my); //--- Update Y distance ObjectSetString(local_chart_id, label_name, OBJPROP_TEXT, text); //--- Update text } }
Для измерительной линии мы реализуем функцию "UpdateMeasureLine", которая отображает соединительную линию в режиме измерения, задавая имя "Measure_Line", проверяя ее существование с помощью ObjectFind, создавая ее как OBJ_TREND от фиксированного значения к текущему значению времени/цены, в виде синей пунктирной линии, шириной 1 и без луча, если она отсутствует, или перемещая ее точки с помощью ObjectMove, если она присутствует, обеспечивая визуальную связь между точками. Мы создаём функцию "UpdateMeasureLabel" для отображения динамических метрик рядом с курсором мыши, вычисляя разницу баров путём деления времени на период в секундах из PeriodSeconds для получения виртуальных баров и вычисления абсолютного значения с помощью MathAbs, затем пипсов, используя значение символа из SymbolInfoDouble, скорректированное на 3 или 5 знаков (умноженное на 10), при этом абсолютная разница цен форматируется в цифры из функции SymbolInfoInteger.
Для форматирования текста с помощью StringFormat, включая бары, пипсы, округленные до одного десятичного знака и разницу цен, мы проверяем наличие метки, создаем ее как OBJ_LABEL в верхнем левом углу с учетом смещения курсора мыши, шрифтом "Arial" размером 9, синим цветом (если метка новая) или обновляем положение и текст (если метка уже существует), улучшая обратную связь с пользователем по результатам измерений. Вычисление этих меток имеет решающее значение, поскольку точно определяет временные и ценовые диапазоны для различных значений точности символов и периодов, что позволяет проводить точный анализ без ручных вычислений. Чтобы оживить палитру, мы обработаем события графика в обработчике событий для обеспечения интуитивно понятного взаимодействия.
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_KEYDOWN) { //--- Check key down event if (lparam == 27) { //--- Check escape key DeactivateTool(); //--- Deactivate tool } return; //--- Return } if (id != CHARTEVENT_MOUSE_MOVE) return; //--- Return if not mouse move int mx = (int)lparam; //--- Set mouse X int my = (int)dparam; //--- Set mouse Y int ms = (int)sparam; //--- Set mouse state bool prev_hh = header_hovered; //--- Store previous header hover bool prev_th = theme_hovered; //--- Store previous theme hover bool prev_mh = minimize_hovered; //--- Store previous minimize hover bool prev_ch = close_hovered; //--- Store previous close hover bool prev_rh = resize_hovered; //--- Store previous resize hover header_hovered = IsMouseOverHeader(mx, my); //--- Update header hover theme_hovered = IsMouseOverButton(mx, my, theme_x_offset); //--- Update theme hover minimize_hovered = IsMouseOverButton(mx, my, minimize_x_offset); //--- Update minimize hover close_hovered = IsMouseOverButton(mx, my, close_x_offset); //--- Update close hover resize_hovered = IsMouseOverResize(mx, my, hover_mode); //--- Update resize hover string resize_tooltip = ""; //--- Initialize resize tooltip if (resize_hovered) { //--- Check resize hovered switch (hover_mode) { //--- Switch on hover mode case BOTTOM: resize_tooltip = "Resize Bottom"; break; //--- Set bottom tooltip case RIGHT: resize_tooltip = "Resize Right"; break; //--- Set right tooltip case BOTTOM_RIGHT: resize_tooltip = "Resize Bottom-Right"; break; //--- Set bottom-right tooltip } ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, resize_tooltip); //--- Set tooltip } else if (!panel_minimized && hovered_tool_index >= 0) { //--- Check tool hovered ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, buttons[hovered_tool_index].tooltip); //--- Set tool tooltip } else { //--- Handle no hover ObjectSetString(0, canvasToolsName, OBJPROP_TOOLTIP, ""); //--- Clear tooltip } if (prev_hh != header_hovered || prev_th != theme_hovered || prev_mh != minimize_hovered || prev_ch != close_hovered || prev_rh != resize_hovered) { //--- Check if hovers changed DrawHeader(); //--- Redraw header if (!panel_minimized) DrawToolsPanel(); //--- Redraw tools if not minimized ChartRedraw(); //--- Redraw chart } if (!panel_minimized) { //--- Check not minimized int ht = GetHoveredTool(mx, my); //--- Get hovered tool if (ht != hovered_tool_index) { //--- Check if changed hovered_tool_index = ht; //--- Update index DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } } if (resize_hovered || resizing) { //--- Check resize hovered or resizing hover_mouse_local_x = mx - currentCanvasX; //--- Update local X hover_mouse_local_y = my - (currentCanvasY + header_height + tools_y_offset); //--- Update local Y DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } if (ms == 1 && prev_mouse_state == 0) { //--- Check mouse down if (header_hovered) { //--- Check header hovered panel_dragging = true; //--- Set dragging flag panel_drag_x = mx; //--- Set drag X panel_drag_y = my; //--- Set drag Y panel_start_x = currentCanvasX; //--- Set start X panel_start_y = currentCanvasY; //--- Set start Y ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll DrawHeader(); //--- Redraw header ChartRedraw(); //--- Redraw chart } else if (theme_hovered) { //--- Check theme hovered ToggleTheme(); //--- Toggle theme } else if (minimize_hovered) { //--- Check minimize hovered ToggleMinimize(); //--- Toggle minimize } else if (close_hovered) { //--- Check close hovered OnDeinit(0); //--- Call deinit } else if (!panel_minimized) { //--- Check not minimized int ht = GetHoveredTool(mx, my); //--- Get hovered tool if (ht >= 0) { //--- Check valid tool ToggleTool(buttons[ht].type); //--- Toggle tool } else if (active_tool != TOOL_NONE && active_tool != TOOL_CROSSHAIR) { //--- Check active tool HandleDrawing(mx, my); //--- Handle drawing } else if (IsMouseOverResize(mx, my, resize_mode)) { //--- Check resize area resizing = true; //--- Set resizing flag resize_start_x = mx; //--- Set start X resize_start_y = my; //--- Set start Y start_width = currentWidth; //--- Set start width start_height = currentHeight; //--- Set start height ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } } } if (panel_dragging && ms == 1) { //--- Check dragging int dx = mx - panel_drag_x; //--- Compute delta X int dy = my - panel_drag_y; //--- Compute delta Y currentCanvasX = MathMax(0, MathMin((int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - currentWidth, panel_start_x + dx)); //--- Update X currentCanvasY = MathMax(0, MathMin((int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - header_height - (panel_minimized ? 0 : currentHeight) - tools_y_offset, panel_start_y + dy)); //--- Update Y ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, currentCanvasX); //--- Set header X ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, currentCanvasY); //--- Set header Y if (!panel_minimized) { //--- Check not minimized ObjectSetInteger(0, canvasToolsName, OBJPROP_XDISTANCE, currentCanvasX); //--- Set tools X ObjectSetInteger(0, canvasToolsName, OBJPROP_YDISTANCE, currentCanvasY + header_height + tools_y_offset); //--- Set tools Y } ChartRedraw(); //--- Redraw chart } if (resizing && ms == 1) { //--- Check resizing int dx = mx - resize_start_x; //--- Compute delta X int dy = my - resize_start_y; //--- Compute delta Y int nw = MathMax(min_width, start_width + (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT ? dx : 0)); //--- Compute new width int nh = MathMax(min_height, start_height + (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT ? dy : 0)); //--- Compute new height nw = MathMin(nw, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - currentCanvasX); //--- Clamp width nh = MathMin(nh, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - currentCanvasY - header_height - tools_y_offset); //--- Clamp height currentWidth = nw; //--- Update width currentHeight = nh; //--- Update height canvasTools.Resize(currentWidth, currentHeight); //--- Resize tools canvas ObjectSetInteger(0, canvasToolsName, OBJPROP_XSIZE, currentWidth); //--- Set tools X size ObjectSetInteger(0, canvasToolsName, OBJPROP_YSIZE, currentHeight); //--- Set tools Y size canvasHeader.Resize(currentWidth, header_height); //--- Resize header canvas ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, currentWidth); //--- Set header X size ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Set header Y size DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } if (ms == 0 && prev_mouse_state == 1) { //--- Check mouse up panel_dragging = false; //--- Reset dragging resizing = false; //--- Reset resizing ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll DrawHeader(); //--- Redraw header DrawToolsPanel(); //--- Redraw tools ChartRedraw(); //--- Redraw chart } datetime time = 0; //--- Initialize time double price = 0.0; //--- Initialize price int sw = 0; //--- Initialize subwindow if (ChartXYToTimePrice(local_chart_id, mx, my, sw, time, price)) { //--- Convert XY to time/price if (active_tool == TOOL_CROSSHAIR) { //--- Check crosshair active UpdateMovingCrosshair(time, price); //--- Update moving crosshair if (measuring) { //--- Check measuring UpdateFixedCrosshair(); //--- Update fixed crosshair UpdateMeasureLine(time, price); //--- Update measure line UpdateMeasureLabel(mx, my, time, price); //--- Update measure label } ChartRedraw(); //--- Redraw chart } if (ms == 1 && prev_mouse_state == 0 && active_tool == TOOL_CROSSHAIR && !header_hovered && !theme_hovered && !minimize_hovered && !close_hovered && !resize_hovered && (panel_minimized || GetHoveredTool(mx, my) == -1)) { //--- Check crosshair click conditions HandleCrosshairClick(time, price); //--- Handle click ChartRedraw(); //--- Redraw chart } } prev_mouse_state = ms; //--- Update previous state }
Мы используем обработчик OnChartEvent для управления всеми взаимодействиями пользователя с палитрой и инструментами, начиная с проверки CHARTEVENT_KEYDOWN для обнаружения клавиши Escape (lparam 27) и деактивации текущего инструмента с помощью функции "DeactivateTool", после чего возвращаемся к исходному состоянию. Если вам интересно, то escape-символ в таблице ASCII имеет код 27. Смотрите ниже.

Далее, если событие CHARTEVENT_MOUSE_MOVE не выполняется, мы выходим; в противном случае, извлекаем координаты мыши по осям X, Y и состояние из параметров, сохраняем предыдущие флаги наведения курсора и обновляем их с помощью таких функций, как "IsMouseOverHeader", "IsMouseOverButton" для темы/сворачивания/закрытия и "IsMouseOverResize", которая устанавливает "hover_mode". Далее, с помощью ObjectSetString устанавливаем всплывающие подсказки на объекте Canvas инструментов для режимов изменения размера или при наведении курсора на кнопки инструментов, очищая их, если ни один из элементов не находится под курсором; если при наведении курсора что-либо изменилось, перерисовываем заголовок и инструменты (если не свернуто), после чего выполняется ChartRedraw функция. Если окно не свернуто, мы определяем индекс инструмента, на который наведен курсор с помощью функции "GetHoveredTool" и перерисовываем его при изменении; для состояний изменения размера обновляем локальные координаты при наведении курсора и перерисовываем инструменты.
При нажатии кнопки мыши (1 мс, prev 0) мы инициируем перетаскивание, если на заголовок наведен курсор, устанавливая флаги, отключая прокрутку и перерисовывая заголовок; переключаем тему или сворачиваем, если наведен курсор на соответствующие кнопки; вызываем deinit при закрытии; или, если наведен курсор на кнопку инструмента, переключаем ее тип; обрабатываем рисование, не являющееся перекрестием, с помощью "HandleDrawing"; начинаем изменение размера, если область изменения размера выходит за пределы, устанавливая флаги, запуская и отключая прокрутку перед перерисовкой инструментов. Во время перетаскивания (1 мс) мы вычисляем дельты, ограничиваем новые позиции границами графика с помощью MathMax и MathMin функций, обновляем глобальные переменные и устанавливаем расстояния между объектами для заголовка и инструментов (если они видны). Затем перерисовываем график. Для изменения размера (1 мс) вычисляем и ограничиваем новые размеры на основе режима, обновляем глобальные переменные, изменяем размер объектов Canvas с помощью Resize и устанавливаем размеры с помощью ObjectSetInteger, затем перерисовываем заголовок, инструменты и график.
При отпускании кнопки мыши (мс 0, prev 1) сбрасываем флаги перетаскивания и изменения размера, снова включаем прокрутку, перерисовываем заголовок и инструменты, а также обновляем график. Мы преобразуем данные мыши во время/цену с помощью ChartXYToTimePrice, и если перекрестие активно, обновляем движущееся перекрестие; если производится измерение, также обновляем фиксированную линию, линию измерения и метку; при щелчке мышью за пределами палитры обрабатываем событие с помощью "HandleCrosshairClick" и перерисовываем изображение. Наконец, обновим значение параметра "prev_mouse_state" до текущего значения для последующих событий, обеспечивая отзывчивое и сохраняющее состояние взаимодействие. Наконец, необходимо удалить объекты, когда они не нужны.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { canvasHeader.Destroy(); //--- Destroy header canvas canvasTools.Destroy(); //--- Destroy tools canvas DeleteCrosshair(); //--- Delete crosshair measuring = false; //--- Reset measuring flag ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable mouse scroll int total = ObjectsTotal(local_chart_id); //--- Get total objects for (int i = total - 1; i >= 0; i--) { //--- Loop over objects backward string obj_name = ObjectName(local_chart_id, i); //--- Get object name if (StringFind(obj_name, "Tool_") == 0) { //--- Check if our tool ObjectDelete(local_chart_id, obj_name); //--- Delete object } } ChartRedraw(); //--- Redraw chart }
В обработчике OnDeinit мы выполняем очистку, уничтожая объекты Canvas заголовка и инструментов с помощью метода Destroy, удаляя любое активное перекрестие с помощью метода "DeleteCrosshair", сбрасывая флаг измерения в значение false и повторно включая прокрутку мышью с помощью ChartSetInteger функции. Чтобы исключить остаточные объекты, мы получаем общее количество объектов с помощью ObjectsTotal на локальном графике, выполняем обратный цикл для безопасного удаления, получаем имена с помощью ObjectName и удаляем объекты с префиксом "Tool_" с помощью ObjectDelete функции. Наконец, мы вызываем ChartRedraw для обновления графика, завершая освобождение ресурсов при выходе из программы. После компиляции получаем следующий результат.

Как видно на изображении, мы создали интерактивную палитру инструментов, тем самым достигнув своих целей. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.
Тестирование на истории
Мы провели тестирование и ниже показан итоговый результат в формате Graphics Interchange Format (GIF).

Заключение
В заключение, мы создали интерактивную палитру инструментов на языке MQL5 для построения графиков, включающую перетаскиваемые и изменяемые по размеру панели с возможностью переключения между темным и светлым режимами. Мы добавили кнопки для различных инструментов, таких как перекрестия, линии тренда, прямые линии, прямоугольники, уровни коррекции Фибоначчи, текст и стрелки, управляющие событиями мыши для бесперебойной активации и взаимодействия на графике. Эта настраиваемая система улучшает анализ торговых операций, предоставляя обратную связь и указания в режиме реального времени, а также универсальный пользовательский интерфейс для эффективного сценария работы. С помощью этой интерактивной палитры инструментов вы сможете оптимизировать аннотирование и измерение графиков, что позволит интегрировать их в продвинутые торговые стратегии. В последующих частях мы перейдем к более детальной палитре с более упорядоченными инструментами. Следите за обновлениями!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21275
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Окончание)
Разработка инструментария для анализа Price Action (Часть 27): Инструмент выявления снятия ликвидности с MA-фильтром
Торговые инструменты на MQL5 (Часть 20): Построение графиков на Canvas с использованием статистической корреляции и регрессионного анализа
Внедрение в MQL5 практических модулей из других языков (Часть 06): Операции файлового ввода-вывода в MQL5, как в Python
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования