Знакомство с языком MQL5 (Часть 35): Освоение API и функции WebRequest в языке MQL5 (IX)
Введение
И снова приветствуем вас в Части 35 серии "Знакомство с языком MQL5"! В последней статье мы сосредоточились на разработке панели управления на графике MetaTrader 5, служащей в качестве фронтенда проекта. Мы узнали как создать компоновку панели, добавить кнопки и поля ввода и отображать текст на панели. На тот момент панель еще не взаимодействовала ни с какими внешними сервисами и была исключительно визуальной. В этом разделе, связав эту панель управления с логикой бэкенда, мы продвинем проект на шаг вперед. Основное внимание в этой статье уделяется управлению взаимодействием с пользователем с помощью событий графика, определению момента нажатия кнопки "Send" и использованию функции WebRequest для подготовки данных пользователя к отправке во внешний API.
Кроме того, мы опишем базовую схему получения ответа сервера и подготовки его к отображению на панели. Как и в предыдущих частях этой серии, мы не будем подробно разбирать каждую идею. Вместо этого мы сосредоточимся на том, что нужно для выполнения задания. Это позволяет избежать перегрузки лишней информацией и делает процесс обучения более практичным. После прочтения этой статьи панель управления не будет статичной. Она будет взаимодействовать с внешним API-сервером и активно реагировать на ввод пользователя.
Обнаружение нажатий кнопок с помощью событий графика
В этом разделе мы сосредоточимся на том, как MetaTrader 5 распознает ввод пользователя в панели управления и реагирует на него. MetaTrader 5 не рассматривает щелчок пользователя по кнопке (например, по кнопке "Send" на нашей панели) как обычный клик по объекту. Вместо этого платформа создает событие графика. Когда на графике происходит клик мыши, нажатие клавиши или взаимодействие с элементом управления, платформа отправляет в вашу программу сообщения, которые называются событиями графика. Самое важное в этой ситуации – понимать, что кнопки и другие элементы управления не выполняют логику самостоятельно. Они лишь сигнализируют о том, что произошло действие. Задача отслеживать эти сигналы и определять дальнейшие действия лежит на вашей программе. Это выполняется с помощью системы обработки событий графика, которая позволяет вашему коду реагировать, когда пользователь нажимает кнопку "Send".
При нажатии кнопки MetaTrader 5 формирует событие с подробностями о том, над чем было выполнено действие. В эти данные входят тип события и идентификатор элемента управления, который его вызвал. Анализируя эти данные, программа может определить, была ли нажата кнопка "Send", или событие пришло от другого объекта на графике. После того как будет подтверждено, что событие вызвала кнопка "Send", программа сможет считать ввод пользователя и подготовить его для вызова API. Такое четкое разделение обязанностей крайне важно. Событие графика сообщает о действии пользователя, представленном кнопкой, а ваш код определяет, что будет происходить дальше. Чтобы в следующем разделе серии связать панель управления с логикой бэкенда, которая будет взаимодействовать с ИИ, важно понимать этот процесс.
Пример://+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == send_button_name) { Comment("BUTTON WORKING PERFECTLY"); } } }
Вывод:

