English Deutsch 日本語
preview
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 5): Сворачиваемая боковая панель и всплывающие окна истории чатов

Создание торговых систем с искусственным интеллектом на MQL5 (Часть 5): Сворачиваемая боковая панель и всплывающие окна истории чатов

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

Введение

В нашей предыдущей статье (часть 4) мы улучшили программу на MetaQuotes Language 5 (MQL5) с интеграцией ChatGPT: добавили обработку многострочного ввода, зашифрованное хранение чатов с помощью AES256 и ZIP, а также генерацию торговых сигналов по данным графика. В части 5 мы добавляем в торговую систему с ИИ сворачиваемую боковую панель. В обновлении добавлена динамическая боковая панель, которая переключается между развернутым и свернутым состояниями, помогая эффективнее использовать пространство экрана. Малое и большое всплывающие окна истории обеспечивают быстрый доступ к истории чатов, а обработка многострочного ввода, зашифрованное хранение между сеансами и генерация сигналов сохраняются. Мы рассмотрим следующие темы:

  1. Значение сворачиваемой боковой панели в торговых интерфейсах с ИИ
  2. Реализация на MQL5
  3. Тестирование сворачиваемой боковой панели
  4. Заключение

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


Значение сворачиваемой боковой панели в торговых интерфейсах с ИИ

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

Наш подход развивает сильные стороны существующей системы. Если вы еще не знакомы с ней, обратитесь к предыдущим статьям серии. Сворачиваемая боковая панель расширяет возможности системы: в развернутом режиме она обеспечивает понятную навигацию по чатам, а в свернутом занимает минимум места. Малое всплывающее окно открывает быстрый доступ к истории, а большое позволяет просматривать и выбирать все чаты. Благодаря этому переход между режимами остается плавным и понятным. Такой подход учитывает потребности трейдеров, которым нужны как компактный, так и детализированный режимы просмотра. Интерфейс адаптируется к разным задачам, сохраняя возможность работать с инсайтами, сгенерированными ИИ, для выявления рыночных возможностей, включая анализ трендов и разворотов. Мы также добавим более крупное представление чата для функций динамического поиска, но пока ограничимся базовой реализацией. Ниже приведены визуальные примеры предполагаемых вариантов компоновки интерфейса.

Свернутая боковая панель с малым всплывающим окном истории:

CONTRACTED SIDEBAR WITH SMALL HISTORY POPUP

Большое всплывающее окно истории:

LARGE HISTORY POPUP WITH SCROLLBAR


Реализация на MQL5

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

#define BIG_SCROLL_LEADER "ChatGPT_Big_Scroll_Leader"
#define BIG_SCROLL_UP_REC "ChatGPT_Big_Scroll_Up_Rec"
#define BIG_SCROLL_UP_LABEL "ChatGPT_Big_Scroll_Up_Label"
#define BIG_SCROLL_DOWN_REC "ChatGPT_Big_Scroll_Down_Rec"
#define BIG_SCROLL_DOWN_LABEL "ChatGPT_Big_Scroll_Down_Label"
#define BIG_SCROLL_SLIDER "ChatGPT_Big_Scroll_Slider"

Здесь с помощью #define определяются константы элементов полосы прокрутки большого окна истории: BIG_SCROLL_LEADER для дорожки, BIG_SCROLL_UP_REC и BIG_SCROLL_UP_LABEL для кнопки прокрутки вверх и ее стрелки, BIG_SCROLL_DOWN_REC и BIG_SCROLL_DOWN_LABEL для кнопки прокрутки вниз, а также BIG_SCROLL_SLIDER для перетаскиваемого ползунка. Это обеспечит единообразные ссылки на объекты в функциях создания объектов и управления ими при реализации прокрутки. Далее добавим глобальные переменные для управления этими новыми элементами.

color toggle_original_bg = clrLightGray;
color toggle_darker_bg;
bool toggle_hover = false;
color history_original_bg = clrWhite;
color history_darker_bg;
bool history_hover = false;
color see_more_original_bg = clrRoyalBlue;
color see_more_darker_bg;
bool see_more_hover = false;
color big_close_original_bg = clrLightGray;
color big_close_darker_bg;
bool big_close_hover = false;
int expandedSidebarWidth = 150;
int contractedSidebarWidth = 50;
bool sidebarExpanded = true;

bool showing_small_history_popup = false;
int small_popup_x, small_popup_y, small_popup_w, small_popup_h;
string small_popup_objects[];
string small_chat_bgs[];
bool showing_big_history_popup = false;
int big_popup_x, big_popup_y, big_popup_w, big_popup_h;
string big_popup_objects[];
string big_chat_bgs[];
string side_chat_bgs[];
int big_scroll_pos = 0;
bool big_scroll_visible = false;
int big_total_height = 0;
int big_visible_height = 0;
int big_slider_height = 20;
bool big_movingStateSlider = false;
int big_mlbDownX_Slider = 0;
int big_mlbDownY_Slider = 0;
int big_mlbDown_YD_Slider = 0;
bool just_opened_big = false;
bool just_opened_small = false;

Далее определяются переменные сворачиваемой боковой панели и всплывающих окон истории. Сначала задаются цвета кнопки переключения (toggle_original_bg – светло-серый, toggle_darker_bg – при наведении), кнопки истории (history_original_bg – белый, history_darker_bg), кнопки See More (see_more_original_bg – цвет royal blue, see_more_darker_bg) и кнопки закрытия большого окна (big_close_original_bg – светло-серый, big_close_darker_bg). Для каждой кнопки предусмотрен флаг наведения (toggle_hover, history_hover, see_more_hover, big_close_hover) со значением false при инициализации. Для задания размера боковой панели переменной expandedSidebarWidth присваивается значение 150 пикселей, а contractedSidebarWidth – 50 пикселей. Переменная sidebarExpanded по умолчанию имеет значение true, соответствующее развернутому состоянию.

Используются два всплывающих окна истории. Для малого окна определены флаг showing_small_history_popup, координаты small_popup_x и small_popup_y, размеры small_popup_w и small_popup_h, а также массивы small_popup_objects и small_chat_bgs для объектов и фонов чатов. Для большого окна определены флаг showing_big_history_popup, координаты big_popup_x и big_popup_y, размеры big_popup_w и big_popup_h, массивы big_popup_objects и big_chat_bgs, а также параметры полосы прокрутки: big_scroll_pos, big_scroll_visible, big_total_height, big_visible_height, big_slider_height со значением 20, big_movingStateSlider, big_mlbDownX_Slider, big_mlbDownY_Slider, big_mlbDown_YD_Slider. Массив side_chat_bgs хранит фоны чатов на боковой панели, а флаги just_opened_big и just_opened_small предотвращают немедленное закрытие окон после открытия. Это обеспечивает динамическое управление интерфейсом, сворачиваемой навигацией и доступом к истории чатов. Теперь с помощью этих переменных инициализируем новый интерфейс.

int OnInit() {
   //--- initialize the new variables
   toggle_darker_bg = DarkenColor(toggle_original_bg);
   history_darker_bg = DarkenColor(history_original_bg, 0.9);
   see_more_darker_bg = DarkenColor(see_more_original_bg);
   big_close_darker_bg = DarkenColor(big_close_original_bg);
   logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT);
   if (logFileHandle == INVALID_HANDLE) {
      Print("Failed to open log file: ", GetLastError());
      return(INIT_FAILED);
   }
   FileSeek(logFileHandle, 0, SEEK_END);
   uint img_pixels[];
   uint orig_width = 0, orig_height = 0;
   bool image_loaded = ResourceReadImage(resourceImg, img_pixels, orig_width, orig_height);
   if (image_loaded && orig_width > 0 && orig_height > 0) {
      ScaleImage(img_pixels, (int)orig_width, (int)orig_height, 104, 40);
      g_scaled_image_resource = "::ChatGPT_HeaderImageScaled";
      if (ResourceCreate(g_scaled_image_resource, img_pixels, 104, 40, 0, 0, 104, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled image resource created successfully");
      } else {
         Print("Failed to create scaled image resource");
      }
   } else {
      Print("Failed to load original image resource");
   }
   uint img_pixels_logo[];
   uint orig_width_logo = 0, orig_height_logo = 0;
   bool image_loaded_logo = ResourceReadImage(resourceImgLogo, img_pixels_logo, orig_width_logo, orig_height_logo);
   if (image_loaded_logo && orig_width_logo > 0 && orig_height_logo > 0) {
      ScaleImage(img_pixels_logo, (int)orig_width_logo, (int)orig_height_logo, 81, 81);
      g_scaled_sidebar_resource = "::ChatGPT_SidebarImageScaled";
      if (ResourceCreate(g_scaled_sidebar_resource, img_pixels_logo, 81, 81, 0, 0, 81, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled sidebar image resource created successfully");
      } else {
         Print("Failed to create scaled sidebar image resource");
      }
      uint img_pixels_logo_small[];
      ArrayCopy(img_pixels_logo_small, img_pixels_logo);
      ScaleImage(img_pixels_logo_small, 81, 81, 30, 30);
      g_scaled_sidebar_small = "::ChatGPT_SidebarImageSmall";
      if (ResourceCreate(g_scaled_sidebar_small, img_pixels_logo_small, 30, 30, 0, 0, 30, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled small sidebar image resource created successfully");
      } else {
         Print("Failed to create scaled small sidebar image resource");
      }
   } else {
      Print("Failed to load sidebar image resource");
   }
   uint img_pixels_newchat[];
   uint orig_width_newchat = 0, orig_height_newchat = 0;
   bool image_loaded_newchat = ResourceReadImage(resourceNewChat, img_pixels_newchat, orig_width_newchat, orig_height_newchat);
   if (image_loaded_newchat && orig_width_newchat > 0 && orig_height_newchat > 0) {
      ScaleImage(img_pixels_newchat, (int)orig_width_newchat, (int)orig_height_newchat, 30, 30);
      g_scaled_newchat_resource = "::ChatGPT_NewChatIconScaled";
      if (ResourceCreate(g_scaled_newchat_resource, img_pixels_newchat, 30, 30, 0, 0, 30, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled new chat icon resource created successfully");
      } else {
         Print("Failed to create scaled new chat icon resource");
      }
   } else {
      Print("Failed to load new chat icon resource");
   }
   uint img_pixels_clear[];
   uint orig_width_clear = 0, orig_height_clear = 0;
   bool image_loaded_clear = ResourceReadImage(resourceClear, img_pixels_clear, orig_width_clear, orig_height_clear);
   if (image_loaded_clear && orig_width_clear > 0 && orig_height_clear > 0) {
      ScaleImage(img_pixels_clear, (int)orig_width_clear, (int)orig_height_clear, 30, 30);
      g_scaled_clear_resource = "::ChatGPT_ClearIconScaled";
      if (ResourceCreate(g_scaled_clear_resource, img_pixels_clear, 30, 30, 0, 0, 30, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled clear icon resource created successfully");
      } else {
         Print("Failed to create scaled clear icon resource");
      }
   } else {
      Print("Failed to load clear icon resource");
   }
   uint img_pixels_history[];
   uint orig_width_history = 0, orig_height_history = 0;
   bool image_loaded_history = ResourceReadImage(resourceHistory, img_pixels_history, orig_width_history, orig_height_history);
   if (image_loaded_history && orig_width_history > 0 && orig_height_history > 0) {
      ScaleImage(img_pixels_history, (int)orig_width_history, (int)orig_height_history, 30, 30);
      g_scaled_history_resource = "::ChatGPT_HistoryIconScaled";
      if (ResourceCreate(g_scaled_history_resource, img_pixels_history, 30, 30, 0, 0, 30, COLOR_FORMAT_ARGB_NORMALIZE)) {
         Print("Scaled history icon resource created successfully");
      } else {
         Print("Failed to create scaled history icon resource");
      }
   } else {
      Print("Failed to load history icon resource");
   }
   g_sidebarWidth = sidebarExpanded ? expandedSidebarWidth : contractedSidebarWidth;
   g_mainContentX = g_dashboardX + g_sidebarWidth;
   g_dashboardWidth = g_sidebarWidth + g_mainWidth;
   g_mainHeight = g_headerHeight + 2 * g_padding + g_displayHeight + g_footerHeight;
   createRecLabel("ChatGPT_DashboardBg", g_dashboardX, g_mainY, g_dashboardWidth, g_mainHeight, clrWhite, 1, clrLightGray);
   ObjectSetInteger(0, "ChatGPT_DashboardBg", OBJPROP_ZORDER, 0);
   createRecLabel("ChatGPT_SidebarBg", g_dashboardX+2, g_mainY+2, g_sidebarWidth - 2 - 1, g_mainHeight - 2 - 2, clrGainsboro, 1, clrNONE);
   ObjectSetInteger(0, "ChatGPT_SidebarBg", OBJPROP_ZORDER, 0);
   
   //--- the rest of the functions

   return(INIT_SUCCEEDED);
}

В функции OnInit инициализируются новые элементы: задаются их новые цвета, масштабируются небольшие логотипы для свернутого состояния и инициализируется компактная конфигурация боковой панели. Сначала с помощью DarkenColor для исходных фонов добавленных кнопок задаются более темные цвета при наведении: для кнопок переключения, истории, See More и закрытия большого всплывающего окна. Затем с помощью ResourceCreate в формате ARGB создается ресурс g_scaled_image_resource с именем "::ChatGPT_HeaderImageScaled"; результат операции фиксируется в журнале.

Аналогично масштабируется логотип боковой панели из resourceImgLogo: до 81x81 для g_scaled_sidebar_resource, как и раньше, и до 30x30 для компактной версии g_scaled_sidebar_small, которая необходима на данном этапе. Создание ресурсов и результаты операций фиксируются в журнале. Аналогично создаются и новые ресурсы, которые затем используются по мере необходимости. Для наглядности мы отметили внесенные изменения. Далее обновим динамическую функцию боковой панели, добавив логику свернутого состояния и кнопку переключения.

void UpdateSidebarDynamic() {
   int total = ObjectsTotal(0, 0, -1);
   for (int j = total - 1; j >= 0; j--) {
      string name = ObjectName(0, j, 0, -1);
      if (StringFind(name, "ChatGPT_NewChatButton") == 0 || StringFind(name, "ChatGPT_ClearButton") == 0 || StringFind(name, "ChatGPT_HistoryButton") == 0 || StringFind(name, "ChatGPT_ChatLabel_") == 0 || StringFind(name, "ChatGPT_ChatBg_") == 0 || StringFind(name, "ChatGPT_SidebarLogo") == 0 || StringFind(name, "ChatGPT_NewChatIcon") == 0 || StringFind(name, "ChatGPT_NewChatLabel") == 0 || StringFind(name, "ChatGPT_ClearIcon") == 0 || StringFind(name, "ChatGPT_ClearLabel") == 0 || StringFind(name, "ChatGPT_HistoryIcon") == 0 || StringFind(name, "ChatGPT_HistoryLabel") == 0 || StringFind(name, "ChatGPT_ToggleButton") == 0) {
         ObjectDelete(0, name);
      }
   }
   ArrayResize(side_chat_bgs, 0);
   int sidebarX = g_dashboardX;
   int itemY = g_mainY + 10;
   string sidebar_logo_resource = sidebarExpanded ? ((StringLen(g_scaled_sidebar_resource) > 0) ? g_scaled_sidebar_resource : resourceImgLogo) : ((StringLen(g_scaled_sidebar_small) > 0) ? g_scaled_sidebar_small : resourceImgLogo);
   int logo_size = sidebarExpanded ? 81 : 30;
   createBitmapLabel("ChatGPT_SidebarLogo", sidebarX + (g_sidebarWidth - logo_size)/2, itemY, logo_size, logo_size, sidebar_logo_resource, clrNONE, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, "ChatGPT_SidebarLogo", OBJPROP_ZORDER, 1);
   itemY += logo_size + 10;
   createButton("ChatGPT_NewChatButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, "", clrWhite, 11, new_chat_original_bg, clrRoyalBlue);
   ObjectSetInteger(0, "ChatGPT_NewChatButton", OBJPROP_ZORDER, 1);
   string newchat_icon_resource = (StringLen(g_scaled_newchat_resource) > 0) ? g_scaled_newchat_resource : resourceNewChat;
   int iconX = sidebarExpanded ? sidebarX + 5 + 10 : sidebarX + 5 + (g_sidebarWidth - 10 - 30)/2;
   createBitmapLabel("ChatGPT_NewChatIcon", iconX, itemY + (g_buttonHeight - 30)/2, 30, 30, newchat_icon_resource, clrNONE, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, "ChatGPT_NewChatIcon", OBJPROP_ZORDER, 2);
   ObjectSetInteger(0, "ChatGPT_NewChatIcon", OBJPROP_SELECTABLE, false);
   if (sidebarExpanded) {
      createLabel("ChatGPT_NewChatLabel", sidebarX + 5 + 10 + 30 + 5, itemY + (g_buttonHeight - 20)/2, "New Chat", clrWhite, 11, "Arial", CORNER_LEFT_UPPER);
      ObjectSetInteger(0, "ChatGPT_NewChatLabel", OBJPROP_ZORDER, 2);
      ObjectSetInteger(0, "ChatGPT_NewChatLabel", OBJPROP_SELECTABLE, false);
   }
   itemY += g_buttonHeight + 5;
   createButton("ChatGPT_ClearButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, "", clrWhite, 11, clear_original_bg, clrIndianRed);
   ObjectSetInteger(0, "ChatGPT_ClearButton", OBJPROP_ZORDER, 1);
   string clear_icon_resource = (StringLen(g_scaled_clear_resource) > 0) ? g_scaled_clear_resource : resourceClear;
   iconX = sidebarExpanded ? sidebarX + 5 + 10 : sidebarX + 5 + (g_sidebarWidth - 10 - 30)/2;
   createBitmapLabel("ChatGPT_ClearIcon", iconX, itemY + (g_buttonHeight - 30)/2, 30, 30, clear_icon_resource, clrNONE, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, "ChatGPT_ClearIcon", OBJPROP_ZORDER, 2);
   ObjectSetInteger(0, "ChatGPT_ClearIcon", OBJPROP_SELECTABLE, false);
   if (sidebarExpanded) {
      createLabel("ChatGPT_ClearLabel", sidebarX + 5 + 10 + 30 + 5, itemY + (g_buttonHeight - 20)/2, "Clear", clrWhite, 11, "Arial", CORNER_LEFT_UPPER);
      ObjectSetInteger(0, "ChatGPT_ClearLabel", OBJPROP_ZORDER, 2);
      ObjectSetInteger(0, "ChatGPT_ClearLabel", OBJPROP_SELECTABLE, false);
   }
   itemY += g_buttonHeight + 10;
   createButton("ChatGPT_HistoryButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, "", clrBlack, 12, history_original_bg, clrGray);
   ObjectSetInteger(0, "ChatGPT_HistoryButton", OBJPROP_ZORDER, 1);
   string history_icon_resource = (StringLen(g_scaled_history_resource) > 0) ? g_scaled_history_resource : resourceHistory;
   iconX = sidebarExpanded ? sidebarX + 5 + 10 : sidebarX + 5 + (g_sidebarWidth - 10 - 30)/2;
   createBitmapLabel("ChatGPT_HistoryIcon", iconX, itemY + (g_buttonHeight - 30)/2, 30, 30, history_icon_resource, clrNONE, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, "ChatGPT_HistoryIcon", OBJPROP_ZORDER, 2);
   ObjectSetInteger(0, "ChatGPT_HistoryIcon", OBJPROP_SELECTABLE, false);
   if (sidebarExpanded) {
      createLabel("ChatGPT_HistoryLabel", sidebarX + 5 + 10 + 30 + 5, itemY + (g_buttonHeight - 20)/2, "History", clrBlack, 12, "Arial", CORNER_LEFT_UPPER);
      ObjectSetInteger(0, "ChatGPT_HistoryLabel", OBJPROP_ZORDER, 2);
      ObjectSetInteger(0, "ChatGPT_HistoryLabel", OBJPROP_SELECTABLE, false);
   }
   itemY += g_buttonHeight + 5;
   if (sidebarExpanded) {
      int numChats = MathMin(ArraySize(chats), 7);
      int chatIndices[7];
      for (int i = 0; i < numChats; i++) {
         chatIndices[i] = ArraySize(chats) - 1 - i;
      }
      for (int i = 0; i < numChats; i++) {
         int chatIdx = chatIndices[i];
         string hashed_id = EncodeID(chats[chatIdx].id);
         string fullText = chats[chatIdx].title + " > " + hashed_id;
         string labelText = fullText;
         if (StringLen(fullText) > 19) {
            labelText = StringSubstr(fullText, 0, 16) + "...";
         }
         string bgName = "ChatGPT_ChatBg_" + hashed_id;
         createRecLabel(bgName, sidebarX + 5 + 10, itemY, g_sidebarWidth - 10 - 10, 25, clrBeige, 1, DarkenColor(clrBeige, 0.9), BORDER_FLAT, STYLE_SOLID);
         ObjectSetInteger(0, bgName, OBJPROP_ZORDER, 1);
         color textColor = (chats[chatIdx].id == current_chat_id) ? clrBlue : clrBlack;
         createLabel("ChatGPT_ChatLabel_" + hashed_id, sidebarX + 10 + 10, itemY + 3, labelText, textColor, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
         ObjectSetInteger(0, "ChatGPT_ChatLabel_" + hashed_id, OBJPROP_ZORDER, 2);
         int size = ArraySize(side_chat_bgs);
         ArrayResize(side_chat_bgs, size + 1);
         side_chat_bgs[size] = bgName;
         itemY += 25 + 5;
      }
   }
   itemY += 10;
   string toggle_text = sidebarExpanded ? "<<" : ">>";
   createButton("ChatGPT_ToggleButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, toggle_text, clrBlack, 12, toggle_original_bg, clrGray);
   ObjectSetInteger(0, "ChatGPT_ToggleButton", OBJPROP_ZORDER, 1);
   ChartRedraw();
}

В функции UpdateSidebarDynamic внешний вид боковой панели обновляется в зависимости от ее состояния – развернутого или свернутого. Сначала удаляются существующие объекты боковой панели, включая объекты с префиксами наподобие ChatGPT_NewChatButton и кнопку переключения. Для этого используются ObjectsTotal, ObjectDelete и проверки StringFind; затем сбрасывается массив side_chat_bgs. Координате X боковой панели присваивается значение g_dashboardX, а начальной координате Y – g_mainY + 10. В развернутом состоянии для логотипа, как и прежде, используется g_scaled_sidebar_resource размером 81x81, а в свернутом – g_scaled_sidebar_small размером 30x30. Логотип создается через createBitmapLabel, центрируется выражением sidebarX + (g_sidebarWidth - logo_size)/2 и получает значение порядка наложения (ZORDER), равное 1.

Для кнопок New Chat, Clear и History вызывается createButton: ширина задается выражением g_sidebarWidth - 10, высота – g_buttonHeight, порядок наложения – 1. Используются цвета new_chat_original_bg, clear_original_bg и history_original_bg. Иконки создаются через createBitmapLabel в позиции iconX: в развернутом состоянии она рассчитывается с выравниванием по левому краю, в свернутом – по центру. Используются масштабированные ресурсы или исходные, например resourceNewChat; порядок наложения равен 2, выбор объектов отключен.

В развернутом состоянии для каждой кнопки добавляются текстовые метки New Chat, Clear и History через createLabel. Используется шрифт Arial размером 11 или 12, порядок наложения 2; выбор объектов отключен, метки размещаются справа от иконок. В развернутом состоянии, как и раньше, отображается до семи последних чатов из массива chats. Наконец, через createButton добавляется кнопка переключения ChatGPT_ToggleButton: в развернутом состоянии она показывает <<, в свернутом – >>. В следующей версии для нее будет выбран более удачный вид. Используются шрифт Arial размером 12 и порядок наложения 1; затем график перерисовывается с помощью ChartRedraw, чтобы отобразить обновленную компоновку боковой панели. Теперь, когда мы разобрались с кнопкой переключения, нужна функция, которая при необходимости пересчитывает и задает позиции и размеры всех объектов интерфейса панели, предотвращая наложения и зазоры.

void UpdateDashboardPositions() {
   ObjectSetInteger(0, "ChatGPT_DashboardBg", OBJPROP_XSIZE, g_dashboardWidth);
   ObjectSetInteger(0, "ChatGPT_SidebarBg", OBJPROP_XSIZE, g_sidebarWidth - 2 - 1);
   int diff = g_mainContentX - (g_dashboardX + (sidebarExpanded ? contractedSidebarWidth : expandedSidebarWidth));
   for (int i = 0; i < objCount; i++) {
      string obj = dashboardObjects[i];
      if (ObjectFind(0, obj) >= 0) {
         long curX = ObjectGetInteger(0, obj, OBJPROP_XDISTANCE);
         ObjectSetInteger(0, obj, OBJPROP_XDISTANCE, curX + diff);
      }
   }
   uint date_wid, date_hei;
   string dateStr = TimeToString(TimeTradeServer(), TIME_MINUTES);
   TextSetFont("Arial", 12);
   TextGetSize(dateStr, date_wid, date_hei);
   int dateX = g_mainContentX + g_mainWidth / 2 - (int)(date_wid / 2) + 20;
   int dateY = (int)ObjectGetInteger(0, "ChatGPT_DateLabel", OBJPROP_YDISTANCE);
   ObjectSetInteger(0, "ChatGPT_DateLabel", OBJPROP_XDISTANCE, dateX);
   int closeWidth = 100;
   int closeX = g_mainContentX + g_mainWidth - closeWidth - g_sidePadding;
   ObjectSetInteger(0, "ChatGPT_CloseButton", OBJPROP_XDISTANCE, closeX);
   int promptW = g_mainWidth - 2 * g_sidePadding;
   int editX = g_mainContentX + g_sidePadding + g_textPadding;
   g_editW = promptW - 2 * g_textPadding;
   ObjectSetInteger(0, "ChatGPT_PromptEdit", OBJPROP_XDISTANCE, editX);
   ObjectSetInteger(0, "ChatGPT_PromptEdit", OBJPROP_XSIZE, g_editW);
   int buttonW = 140;
   int chartX = g_mainContentX + g_sidePadding;
   int sendX = g_mainContentX + g_mainWidth - g_sidePadding - buttonW;
   ObjectSetInteger(0, "ChatGPT_GetChartButton", OBJPROP_XDISTANCE, chartX);
   ObjectSetInteger(0, "ChatGPT_SendPromptButton", OBJPROP_XDISTANCE, sendX);
   int displayX = g_mainContentX + g_sidePadding;
   int displayW = g_mainWidth - 2 * g_sidePadding;
   ObjectSetInteger(0, "ChatGPT_ResponseBg", OBJPROP_XDISTANCE, displayX);
   ObjectSetInteger(0, "ChatGPT_ResponseBg", OBJPROP_XSIZE, displayW);
   ObjectSetInteger(0, "ChatGPT_PromptBg", OBJPROP_XDISTANCE, displayX);
   ObjectSetInteger(0, "ChatGPT_PromptBg", OBJPROP_XSIZE, displayW);
   int footerY = g_mainY + g_headerHeight + g_padding + g_displayHeight + g_padding;
   ObjectSetInteger(0, "ChatGPT_FooterBg", OBJPROP_XDISTANCE, g_mainContentX);
   ObjectSetInteger(0, "ChatGPT_FooterBg", OBJPROP_XSIZE, g_mainWidth);
   if (ObjectFind(0, "ChatGPT_PromptPlaceholder") >= 0) {
      int labelY = (int)ObjectGetInteger(0, "ChatGPT_PromptPlaceholder", OBJPROP_YDISTANCE);
      ObjectSetInteger(0, "ChatGPT_PromptPlaceholder", OBJPROP_XDISTANCE, editX + 2);
   }
   if (scroll_visible) {
      int displayY = g_mainY + g_headerHeight + g_padding;
      int scrollbar_x = displayX + displayW - 16;
      int button_size = 16;
      ObjectSetInteger(0, SCROLL_LEADER, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2);
      ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2);
      ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE, scrollbar_x);
      UpdateSliderPosition();
   }
   if (p_scroll_visible) {
      int promptX = g_mainContentX + g_sidePadding;
      int promptY = footerY + g_margin;
      int promptW = g_mainWidth - 2 * g_sidePadding;
      int scrollbar_x = promptX + promptW - 16;
      int button_size = 16;
      ObjectSetInteger(0, P_SCROLL_LEADER, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, P_SCROLL_UP_REC, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, P_SCROLL_UP_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2);
      ObjectSetInteger(0, P_SCROLL_DOWN_REC, OBJPROP_XDISTANCE, scrollbar_x);
      ObjectSetInteger(0, P_SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2);
      ObjectSetInteger(0, P_SCROLL_SLIDER, OBJPROP_XDISTANCE, scrollbar_x);
      UpdatePromptSliderPosition();
   }
}

Здесь создается функция, которая смещает основное содержимое влево или вправо при изменении размера боковой панели и предотвращает перекрытия и зазоры. Она будет вызываться при переключении состояния боковой панели, чтобы переходы оставались плавными. Функция называется UpdateDashboardPositions. Сначала через ObjectSetInteger со свойством OBJPROP_XSIZE ширина фона панели ChatGPT_DashboardBg обновляется до g_dashboardWidth, а ширина фона боковой панели ChatGPT_SidebarBg – до g_sidebarWidth - 2 - 1.

Горизонтальное смещение diff вычисляется как изменение g_mainContentX относительно начала панели с учетом ширины боковой панели в противоположном состоянии: свернутого вместо развернутого и наоборот. Затем цикл обрабатывает objCount объектов массива dashboardObjects: для каждого через ObjectFind проверяется наличие, через ObjectGetInteger и OBJPROP_XDISTANCE считывается текущая координата X, после чего она смещается на diff. Важно, что существующее основное содержимое не удаляется и не создается заново – изменяется только его позиция. Поэтому интерфейс не мерцает.

Для метки даты через TimeToString получаем строку текущего серверного времени, задаем шрифт Arial размером 12 и через TextGetSize вычисляем размер текста. Затем метка центрируется в основной области с учетом g_mainContentX и g_mainWidth, после чего ей задается новая координата X. Координата X кнопки закрытия пересчитывается на основе g_mainContentX и g_mainWidth с учетом отступа и ширины кнопки. Для поля промпта обновляются ширина promptW, а также координата X поля ввода и его ширина g_editW. Новые значения применяются к полю ввода через ObjectSetInteger и OBJPROP_XSIZE. Для кнопок графика и отправки аналогично рассчитываются новые координаты X на основе g_mainContentX и g_sidePadding.

Фоны ответа и промпта перемещаются с учетом обновленной координаты X и размеров, соответствующих displayX и displayW. Фон нижней панели смещается к g_mainContentX, а его ширина задается значением g_mainWidth. Если существует заполнитель поля промпта, его координата X обновляется в соответствии с координатой поля ввода. Если основная полоса прокрутки видима, ее координата X пересчитывается как displayX + displayW - 16. Затем обновляются координаты X всех компонентов – дорожки, прямоугольников и меток кнопок вверх и вниз, ползунка, – после чего вызывается UpdateSliderPosition. Аналогично для полосы прокрутки промпта координата X вычисляется как promptX + promptW - 16, после чего корректируются все ее элементы. Вызов UpdatePromptSliderPosition сохраняет выравнивание при изменении состояния. На этом этапе все работает как задумано. Теперь нужно создать всплывающие окна. Для этого потребуются новые функции.

void ShowSmallHistoryPopup(int button_y) {
   ArrayResize(small_popup_objects, 0);
   ArrayResize(small_chat_bgs, 0);
   long history_x = ObjectGetInteger(0, "ChatGPT_HistoryButton", OBJPROP_XDISTANCE);
   long history_w = ObjectGetInteger(0, "ChatGPT_HistoryButton", OBJPROP_XSIZE);
   int popup_x = (int)(history_x + history_w);
   int popup_y = button_y;
   int popup_w = 200;
   int item_height = 25;
   int num_chats = MathMin(ArraySize(chats), 7);
   int items_h = num_chats * (item_height + 5) - 5;
   int popup_h = items_h + g_buttonHeight + 20;
   string popup_bg = "ChatGPT_SmallHistoryBg";
   createRecLabel(popup_bg, popup_x, popup_y, popup_w, popup_h, clrWhite, 1, clrLightGray);
   int size = ArraySize(small_popup_objects);
   ArrayResize(small_popup_objects, size + 1);
   small_popup_objects[size] = popup_bg;
   int item_y = popup_y + 10;
   for (int i = 0; i < num_chats; i++) {
      int chatIdx = ArraySize(chats) - 1 - i;
      string hashed_id = EncodeID(chats[chatIdx].id);
      string labelText = chats[chatIdx].title;
      if (StringLen(labelText) > 25) labelText = StringSubstr(labelText, 0, 22) + "...";
      string bgName = "ChatGPT_SmallChatBg_" + hashed_id;
      createRecLabel(bgName, popup_x + 10, item_y, popup_w - 20, item_height, clrBeige, 1, DarkenColor(clrBeige, 0.9), BORDER_FLAT, STYLE_SOLID);
      string labelName = "ChatGPT_SmallChatLabel_" + hashed_id;
      createLabel(labelName, popup_x + 20, item_y + 3, labelText, clrBlack, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
      size = ArraySize(small_popup_objects);
      ArrayResize(small_popup_objects, size + 2);
      small_popup_objects[size] = bgName;
      small_popup_objects[size + 1] = labelName;
      int bg_size = ArraySize(small_chat_bgs);
      ArrayResize(small_chat_bgs, bg_size + 1);
      small_chat_bgs[bg_size] = bgName;
      item_y += item_height + 5;
   }
   string see_more = "ChatGPT_SeeMoreButton";
   createButton(see_more, popup_x + 10, item_y, popup_w - 20, g_buttonHeight, "See All", clrWhite, 11, see_more_original_bg, clrDarkBlue);
   size = ArraySize(small_popup_objects);
   ArrayResize(small_popup_objects, size + 1);
   small_popup_objects[size] = see_more;
   small_popup_x = popup_x;
   small_popup_y = popup_y;
   small_popup_w = popup_w;
   small_popup_h = popup_h;
   showing_small_history_popup = true;
   just_opened_small = true;
}

void DeleteSmallHistoryPopup() {
   for (int i = 0; i < ArraySize(small_popup_objects); i++) {
      ObjectDelete(0, small_popup_objects[i]);
   }
   ArrayResize(small_popup_objects, 0);
   ArrayResize(small_chat_bgs, 0);
   showing_small_history_popup = false;
}

void ShowBigHistoryPopup() {
   ArrayResize(big_popup_objects, 0);
   ArrayResize(big_chat_bgs, 0);
   big_scroll_pos = 0;
   big_scroll_visible = false;
   int popup_w = g_mainWidth - 20;
   int item_height = 25;
   int num_chats = ArraySize(chats);
   big_total_height = num_chats * (item_height + 5) - 5;
   int max_h = g_displayHeight - 20;
   int content_h = max_h - 40 - 10;
   big_visible_height = content_h - 35;
   int popup_h = MathMin(big_total_height +40+20 + 10, max_h);
   big_popup_w = popup_w;
   big_popup_h = popup_h;
   big_popup_x = g_mainContentX + 10;
   big_popup_y = g_mainY + g_headerHeight + g_padding + 10;
   string popup_bg = "ChatGPT_BigHistoryBg";
   createRecLabel(popup_bg, big_popup_x, big_popup_y, popup_w, popup_h, C'250,250,250', 1, clrDodgerBlue);
   int size = ArraySize(big_popup_objects);
   ArrayResize(big_popup_objects, size + 1);
   big_popup_objects[size] = popup_bg;
   string close_button = "ChatGPT_BigCloseButton";
   createButton(close_button, big_popup_x + popup_w - 80 - 10 + 40, big_popup_y + 5, 40, 30, "r", clrRed, 13, big_close_original_bg, clrGray,"Webdings");
   size = ArraySize(big_popup_objects);
   ArrayResize(big_popup_objects, size + 1);
   big_popup_objects[size] = close_button;
   int content_y = big_popup_y + 40;
   string content_bg = "ChatGPT_BigContentBg";
   createRecLabel(content_bg, big_popup_x + 10, content_y, popup_w - 20, content_h, clrWhite, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID);
   size = ArraySize(big_popup_objects);
   ArrayResize(big_popup_objects, size + 1);
   big_popup_objects[size] = content_bg;
   bool need_big_scroll = big_total_height > big_visible_height;
   int reserved_w = need_big_scroll ? 16 : 0;
   int item_w = popup_w - 20 - 20 - reserved_w;
   UpdateBigHistoryDisplay();
   if (need_big_scroll) {
      CreateBigScrollbar();
      big_scroll_visible = true;
      UpdateBigSliderPosition();
      UpdateBigButtonColors();
   }
   showing_big_history_popup = true;
   just_opened_big = true;
   ChartRedraw();
}

void CreateBigScrollbar() {
   int scrollbar_x = big_popup_x + big_popup_w - 10 - 16;
   int scrollbar_y = big_popup_y + 40 + 16;
   int scrollbar_width = 16;
   int scrollbar_height = big_popup_h - 40 - 10 - 2 * 16;
   int button_size = 16;
   createRecLabel(BIG_SCROLL_LEADER, scrollbar_x, scrollbar_y, scrollbar_width, scrollbar_height, C'220,220,220', 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER);
   createRecLabel(BIG_SCROLL_UP_REC, scrollbar_x, big_popup_y + 40, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER);
   createLabel(BIG_SCROLL_UP_LABEL, scrollbar_x + 2, big_popup_y + 40 + -2, CharToString(0x35), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER);
   createRecLabel(BIG_SCROLL_DOWN_REC, scrollbar_x, big_popup_y + 40 + (big_popup_h - 40 - 10) - button_size, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER);
   createLabel(BIG_SCROLL_DOWN_LABEL, scrollbar_x + 2, big_popup_y + 40 + (big_popup_h - 40 - 10) - button_size + -2, CharToString(0x36), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER);
   big_slider_height = CalculateBigSliderHeight();
   createRecLabel(BIG_SCROLL_SLIDER, scrollbar_x, big_popup_y + 40 + (big_popup_h - 40 - 10) - button_size - big_slider_height, scrollbar_width, big_slider_height, clrSilver, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER);
   int size = ArraySize(big_popup_objects);
   ArrayResize(big_popup_objects, size + 6);
   big_popup_objects[size] = BIG_SCROLL_LEADER;
   big_popup_objects[size + 1] = BIG_SCROLL_UP_REC;
   big_popup_objects[size + 2] = BIG_SCROLL_UP_LABEL;
   big_popup_objects[size + 3] = BIG_SCROLL_DOWN_REC;
   big_popup_objects[size + 4] = BIG_SCROLL_DOWN_LABEL;
   big_popup_objects[size + 5] = BIG_SCROLL_SLIDER;
}

int CalculateBigSliderHeight() {
   int scroll_area_height = big_popup_h - 40 - 25 - 2 * 16;
   int slider_min_height = 20;
   if (big_total_height <= big_visible_height) return scroll_area_height;
   double visible_ratio = (double)big_visible_height / big_total_height;
   int height = (int)MathFloor(scroll_area_height * visible_ratio);
   return MathMax(slider_min_height, height);
}

void UpdateBigSliderPosition() {
   int scrollbar_x = big_popup_x + big_popup_w - 10 - 16;
   int scrollbar_y = big_popup_y + 40 + 16;
   int scroll_area_height = big_popup_h - 40 - 25 - 2 * 16;
   int max_scroll = MathMax(0, big_total_height - big_visible_height);
   if (max_scroll <= 0) return;
   double scroll_ratio = (double)big_scroll_pos / max_scroll;
   int scroll_area_y_max = scrollbar_y + scroll_area_height - big_slider_height;
   int scroll_area_y_min = scrollbar_y;
   int new_y = scroll_area_y_min + (int)(scroll_ratio * (scroll_area_y_max - scroll_area_y_min));
   new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
   ObjectSetInteger(0, BIG_SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
}

void UpdateBigButtonColors() {
   int max_scroll = MathMax(0, big_total_height - big_visible_height);
   if (big_scroll_pos == 0) {
      ObjectSetInteger(0, BIG_SCROLL_UP_LABEL, OBJPROP_COLOR, clrSilver);
   } else {
      ObjectSetInteger(0, BIG_SCROLL_UP_LABEL, OBJPROP_COLOR, clrDimGray);
   }
   if (big_scroll_pos == max_scroll) {
      ObjectSetInteger(0, BIG_SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrSilver);
   } else {
      ObjectSetInteger(0, BIG_SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrDimGray);
   }
}

void BigScrollUp() {
   if (big_scroll_pos > 0) {
      big_scroll_pos = MathMax(0, big_scroll_pos - 30);
      UpdateBigHistoryDisplay();
      if (big_scroll_visible) {
         UpdateBigSliderPosition();
         UpdateBigButtonColors();
      }
   }
}

void BigScrollDown() {
   int max_scroll = MathMax(0, big_total_height - big_visible_height);
   if (big_scroll_pos < max_scroll) {
      big_scroll_pos = MathMin(max_scroll, big_scroll_pos + 30);
      UpdateBigHistoryDisplay();
      if (big_scroll_visible) {
         UpdateBigSliderPosition();
         UpdateBigButtonColors();
      }
   }
}

void UpdateBigHistoryDisplay() {
   int total = ObjectsTotal(0, 0, -1);
   for (int j = total - 1; j >= 0; j--) {
      string name = ObjectName(0, j, 0, -1);
      if (StringFind(name, "ChatGPT_BigChatBg_") == 0 || StringFind(name, "ChatGPT_BigChatLabel_") == 0) {
         ObjectDelete(0, name);
      }
   }
   int content_y = big_popup_y + 40;
   int content_h = big_popup_h - 40 - 10;
   int item_height = 25;
   int num_chats = ArraySize(chats);
   big_total_height = num_chats * (item_height + 5) - 5;
   bool need_big_scroll = big_total_height > big_visible_height;
   int reserved_w = need_big_scroll ? 16 : 0;
   int item_w = big_popup_w - 20 - 20 - reserved_w;
   int item_y = content_y + 10 - big_scroll_pos;
   int end_y = content_y + content_h - 25;
   int start_idx = 0;
   int current_h = 0;
   for (int i = 0; i < num_chats; i++) {
      if (current_h >= big_scroll_pos) {
         start_idx = i;
         item_y = content_y + 10 + (current_h - big_scroll_pos);
         break;
      }
      current_h += item_height + 5;
   }
   int visible_count = 0;
   current_h = 0;
   for (int i = start_idx; i < num_chats; i++) {
      if (current_h + item_height > big_visible_height) break;
      current_h += item_height + 5;
      visible_count++;
   }
   for (int i = 0; i < visible_count; i++) {
      int chatIdx = num_chats - 1 - (start_idx + i);
      string hashed_id = EncodeID(chats[chatIdx].id);
      string fullText = chats[chatIdx].title + " > " + hashed_id;
      string labelText = fullText;
      if (StringLen(fullText) > 35) {
         labelText = StringSubstr(fullText, 0, 32) + "...";
      }
      string bgName = "ChatGPT_BigChatBg_" + hashed_id;
      if (item_y >= content_y + 10 && item_y < end_y) {
         createRecLabel(bgName, big_popup_x + 20, item_y, item_w, item_height, clrBeige, 1, DarkenColor(clrBeige, 0.9), BORDER_FLAT, STYLE_SOLID);
      }
      string labelName = "ChatGPT_BigChatLabel_" + hashed_id;
      if (item_y >= content_y + 10 && item_y < end_y) {
         createLabel(labelName, big_popup_x + 30, item_y + 3, labelText, clrBlack, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
      }
      int size = ArraySize(big_popup_objects);
      ArrayResize(big_popup_objects, size + 2);
      big_popup_objects[size] = bgName;
      big_popup_objects[size + 1] = labelName;
      int bg_size = ArraySize(big_chat_bgs);
      ArrayResize(big_chat_bgs, bg_size + 1);
      big_chat_bgs[bg_size] = bgName;
      item_y += item_height + 5;
   }
   ChartRedraw();
}

void DeleteBigHistoryPopup() {
   for (int i = 0; i < ArraySize(big_popup_objects); i++) {
      ObjectDelete(0, big_popup_objects[i]);
   }
   ArrayResize(big_popup_objects, 0);
   ArrayResize(big_chat_bgs, 0);
   showing_big_history_popup = false;
   big_scroll_visible = false;
}

Функция ShowSmallHistoryPopup выводит компактное всплывающее окно с недавними чатами при наведении на кнопку истории в свернутом состоянии боковой панели. Перед этим сбрасываются массивы small_popup_objects и small_chat_bgs. Позиция всплывающего окна справа от кнопки истории вычисляется через ObjectGetInteger по ее координате X и ширине. Ширина окна задается равной 200 пикселям, высота элемента – 25 пикселям, а общая высота рассчитывается динамически по семи последним чатам и кнопке See All. Для фона ChatGPT_SmallHistoryBg через createRecLabel задаются белая заливка и светло-серая рамка. Затем цикл обрабатывает недавние чаты, начиная с последнего: через createRecLabel и createLabel создаются фоны ChatGPT_SmallChatBg_ и метки ChatGPT_SmallChatLabel_; при необходимости заголовки сокращаются до 22 символов с многоточием. Имена сохраняются в массивах для последующего удаления, а внизу через createButton добавляется кнопка See More цвета royal blue.

Функция DeleteSmallHistoryPopup закрывает малое всплывающее окно: через ObjectDelete удаляются все объекты из small_popup_objects, сбрасываются массивы, а showing_small_history_popup получает значение false. Функция ShowBigHistoryPopup выводит большое прокручиваемое всплывающее окно истории. Сбрасываются big_popup_objects и big_chat_bgs, переменной big_scroll_pos присваивается значение 0, а видимость полосы прокрутки отключается. Ширина всплывающего окна вычисляется как g_mainWidth - 20. Высота ограничивается максимальным значением, зависящим от количества чатов, высоты элемента 25 пикселей и отступов; окно размещается с отступом от границ основной области. Для фона ChatGPT_BigHistoryBg задаются светло-серый цвет, а для рамки цвет dodger blue. Через createButton добавляется кнопка закрытия ChatGPT_BigCloseButton с красным символом r шрифта Webdings. Для фона содержимого ChatGPT_BigContentBg задаются белая заливка и рамка gainsboro. Сопоставление big_total_height и big_visible_height определяет необходимость прокрутки; функция UpdateBigHistoryDisplay заполняет список чатов. Если прокрутка нужна, вызывается CreateBigScrollbar, включается видимость полосы и обновляются ее позиция и цвета. Переменным showing_big_history_popup и just_opened_big присваивается значение true, после чего график перерисовывается через ChartRedraw.

Функция CreateBigScrollbar создает полосу прокрутки большого всплывающего окна по образцу остальных. Через createRecLabel создаются дорожка, прямоугольники кнопок вверх и вниз цвета gainsboro, метки со стрелками Webdings цвета dim gray, а также серебристый ползунок с рамкой gainsboro. Позиции вычисляются по big_popup_x, big_popup_y, big_popup_w и big_popup_h, а имена объектов добавляются в big_popup_objects для дальнейшего управления. Функция CalculateBigSliderHeight вычисляет высоту большого ползунка пропорционально отношению видимой высоты к общей. Минимальная высота составляет 20 пикселей; если прокрутка не требуется, возвращается высота всей области. Функция UpdateBigSliderPosition корректирует координату Y большого ползунка по отношению big_scroll_pos к максимальной прокрутке. Значение ограничивается пределами области прокрутки через MathMax и MathMin, затем задается с помощью ObjectSetInteger. Функция UpdateBigButtonColors окрашивает недоступную стрелку вверх или вниз в серебристый цвет, если достигнут соответствующий предел прокрутки, и в цвет dim gray, если прокрутка в этом направлении доступна. Функции BigScrollUp и BigScrollDown уменьшают или увеличивают big_scroll_pos на 30, ограничивая значение диапазоном от 0 до максимума. Затем через UpdateBigHistoryDisplay обновляется список, а если полоса прокрутки видима, через UpdateBigSliderPosition и UpdateBigButtonColors обновляются положение ползунка и цвета кнопок.

В UpdateBigHistoryDisplay через ObjectsTotal и ObjectDelete удаляются существующие фоны и метки чатов большого окна. Нужные объекты определяются с помощью StringFind. На основе количества чатов пересчитывается big_total_height, определяется необходимость прокрутки и резервируемая под нее ширина, после чего задается ширина элемента. По big_scroll_pos рассчитываются начальный индекс и координата Y, а по big_visible_height – число видимых элементов. Затем в цикле для видимых чатов, начиная с последних, создаются фоны ChatGPT_BigChatBg_ и метки ChatGPT_BigChatLabel_; заголовки при необходимости сокращаются до 32 символов с многоточием. Имена объектов сохраняются в массивах, объекты размещаются только в пределах видимой области, затем вызывается ChartRedraw. Функция DeleteBigHistoryPopup через ObjectDelete удаляет все объекты из big_popup_objects, сбрасывает массивы и присваивает showing_big_history_popup и big_scroll_visible значение false. Далее эти функции нужно вызывать в обработчике событий графика там, где это необходимо.

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {

//--- most relevant events

   if (id == CHARTEVENT_CLICK) {
      int clickX = (int)lparam;
      int clickY = (int)dparam;
      if (showing_big_history_popup) {
         if (just_opened_big) {
            just_opened_big = false;
         } else if (clickX < big_popup_x || clickX > big_popup_x + big_popup_w || clickY < big_popup_y || clickY > big_popup_y + big_popup_h) {
            DeleteBigHistoryPopup();
            ChartRedraw();
         }
      }
      if (showing_small_history_popup) {
         if (just_opened_small) {
            just_opened_small = false;
         } else if (clickX < small_popup_x || clickX > small_popup_x + small_popup_w || clickY < small_popup_y || clickY > small_popup_y + small_popup_h) {
            DeleteSmallHistoryPopup();
            ChartRedraw();
         }
      }
      return;
   }

   else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_ToggleButton") {
      sidebarExpanded = !sidebarExpanded;
      g_sidebarWidth = sidebarExpanded ? expandedSidebarWidth : contractedSidebarWidth;
      g_mainContentX = g_dashboardX + g_sidebarWidth;
      g_dashboardWidth = g_sidebarWidth + g_mainWidth;
      if (showing_big_history_popup) DeleteBigHistoryPopup();
      if (showing_small_history_popup) DeleteSmallHistoryPopup();
      UpdateSidebarDynamic();
      UpdateDashboardPositions();
      UpdateResponseDisplay();
      UpdatePromptDisplay();
      ChartRedraw();
   } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_HistoryButton" && sidebarExpanded) {
      ShowBigHistoryPopup();
      just_opened_big = true;
   } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SeeMoreButton") {
      DeleteSmallHistoryPopup();
      ShowBigHistoryPopup();
      just_opened_big = true;
   } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_BigCloseButton") {
      DeleteBigHistoryPopup();
      ChartRedraw();
   }

   else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == BIG_SCROLL_UP_REC || sparam == BIG_SCROLL_UP_LABEL)) {
      BigScrollUp();
   } else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == BIG_SCROLL_DOWN_REC || sparam == BIG_SCROLL_DOWN_LABEL)) {
      BigScrollDown();
   } 

//--- the rest of the hover and mouse move logic implementation for new elements is the same

}

Обработчик событий OnChartEvent используется для обработки действий пользователя со сворачиваемой боковой панелью и всплывающими окнами истории. Основное внимание уделено кликам вне окон и кликам по отдельным объектам. При обычном клике CHARTEVENT_CLICK сохраняются координаты clickX и clickY. Если большое всплывающее окно отображается, но не было только что открыто – это проверяется по just_opened_big, чтобы избежать случайного немедленного закрытия, – определяется, находится ли клик за его пределами. В таком случае вызывается DeleteBigHistoryPopup, а затем ChartRedraw. Для малого всплывающего окна аналогично используются just_opened_small и DeleteSmallHistoryPopup. Это важно: при клике вне открытых всплывающих окон они должны закрываться. Можно было бы счесть отдельный обработчик события клика вне окна изящным решением, но на практике в этом нет необходимости.

При клике по объекту CHARTEVENT_OBJECT_CLICK на кнопке переключения ChatGPT_ToggleButton меняется значение sidebarExpanded. Затем g_sidebarWidth пересчитывается для развернутого или свернутого состояния, обновляются g_mainContentX и g_dashboardWidth, а открытые всплывающие окна закрываются через DeleteBigHistoryPopup и DeleteSmallHistoryPopup. Интерфейс обновляется вызовами UpdateSidebarDynamic, UpdateDashboardPositions, UpdateResponseDisplay, UpdatePromptDisplay и ChartRedraw. При клике по кнопке истории ChatGPT_HistoryButton, если боковая панель развернута, вызывается ShowBigHistoryPopup, а just_opened_big получает значение true.

При клике по кнопке See More с именем ChatGPT_SeeMoreButton малое окно закрывается через DeleteSmallHistoryPopup, открывается большое окно, а just_opened_big получает значение true. При клике по кнопке закрытия большого окна ChatGPT_BigCloseButton вызывается DeleteBigHistoryPopup, после чего выполняется перерисовка. При клике по элементам прокрутки вверх большого окна – BIG_SCROLL_UP_REC или соответствующей метке – вызывается BigScrollUp. Для элементов прокрутки вниз аналогично вызывается BigScrollDown. Остальная логика эффектов наведения и перемещения мыши для новых элементов повторяет предыдущую реализацию, обеспечивая единообразное взаимодействие с интерфейсом. После компиляции получаем следующий результат.

UPDATED SIDEBAR WITH SMALL POPUP

Пока всплывающие окна открыты, основная область не должна обновляться. Для этого в функцию обновления области ответа добавляется условие, которое предотвращает лишние перерисовки и конфликты, когда всплывающее окно перекрывает основную область. Это повышает стабильность и уменьшает мерцание при работе с окнами.

void UpdateResponseDisplay() {
   if (showing_small_history_popup || showing_big_history_popup) return;

   //--- rest of the function logic

}

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

BIG CHARTS POPUP WITH HOVER EFFECTS & SCROLLBAR

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


Тестирование сворачиваемой боковой панели

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

BACKTEST GIF


Заключение

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


Приложения

ИмяТипОписание
1AI_JSON_FILE.mqh
Библиотека классов JSON
Класс для сериализации и десериализации JSON
2AI_CREATE_OBJECTS_FNS.mqh
Библиотека функций объектов
Функции для создания объектов визуализации, таких как метки и кнопки
3AI_ChatGPT_EA_Part_5.mq5
Советник
Основной советник для интеграции ИИ

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

Прикрепленные файлы |
AI_JSON_FILE.mqh (26.62 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
AMIRREZA REZAZAD
AMIRREZA REZAZAD | 17 нояб. 2025 в 04:33
Спасибо за создание этого всплывающего чата с ИИ. Если бы в него можно было загружать изображения графиков (например, графики с полезными данными, такими как индикатор ценового движения, активированный на графике), это улучшило бы анализ рынка с помощью ИИ и было бы очень полезно. Если это возможно, пожалуйста, расскажите, как это сделать.
Allan Munene Mutiiria
Allan Munene Mutiiria | 17 нояб. 2025 в 07:14
AMIRREZA REZAZAD # : Спасибо за создание этого всплывающего чата с ИИ. Если бы в него можно было загружать изображения графиков (например, графики с полезными данными, такими как индикатор ценового движения, активный на графике), это улучшило бы анализ рынка с помощью ИИ и было бы очень полезно. Если это возможно, пожалуйста, расскажите, как это сделать.
Здравствуйте. Спасибо за отзыв.
Автоматизация торговых стратегий в MQL5 (Часть 50): Стратегия Turtle Soup на снятии ликвидности Автоматизация торговых стратегий в MQL5 (Часть 50): Стратегия Turtle Soup на снятии ликвидности
В статье создаётся автоматическая программа MQL5 для торговли по стратегии Turtle Soup против ложных пробоев N-барных максимумов и минимумов. Реализуются обнаружение снятия ликвидности, подтверждающие закрытия обратно внутрь уровня, фильтры глубины и возраста экстремума, необязательная проверка тела разворотной свечи, динамические и статические стоп-лоссы, два режима тейк-профита, трейлинг по пунктам и наглядная разметка графика.
От начального до среднего уровня: Классы (I) От начального до среднего уровня: Классы (I)
В этой статье мы рассказываем, что такое класс и почему возникла эта концепция. Хотя тема интересна, здесь мы сосредоточимся на принципах, лежащих в основе программирования на MQL5. Эта статья — лишь введение.
Нейросети в трейдинге: Долговременная память торговых сценариев (Объект верхнего уровня) Нейросети в трейдинге: Долговременная память торговых сценариев (Объект верхнего уровня)
В статье показано, как двухуровневая RAG-память включается в процесс выбора торгового действия через объект CNeuronVLADriverRAGMPI. Механизм сопоставляет текущие предпочтения Актёра с историческими действиями и их результативностью, ограничивает влияние слабых аналогий с помощью Null Scenario и формирует остаточную контекстную поправку. Такой подход позволяет учитывать положительный и отрицательный опыт, сохраняя приоритет текущего состояния рынка.
Автоматизация торговых стратегий в MQL5 (Часть 49): Разворотный паттерн "Квазимодо" (QM) Автоматизация торговых стратегий в MQL5 (Часть 49): Разворотный паттерн "Квазимодо" (QM)
В статье мы создаём автоматическую программу MQL5, которая обнаруживает разворотный паттерн Квазимодо по зигзагу подтверждённых свинговых точек. Рассмотрим поиск свингов, активацию сетапа, вход на возврате к линии QM, структурный Stop Loss и расчёт объёма по риску. Также добавим перенос в безубыток, трейлинг-стоп и частичная фиксация прибыли для сопровождения открытых позиций.