Пояснение:
MetaTrader 5 автоматически вызывает функцию OnChartEvent – специальный обработчик события – каждый раз, когда на графике что-то происходит. Она выполняется только при взаимодействии или наступлении события, в отличие от OnTick, которая выполняется постоянно. Клики мыши, нажатия клавиш, взаимодействие с объектами и операции на панели управления – вот несколько примеров таких событий. Назначение этой функции – отслеживать такие события и реагировать на них. Тип только что произошедшего события графика задается первым параметром, id. Когда пользователь кликает по объекту, нажимает клавишу или изменяет размер графика, MetaTrader 5 описывает эти действия с помощью предопределенных констант. Здесь код проверяет, соответствует ли тип события клику по объекту. Это означает, что программа игнорирует все другие события графика и обрабатывает только те, где пользователь сделал клик.
Следующая проверка после подтверждения клика по объекту определяет, какой объект вызвал событие. Далее в этой ситуации полезен параметр sparam. В значении sparam содержится имя объекта, с которым взаимодействовали. Сравнив это значение с именем кнопки "Send", программа может определить, был ли клик сделан по этой кнопке или по другому объекту на графике. Код в этом блоке выполняется, если выполнены оба условия: по объекту сделан клик, и этим объектом является кнопка "Send". В этом случае на графике отображается сообщение, подтверждающее, что нажатие кнопки было успешно распознано. Этот простой тест демонстрирует корректное подключение кнопки к системе событий графика.
Здесь особенно важно разделение обязанностей. При нажатии кнопка сама по себе ничего не делает. Вместо этого она взаимодействует с графиком. Затем ваш код, опираясь на сведения о событии, определяет, что делать дальше, после того как платформа вызовет обработчик события графика. Когда впоследствии вы свяжете кнопку "Send" с логикой, которая будет читать ввод пользователя и отправлять запрос в API, вам понадобится полный контроль над тем, как программа реагирует на действия пользователя.
Аналогия:
Представьте, что график – это активное рабочее пространство, а функция OnChartEvent – охранник, который наблюдает за ним. Охранник большую часть времени ничего не делает, но когда что-то происходит (например, нажимают кнопку), он сразу это замечает и принимает меры. Идентификатор события работает так же, как сообщение администратору о том, какое действие только что произошло. Это была нажата кнопка, кто-то постучал или зазвонил телефон? В этом примере администратор обращает внимание только на одно конкретное действие – кто-то нажимает кнопку на столе. Все остальные действия в комнате игнорируются.
Заметив, что нажали кнопку, администратор определяет, какая именно кнопка была нажата. Это как будто вы посмотрели на надпись на кнопке и увидели ее название. Администратор точно поймет, что нужно посетителю, если надпись соответствует кнопке "Send". Если кнопка или объект другие, администратор ничего не делает. После того как администратор убедится, что была нажата кнопка "Send", он выполняет нужное действие. В будущем этот же шаг будет расширен: он будет считывать сообщение пользователя и передавать его на сервер. В этом примере это действие лишь сообщает, что кнопка работает, – как если бы администратор сказал: "Ваш запрос получен".
Это сравнение подчеркивает важную идею. Действие не выполняется самой кнопкой. Все, что она делает, – сигнализирует о том, что ее нажали. В роли обработчика события графика стойка администратора – место, где и принимаются решения. Поэтому MetaTrader 5 обрабатывает нажатия кнопок через события графика, а не через саму кнопку.
Отправка API-запросов в ответ на действия пользователя
Когда пользователь вводит промпт в панель управления и нажимает кнопку "Send", этот клик служит сигналом для программы продолжить работу и взаимодействовать с API. Чтобы программа работала последовательно и эффективно, мы намеренно ждем ввода пользователя, а не отправляем запросы автоматически или на каждом тике. Эта связь между логикой и вводом пользователя в MetaTrader 5 управляется событиями графика. Когда нажимают кнопку "Send", платформа генерирует событие графика. В этом обработчике события программа может определить, какой элемент управления был нажат, и выполнить соответствующие действия. После того как будет подтверждено, что событие пришло от кнопки "Send", можно выполнить логику WebRequest. Это гарантирует, что запросы к API будут отправляться только тогда, когда пользователь явно этого хочет.
Эта стратегия важна по ряду причин. Во-первых, она предотвращает бессмысленные запросы к серверу и помогает не превысить ограничения по частоте запросов к API. Во-вторых, она дает пользователю полный контроль над отправкой и получением данных. Наконец, отделяя фоновую логику от взаимодействия с пользователем, она помогает поддерживать код хорошо структурированным. Логика бэкенда управляет взаимодействием с API, а интерфейс – вводом и нажатиями. Вы можете выстроить четкий и отзывчивый процесс, напрямую связав WebRequest с действиями пользователя. Панель управления превращается из визуального компонента в интерактивный шлюз, который дает пользователю контроль над тем, как и когда программа взаимодействует с внешними сервисами.
Пример:#include <Controls\Dialog.mqh> #include <Controls\Edit.mqh> #include <Controls\Button.mqh> #include <Controls\Label.mqh> CAppDialog panel; CEdit input_box; CButton send_button; string send_button_name = "SEND BUTTON"; CLabel response_display; string response_text_name = "AI REPONSE"; int panel_x = 32; int panel_y = 82; int panel_w = 600; int panel_h = 200; ulong chart_ID = ChartID(); string panel_name = "Google Generative AI"; string input_box_name = "INPUT BOX"; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h); input_box.Create(chart_ID,input_box_name,0,5,55,0,0); input_box.Width(500); input_box.Height(30); panel.Add(input_box); send_button.Create(chart_ID,send_button_name,0,510,55,556,85); send_button.Text("Send"); panel.Add(send_button); response_display.Create(0, "PanelText", 0, 0, 0, 0, 0); response_display.Text("THIS WILL BE THE SERVER RESPONSE......"); panel.Add(response_display); panel.Run(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { panel.Destroy(reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == send_button_name) { //Comment("BUTTON WORKING PERFECTLY"); string API_KEY = "AbcdefKJXiFPdvvM6f4ivPZ-zA2Qnoq612345"; string url = "https://generativelanguage.googleapis.com/v1beta/models/" "gemini-2.5-flash-lite:generateContent?key=" + API_KEY; string headers = "Content-Type: application/json\r\n"; string body = "{" "\"contents\": [" "{" "\"parts\": [" "{" "\"text\": \"" + input_box.Text() + "\"" "}" "]" "}" "]" "}"; char data[]; int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); if(copied > 0) ArrayResize(data, copied - 1); char result[]; string result_headers; int timeout = 15000; int response = WebRequest("POST",url,headers,timeout,data,result,result_headers); if(response == -1) { Print("WebRequest failed. Error: ", GetLastError()); return; } string response_text = CharArrayToString(result); Print(response_text); } } }
Вывод:

Пояснение:
Разберем этот код по шагам – от момента нажатия кнопки пользователем до получения ответа от сервера. Все начинается в обработчике события графика. MetaTrader 5 вызывает эту функцию немедленно каждый раз, когда на графике происходит клик мыши, нажатие клавиши или взаимодействие с элементом панели управления. При вызове этой функции MetaTrader 5 передает четыре параметра. В этом случае важнее всего идентификатор события, который указывает тип произошедшего действия, и текстовый аргумент, который содержит имя задействованного объекта.
Первое условие проверяет, соответствует ли идентификатор события значению "клик по объекту". Это гарантирует, что внутренняя логика запустится только тогда, когда пользователь кликнет по объекту на графике. Если бы этой проверки не было, код мог бы реагировать на другие события (например, движения мыши или изменения графика), что могло бы привести к непредсказуемому поведению. После того как подтверждено событие клика, следующая проверка сравнивает имя объекта из события с именем кнопки "Send". Этот шаг важен, потому что на графике одновременно может быть несколько объектов. Сверяя имя объекта, программа проверяет, что клик пришел от кнопки "Send", а не от какого-либо другого элемента управления или объекта. Программа не продолжит выполнение, пока это условие не будет выполнено.
После того как будет подтверждено, что нажали кнопку "Send", код настраивает все необходимое для взаимодействия с API Google Generative AI. Сначала задается API-ключ. Помимо идентификации вашего приложения для Google, этот ключ позволяет серверу вести учет использования, применять ограничения и аутентифицировать запросы. Без этого ключа запрос будет отклонен. Затем создается URL запроса. URL задает выполняемое действие, сервер, выбранную модель ИИ и версию API. Сервер может идентифицировать отправителя, добавляя API-ключ в URL. Это гарантирует, что приложению будет ясно и то, куда отправляется запрос, и с каким сервисом оно взаимодействует.
Далее настраиваются заголовки запроса. Заголовок Content-Type сообщает серверу, что данные отправляются в формате JSON, как того требует API. Без этого заголовка сервер может некорректно разобрать запрос. После того как заголовки заданы, формируется тело запроса. Это тело содержит фактическое сообщение, которое будет передано в ИИ. Вместо жестко заданного запроса текст берется напрямую из поля ввода на панели управления. Другими словами, ИИ получает запрос, который ввел пользователь. Текст пользователя помещается в соответствующие поля JSON, а тело формируется в соответствии с форматом, заданным API.
После того как тело сформировано как строка, его нужно преобразовать в массив символов. Это необходимо, потому что метод WebRequest в языке MQL5 не принимает строки напрямую в качестве тела запроса. В течение всего процесса преобразования используется кодировка UTF-8, чтобы гарантировать точную передачу всех символов, включая специальные. Чтобы после преобразования JSON-структура оставалась корректной, размер массива изменяется так, чтобы удалить лишний нулевой символ, который добавляется автоматически.
Функция WebRequest в языке MQL5 принимает данные только в таком формате; поэтому, когда тело запроса формируется как строка, его преобразуют в массив символов. Чтобы корректно передать все символы, включая специальные, на этом этапе используется кодировка UTF-8. Затем структура JSON сохраняется за счет изменения размера массива, чтобы удалить лишний нулевой символ, добавленный при преобразовании. Затем настраиваются переменные для сохранения ответа сервера. Данные ответа сохраняются в одной переменной, заголовки ответа – в другой. Также задается таймаут, чтобы ограничить время ожидания ответа перед завершением.
После этого вызывается функция WebRequest. На этом этапе запрос фактически отправляется на сервер. Функция принимает ответ в подготовленные переменные после отправки метода HTTP, URL, заголовков, таймаута и тела запроса. Функция возвращает значение, которое указывает, был ли запрос успешен. Код проверяет возвращаемое значение сразу после запроса. Если значение указывает на сбой, выполнение останавливается, а затем выводятся сообщение об ошибке и последний код ошибки. Этот этап обработки ошибок крайне важен: он упрощает отладку и не дает программе продолжать работу с некорректными или отсутствующими данными. Если запрос выполнен успешно, данные ответа, полученные в виде массива символов, преобразуются обратно в читаемую строку. Эта итоговая строка представляет собой необработанный ответ, который вернул сервер ИИ. К этому моменту программа успешно завершила весь цикл. В ответ на нажатие кнопки она собрала ввод пользователя, передала его в API и получила ответ, который можно просмотреть, дополнительно обработать или сохранить для использования в будущем.
Аналогия:
Представьте, что ваша панель – это крошечный стол, а ваш график – загруженный офис. Сообщение можно написать в поле ввода (как в блокноте), а кнопка отправки – это физическая кнопка на столе с надписью "Send". Сервер ИИ можно представить как удаленное рабочее место, где специалист готов отвечать на запросы. Функция события графика похожа на администратора за стойкой, который всегда следит за происходящим. Администратор замечает и сообщает обо всем, что происходит, например, когда кто-то касается кнопки. Однако прежде чем что-либо предпринимать, администратор сначала выясняет детали произошедшего. Если зафиксировано событие "клик по кнопке", администратор определяет, какая именно кнопка была нажата. Если это кнопка "Send", администратор понимает, что сообщение нужно переслать дальше.
После того как подтверждено, что по кнопке был сделан клик, программа собирает всю информацию, необходимую для отправки сообщения. Ваш офис может получить доступ к удаленному офису эксперта с помощью API-ключа, который работает как секретный код доступа. URL – это точный адрес удаленного рабочего места эксперта. Чтобы у эксперта не возникло путаницы, заголовки играют роль записки со словами: "Сообщение написано на понятном вам языке". Затем сообщение аккуратно переписывается из поля ввода блокнота в конверт – это похоже на преобразование строки в массив символов. Затем размер конверта изменяется так, чтобы в нем точно помещалось только сообщение, и не передавалось ничего лишнего.
Теперь конверт передается функции WebRequest, то есть, курьерской службе, вместе с ограничением по времени, которое называется таймаутом. Доставив его в офис эксперта, курьер ждет ответа. Если курьер не может выполнить доставку, вы получаете уведомление об ошибке, которое означает, что что-то пошло не так. Если доставка проходит успешно, курьер возвращает конверт с ответом эксперта, который затем открывается и преобразуется в читаемый для вас формат. Если кратко, пользователь пишет сообщение и нажимает кнопку "Send". После этого администратор проверяет действие, подготавливает сообщение со всей необходимой информацией, отправляет его эксперту через курьера, а затем приносит ответ эксперта обратно, чтобы вы увидели его на своем столе. Именно так управляемый событиями интерфейс панели управления в MetaTrader 5 взаимодействует с бэкенд-сервисами.
Извлечение полезных данных из ответа API
Функция WebRequest часто будет получать от сервера ответ в структурированном формате, например, JSON. Помимо сгенерированного ответа ИИ, этот ответ содержит и другую информацию, например, метаданные или внутренние детали. Для целей нашего проекта мы сосредотачиваемся только на получении текста, который ИИ сгенерировал в ответ на ввод пользователя. Чтобы извлечь эту информацию, сначала мы преобразуем необработанный массив байтов, полученный через WebRequest, в читаемый текст. Так мы получаем полный ответ сервера в виде, удобном для дальнейшей обработки. Затем находится часть строки, в которой содержится текст ИИ. Обычно она помечена определенным ключом, например, "text" в ответе Google Generative AI. После того как с помощью строковых функций определена начальная позиция этого ключа в строке, мы извлекаем подстроку, содержащую фактический ответ ИИ.
Извлеченный текст теперь можно сохранить в переменной и использовать в любом месте приложения, в том числе на панели. Изолируя только нужные данные, мы гарантируем, что остальная часть ответа сервера не будет влиять на отображение или последующую обработку. Этот этап важен для того, чтобы приложение сохраняло ясность и фокус на необходимых деталях, а пользователь видел только значимые результаты, созданные ИИ.
Пример:#include <Controls\Dialog.mqh> #include <Controls\Edit.mqh> #include <Controls\Button.mqh> #include <Controls\Label.mqh> CAppDialog panel; CEdit input_box; CButton send_button; string send_button_name = "SEND BUTTON"; CLabel response_display; string response_text_name = "AI REPONSE"; int panel_x = 32; int panel_y = 82; int panel_w = 600; int panel_h = 200; ulong chart_ID = ChartID(); string panel_name = "Google Generative AI"; string input_box_name = "INPUT BOX"; string ai_response; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h); input_box.Create(chart_ID,input_box_name,0,5,55,0,0); input_box.Width(500); input_box.Height(30); panel.Add(input_box); send_button.Create(chart_ID,send_button_name,0,510,55,556,85); send_button.Text("Send"); panel.Add(send_button); response_display.Create(0, "PanelText", 0, 0, 0, 0, 0); panel.Add(response_display); panel.Run(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { panel.Destroy(reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == send_button_name) { //Comment("BUTTON WORKING PERFECTLY"); string API_KEY = "AIzaSyCKJXiFPdvvM6f4ivPZ-zA2Qnoq6g62X7M"; string url = "https://generativelanguage.googleapis.com/v1beta/models/" "gemini-2.5-flash-lite:generateContent?key=" + API_KEY; string headers = "Content-Type: application/json\r\n"; string body = "{" "\"contents\": [" "{" "\"parts\": [" "{" "\"text\": \"" + input_box.Text() + "\"" "}" "]" "}" "]" "}"; char data[]; int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); if(copied > 0) ArrayResize(data, copied - 1); char result[]; string result_headers; int timeout = 15000; int response = WebRequest("POST",url,headers,timeout,data,result,result_headers); if(response == -1) { Print("WebRequest failed. Error: ", GetLastError()); return; } string response_text = CharArrayToString(result); // Print(response_text); string pattern = "\"text\": "; int pattern_lenght = StringFind(response_text,pattern); pattern_lenght += StringLen(pattern); int end = StringFind(response_text,"}",pattern_lenght + 1); ai_response = StringSubstr(response_text,pattern_lenght,end - pattern_lenght); response_display.Text(ai_response); } } }
Вывод:

Пояснение:
Чтобы сохранить ответ ИИ, сначала мы объявляем переменную. В этой переменной ai_response будет храниться только нужный текст, извлеченный из более длинного ответа сервера и отделенный от метаданных и других данных. Затем мы задаем строку-шаблон, которая представляет ключ в ответе сервера, где хранится текст, созданный ИИ. Фактический текст ответа в JSON-структуре Google Generative AI помечен как "text". Данный шаблон указывает программе, где искать начало нужного сообщения. Затем мы используем функцию поиска по строке, чтобы найти этот шаблон во всем сообщении ответа. В результате мы получаем первую позицию этого шаблона в тексте. Мы добавляем длину шаблона к этой позиции, чтобы начать извлечение сразу после ключа. Так мы получаем точное начало сообщения ИИ.
После начальной позиции выполняется поиск следующей закрывающей фигурной скобки }, которая определяет конец сообщения. Это позволяет определить, где заканчивается текст ИИ, чтобы мы могли записать только нужную часть ответа и исключить остальное. После того как становятся известны начальная и конечная позиции, мы используем функцию подстроки, чтобы получить подстроку с ответом ИИ. Переменная ai_response содержит сгенерированный текст. Наконец, мы обновляем содержимое метки, чтобы отобразить извлеченный текст на панели. Этот шаг гарантирует, что интерфейс панели управления будет напрямую показывать пользователю ответ ИИ.
Аналогия:
Допустим, вы библиотекарь, и издательство присылает вам большой конверт. Внутри находится множество документов, включая отчеты, плакаты, объявления и счета. Рецензия на книгу, которая вас интересует, находится лишь на одной из этих страниц. Ваша задача – просмотреть документы, найти нужную страницу и разместить ее на доске объявлений в библиотеке, чтобы все могли ее прочитать. Чтобы сохранить только рецензию на книгу, вы создаете отдельную папку с именем ai_response. Это равносильно тому, что вы говорите: "У меня есть место, где я буду хранить только нужное мне сообщение и ничего больше". Далее вы знаете, что каждое письмо имеет заголовок "text":. Это ваша подсказка – как стикер на конверте с надписью: "Сообщение начинается здесь". Вы ищете в стопке первый конверт с такой меткой. Место, где вы его находите, становится начальной точкой письма, которое нужно извлечь.
Но просто найти эту метку недостаточно. Чтобы случайно не добавить к отображению лишние документы, нужно знать, где письмо заканчивается. Вы ищете закрывающий знак, который появляется после каждого раздела в JSON-ответе. Это похоже на ситуацию, когда вы открываете конверт, читаете последнюю строку письма и понимаете, что все, что идет дальше, уже относится к чему-то другому. После того как вы точно определили начало и конец, вы аккуратно вырезаете письмо из остальной почты. Именно это и делает функция подстроки: она извлекает только текст между началом и концом, давая вам понятную рецензию на книгу без лишних деталей. Наконец, вы помещаете письмо на доску response_display. Теперь сообщение легко видно всем, кто приходит в библиотеку. Нужная информация аккуратно представлена в рамке, поэтому посетителям не придется самим разбирать стопку документов.
Реализация прокрутки текста на панели управления
В этом разделе мы рассмотрим, как сделать так, чтобы ответ ИИ прокручивался внутри панели управления. В отличие от обычных текстовых областей в программировании, панель управления в языке MQL5 не распознает переносы строк с помощью \n. Это означает, что, если ответ сервера длинный, текст просто выйдет за пределы видимой области: он не будет автоматически переноситься и не начнет отображаться с новой строки. Мы решаем эту задачу, реализуя механизм прокрутки, который постепенно смещает видимую часть текста внутри метки.
Цель состоит в том, чтобы постепенно менять видимую часть текста, при этом в каждый момент времени отображая только фрагмент ответа. Мы добиваемся этого с помощью таймера, который периодически запускает функцию. При каждом срабатывании эта функция обновляет метку новым фрагментом ответа, создавая иллюзию непрерывной прокрутки. Этот метод гарантирует, что даже очень длинные ответы ИИ будут читабельны в пределах заданного размера панели без потери важной информации и без необходимости в утомительной ручной прокрутке. Благодаря реализации прокрутки текста пользователи смогут без труда прочитать весь ответ ИИ, а панель при этом остается компактной и хорошо вписывается в график. Чтобы обеспечить плавное и понятное отображение, в следующих разделах мы разберем логику кода для извлечения содержимого, обновления метки и регулирования скорости прокрутки.
Пример://+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { EventSetMillisecondTimer(150); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ //| Timer function for animation | //+------------------------------------------------------------------+ void OnTimer() { Print("EURUSD") }
Пояснение:
На этапе инициализации программы запускается миллисекундный таймер с коротким интервалом. Этот таймер создает последовательный и предсказуемый ритм, благодаря которому программа может многократно обновлять пользовательский интерфейс. Таймер позволяет программе постепенно обновлять отображаемый текст, вместо того чтобы пытаться сразу вывести весь ответ сервера, что для панели управления непрактично. Обработчик таймера вызывается автоматически каждый раз, когда происходит событие таймера. Это позволяет при каждом вызове немного смещать видимую часть текста. Например, приложение может имитировать движение вверх или в сторону, менять начальную позицию текста или показывать следующий набор символов. Поскольку таймер срабатывает часто, текст кажется медленно прокручивающимся, а не скачущим рывками. Так мы можем имитировать прокрутку без символов переноса строки.
На этапе деинициализации таймер останавливается, когда советник удаляется с графика, или когда меняется состояние графика. Это важно, поскольку прокрутка – непрерывный процесс, требующий частых обновлений. Чтобы логика прокрутки не продолжала работать в фоне после исчезновения панели, таймер нужно остановить. Кроме того, это предотвращает ненужную обработку и поддерживает стабильность платформы.
Аналогия:
Представьте панель управления как небольшую электронную доску объявлений с ограниченной областью отображения текста. Ответ сервера похож на длинное письмо, написанное на листе бумаги, который значительно длиннее экрана этой доски объявлений. Поскольку экран не может показать все сообщение сразу и не поддерживает переносы строк, нужен способ медленно сдвигать бумагу, чтобы со временем становились видны разные части сообщения. Это похоже на включение маленького моторчика внутри доски объявлений при установке таймера во время инициализации. В данном случае этот мотор работает с заданным интервалом – 150 миллисекунд. При каждом срабатывании мотор дает системе команду немного сдвинуть бумагу. Благодаря равномерным срабатываниям движение получается плавным и управляемым, а не хаотичным.
Основное движение происходит в обработчике таймера. Каждый раз, когда мотор срабатывает, вы можете изменить ту часть сообщения, которая сейчас видна на экране. Хотя размер экрана не меняется, и новый функционал не появляется, зрителю кажется, что текст прокручивается. Каждый раз вы показываете лишь другой фрагмент одного и того же длинного сообщения. Мотор выключается, когда доску объявлений убирают или отключают. Это не дает ему работать, когда экрана для отображения сообщения уже нет. Аналогичным образом, когда приложение завершается, остановка таймера гарантирует, что процесс прокрутки завершится корректно и не будет расходовать ресурсы.
Теперь, когда мы понимаем, как это работает, следующим шагом будет использование обработчика события OnTimer, чтобы заставить текст прокручиваться. Вместо того чтобы сразу показывать весь ответ сервера, мы будем использовать таймер для периодического обновления текста, отображаемого на панели управления. Каждый раз, когда таймер срабатывает, будет показан небольшой фрагмент ответа, который при следующем тике немного сместится. При многократном повторении текст будет выглядеть так, будто плавно движется по экрану. Хотя панели управления не поддерживают многострочный текст и переносы строк, этот метод позволяет нам работать с длинными ответами ИИ простым и понятным способом.
Пример:int display_length;//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == send_button_name) { //Comment("BUTTON WORKING PERFECTLY"); string API_KEY = "AIzaSyCKJXiFPdvvM6f4ivPZ-zA2Qnoq6g62X7M"; string url = "https://generativelanguage.googleapis.com/v1beta/models/" "gemini-2.5-flash-lite:generateContent?key=" + API_KEY; string headers = "Content-Type: application/json\r\n"; string body = "{" "\"contents\": [" "{" "\"parts\": [" "{" "\"text\": \"" + input_box.Text() + "\"" "}" "]" "}" "]" "}"; char data[]; int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); if(copied > 0) ArrayResize(data, copied - 1); char result[]; string result_headers; int timeout = 15000; int response = WebRequest("POST",url,headers,timeout,data,result,result_headers); if(response == -1) { Print("WebRequest failed. Error: ", GetLastError()); return; } string response_text = CharArrayToString(result); // Print(response_text); string pattern = "\"text\": "; int pattern_lenght = StringFind(response_text,pattern); pattern_lenght += StringLen(pattern); int end = StringFind(response_text,"}",pattern_lenght + 1); ai_response = StringSubstr(response_text,pattern_lenght,end - pattern_lenght); // response_display.Text(ai_response); Print(ai_response); int res_lenght = StringLen(ai_response); if(res_lenght < 100) { display_length = res_lenght; } if(res_lenght >= 100) { display_length = 100; } } } }
Пояснение:
В этом случае задача кода – определить, какой объем текста, созданного ИИ, должен одновременно отображаться на панели управления. Сначала вычисляется длина ответа, то есть, подсчитывается каждый символ в тексте. Благодаря этой информации программа четко понимает размер ответа еще до того, как начинает управлять его отображением. Затем на основе этой длины принимается простое решение. Если ответ короче 100 символов, программа настраивает длину отображения так, чтобы показать весь ответ целиком. Поскольку текст уже помещается на панели, его можно сразу показать полностью, без прокрутки.
Если ответ ИИ длиннее 100 символов, программа ограничивает видимую часть 100 символами за раз. Сначала виден только этот фрагмент; остальная часть текста постепенно становится видимой благодаря прокрутке. Это сохраняет аккуратный вид панели и не дает длинным ответам занимать слишком много места, при этом позволяя со временем увидеть сообщение целиком.
Аналогия:
Представьте книжную полку с небольшим окном. Длинная стопка книг на полке представляет ответ ИИ. Как и при измерении длины ответа, сначала вы подсчитываете количество томов. Если книг меньше 100, окно может показать их все сразу, и вам не придется его сдвигать. Если книг больше 100, в окно помещается только 100. Вам нужно сдвигать окно, чтобы увидеть остальные. Чтобы полка выглядела аккуратно, пока вы просматриваете все книги, установка предела отображения работает как выбор подходящего размера окна, такого, чтобы книги не выходили за его границы.
После того как вы определили, сколько символов должно отображаться на панели за раз, для прокрутки текста используется обработчик события OnTimer. Чтобы даже длинные ответы ИИ постепенно отображались в пределах фиксированного пространства панели, функция OnTimer по одному символу за раз сдвигает видимую часть текста. Это обеспечивает пользователю плавную прокрутку, позволяя панели отображать все содержимое без переполнения.
Пример:
#include <Controls\Dialog.mqh> #include <Controls\Edit.mqh> #include <Controls\Button.mqh> #include <Controls\Label.mqh> CAppDialog panel; CEdit input_box; CButton send_button; string send_button_name = "SEND BUTTON"; CLabel response_display; string response_text_name = "AI REPONSE"; int panel_x = 32; int panel_y = 82; int panel_w = 600; int panel_h = 200; ulong chart_ID = ChartID(); string panel_name = "Google Generative AI"; string input_box_name = "INPUT BOX"; string ai_response = " "; int display_length; // Number of characters visible at once int scroll_pos = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h); input_box.Create(chart_ID,input_box_name,0,5,55,0,0); input_box.Width(500); input_box.Height(30); panel.Add(input_box); send_button.Create(chart_ID,send_button_name,0,510,55,556,85); send_button.Text("Send"); panel.Add(send_button); response_display.Create(0, "PanelText", 0, 0, 0, 0, 0); response_display.Text(ai_response); panel.Add(response_display); EventSetMillisecondTimer(150); panel.Run(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); panel.Destroy(reason); }
//+------------------------------------------------------------------+ //| Timer function for animation | //+------------------------------------------------------------------+ void OnTimer() { int message_len = StringLen(ai_response); // SAFETY CHECK (very important) if(message_len == 0) return; string visible_text = ""; for(int i = 0; i < display_length; i++) { int char_index = (scroll_pos + i) % message_len; visible_text += StringSubstr(ai_response, char_index, 1); } response_display.Text(visible_text); scroll_pos++; if(scroll_pos >= message_len) scroll_pos = 0; }
Вывод:

Пояснение:
Сначала функция определяет общую длину строки ответа ИИ. Это дает приложению количество символов в сообщении, что важно для определения момента, когда при прокрутке нужно будет вернуться к началу текста. Это можно сравнить с измерением всей ленты перед тем, как пропустить ее через окно просмотра. После этого выполняется проверка безопасности. Если ответ ИИ пуст, функция сразу останавливается и ничего не делает. Это не дает программе пытаться прокручивать строку, которой не существует. Это похоже на то, как если бы вы сначала убедились, что лента вообще есть, и только потом пытались двигать ее через окно просмотра. Фрагмент ответа ИИ, который должен появиться на панели сейчас, сохраняется во временной строке. Представьте, что вы открываете маленькое окно на длинной ленте, чтобы видеть только ее часть. Затем цикл добавляет в видимую строку по одному символу из ответа ИИ в количестве, заданном длиной отображения. Это похоже на то, как лента движется вдоль окна, показывая за раз только один свой участок.
Внутри цикла программа определяет, какой символ ответа ИИ должен быть показан следующим. Операция по модулю гарантирует, что прокрутка будет продолжаться бесконечно: когда достигается конец ответа, отображение снова возвращается к началу. Подобно тому как лента проходит через крошечное окно, каждый выбранный символ добавляется к видимой строке по одному, благодаря чему зритель видит непрерывный поток текста. После того как собраны все символы для текущего кадра, программа отображает этот фрагмент ответа ИИ пользователю, задавая видимый текст на панели. Это похоже на то, как лента целиком попадает в крошечное окно, чтобы читатель мог ее увидеть. После обновления отображения начальная позиция прокрутки сдвигается на один символ, поэтому на следующем тике таймера будет показан следующий фрагмент и создастся плавный эффект прокрутки.
Аналогия:
Представьте ответ ИИ как длинную ленту с сообщением на ней. Панель отображения позволяет видеть только часть ленты за раз – как маленькое окно в стене. Сначала программа определяет длину ленты. Перед тем как начать прокручивать ленту через окно, ее словно растягивают, чтобы определить ее длину. Понимание полной длины важно, поскольку оно показывает, когда мы дойдем до конца и должны будем вернуться к началу. Затем программа выполняет проверку безопасности. Если текста нет, то лента не движется, и ничего не происходит, что позволяет избежать ошибок или пустого отображения. Затем формируется пустая строка для хранения той части ленты, которая будет видна через окно – так же, как если бы вы выбрали в рамке участок, который нужно показать зрителю.
Затем выполняется цикл, чтобы заполнить это окно. Для каждого символа в пределах длины отображения программа выбирает соответствующий символ из ленты и вставляет его в видимый текст. Это похоже на то, как лента проходит перед окном, и символы появляются по одному, делая сообщение видимым. Когда все символы для текущего кадра собраны, программа обновляет панель, чтобы отобразить этот фрагмент ленты. Это похоже на то, как через стекло четко видна текущая часть ленты. После отображения текущего фрагмента позиция прокрутки сдвигается на один символ, чтобы на следующем тике таймера окно показало следующую часть ленты. Как будто вы подталкиваете ленту вперед, чтобы сообщение плавно проходило через окно.
Наконец, когда прокрутка достигает конца ленты, программа сбрасывает позицию в начало. Это гарантирует, что сообщение будет циклически повторяться, подобно тому как лента возвращается к началу для бесконечного отображения сообщения.
Заключение
В этой статье мы рассмотрели, как сделать так, чтобы наша панель управления в MetaTrader 5 реагировала на действия пользователя, определяя эти действия и отправляя запросы в API. Мы узнали, как обрабатывать события графика, чтобы программа реагировала, когда пользователь нажимает кнопку "Send", и как обрабатывать ответ сервера, чтобы извлекать из него полезный текст, сгенерированный ИИ. Мы также ввели концепцию прокрутки текста, используя обработчик события OnTimer, чтобы создать плавное и непрерывное отображение для ответов, которые длиннее, чем панель может показать за раз.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20859
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 69): Использование паттернов SAR и RVI
Создание торговой панели администратора на MQL5 (Часть XII): Интеграция форекс-калькулятора
Преодоление ограничений машинного обучения (Часть 3): Новый взгляд на неустранимую ошибку
Торговые инструменты на языке MQL5 (Часть 8): Улучшенная информационная панель с возможностью перетаскивания и сворачивания
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования