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

Реализация на MQL5
Для реализации обновлений сначала определим объекты полосы прокрутки, добавляемые в окно поиска. Это будет соответствовать принятому у нас подходу: как и раньше, нужные элементы мы сначала объявим через #define. Для остальных объектов, включая кнопки удаления и строки поиска, будет использоваться стандартная конкатенация префиксов в именах объектов, что упростит их удаление.
//+------------------------------------------------------------------+ //| AI ChatGPT EA Part 6.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #property icon "1. Forex Algo-Trader.ico" #define SEARCH_SCROLL_LEADER "ChatGPT_Search_Scroll_Leader" #define SEARCH_SCROLL_UP_REC "ChatGPT_Search_Scroll_Up_Rec" #define SEARCH_SCROLL_UP_LABEL "ChatGPT_Search_Scroll_Up_Label" #define SEARCH_SCROLL_DOWN_REC "ChatGPT_Search_Scroll_Down_Rec" #define SEARCH_SCROLL_DOWN_LABEL "ChatGPT_Search_Scroll_Down_Label" #define SEARCH_SCROLL_SLIDER "ChatGPT_Search_Scroll_Slider"
С помощью #define определяются константы для элементов полосы прокрутки окна поиска: SEARCH_SCROLL_LEADER для дорожки полосы прокрутки, SEARCH_SCROLL_UP_REC и SEARCH_SCROLL_UP_LABEL для кнопки прокрутки вверх и ее стрелки, SEARCH_SCROLL_DOWN_REC и SEARCH_SCROLL_DOWN_LABEL для кнопки прокрутки вниз, а также SEARCH_SCROLL_SLIDER для перетаскиваемого ползунка. Это обеспечит единообразное обращение к объектам в функциях их создания и управления прокруткой в результатах поиска. Далее добавим переменные цветов и наведения для элементов удаления и поиска.
color search_original_bg = clrLightSlateGray; color search_darker_bg; bool search_hover = false; color search_close_original_bg = clrLightGray; color search_close_darker_bg; bool search_close_hover = false; color delete_original_bg = clrBeige; color delete_darker_bg; bool showing_search_popup = false; int search_popup_x, search_popup_y, search_popup_w, search_popup_h; string search_popup_objects[]; string search_chat_bgs[]; string search_delete_btns[]; int search_scroll_pos = 0; bool search_scroll_visible = false; int search_total_height = 0; int search_visible_height = 0; int search_slider_height = 20; bool search_movingStateSlider = false; int search_mlbDownX_Slider = 0; int search_mlbDownY_Slider = 0; int search_mlbDown_YD_Slider = 0; bool just_opened_search = false; string current_search_query = "";
Здесь определяются дополнительные глобальные переменные для поддержки поиска и удаления в интерфейсе с ИИ. Для кнопки поиска задаются цвета search_original_bg (с цветом light slate gray) и search_darker_bg для состояния наведения, а флаг search_hover отслеживает взаимодействие с мышью и обеспечивает динамическое изменение цвета. Аналогично задаются цвета кнопки закрытия окна поиска: search_close_original_bg (с цветом light gray) и search_close_darker_bg, а флаг search_close_hover отслеживает наведение. Для кнопок удаления определяются delete_original_bg (с цветом beige) и delete_darker_bg. Это поддерживает единый стиль окон и боковой панели.
Для окна поиска используются флаг видимости showing_search_popup, координаты search_popup_x, search_popup_y, search_popup_w и search_popup_h, а также массивы search_popup_objects, search_chat_bgs, search_delete_btns. Они нужны для управления объектами окна (фонами, чатами и кнопками) и их удаления. Для поддержки прокрутки добавляются search_scroll_pos, флаг search_scroll_visible, параметры высоты search_total_height, search_visible_height и search_slider_height со значением 20. Также определяются состояние перетаскивания ползунка search_movingStateSlider и координаты нажатия кнопки мыши search_mlbDownX_Slider, search_mlbDownY_Slider, search_mlbDown_YD_Slider.
Флаг just_opened_search предотвращает немедленное закрытие окна при открывающем клике, а current_search_query хранит введенный пользователем запрос для фильтрации в реальном времени. Это позволяет выполнять динамический поиск, удалять чаты в разных представлениях – на боковой панели, в малом и большом окнах – и улучшает управление чатами, не загромождая основной интерфейс. Чтобы задать цвета новых элементов, добавим в обработчик OnInit следующий код.
int OnInit() { //--- added init logic search_darker_bg = DarkenColor(search_original_bg); search_close_darker_bg = DarkenColor(search_close_original_bg); delete_darker_bg = DarkenColor(delete_original_bg); //--- the rest remain the same return(INIT_SUCCEEDED); }
В обработчик OnInit добавляется инициализация более темных цветов при наведении для поиска и удаления. search_darker_bg получается затемнением search_original_bg через DarkenColor; аналогично из search_close_original_bg создается search_close_darker_bg, а из delete_original_bg – delete_darker_bg. Это обеспечивает визуальную реакцию на действия мыши. Остальная часть функции не меняется по сравнению с предыдущими версиями: она открывает файл журнала, масштабирует изображение, настраивает боковую панель, создает панель управления и включает события. Затем функция возвращает INIT_SUCCEEDED, подтверждая успешную инициализацию. Прежде чем добавлять кнопки удаления, определим логику удаления чатов.
void DeleteChat(int id) { int idx = GetChatIndex(id); if (idx < 0) return; if (current_chat_id == id) { if (ArraySize(chats) > 1) { int new_idx = (idx == ArraySize(chats) - 1) ? idx - 1 : ArraySize(chats) - 1; current_chat_id = chats[new_idx].id; current_title = chats[new_idx].title; conversationHistory = chats[new_idx].history; } else { CreateNewChat(); return; } } ArrayRemove(chats, idx, 1); SaveChats(); long history_y = ObjectGetInteger(0, "ChatGPT_HistoryButton", OBJPROP_YDISTANCE); if (showing_small_history_popup) { DeleteSmallHistoryPopup(); if (!sidebarExpanded && ArraySize(chats) > 0) { ShowSmallHistoryPopup((int)history_y); } } if (showing_big_history_popup) { DeleteBigHistoryPopup(); if (ArraySize(chats) > 0) { ShowBigHistoryPopup(); } } if (showing_search_popup) { DeleteSearchPopup(); if (ArraySize(chats) > 0) { ShowSearchPopup(); } } UpdateSidebarDynamic(); UpdateResponseDisplay(); UpdatePromptDisplay(); ChartRedraw(); }
Далее реализуем функцию DeleteChat, которая удаляет чат по ID из хранилища и интерфейса. Активный чат при этом обрабатывается корректно, без сбоев. Сначала через GetChatIndex находим индекс чата. Если чат не найден, функция сразу завершает работу. Если удаляется текущий чат и есть другие чаты, выбирается новый: для последнего чата берется предыдущий индекс, в остальных случаях – последний. Затем обновляются current_chat_id, current_title и conversationHistory. Если чатов не осталось, вызывается CreateNewChat, после чего функция завершает работу.
Затем чат удаляется из массива chats через ArrayRemove по индексу с количеством элементов 1, а изменения сохраняются через SaveChats. Если открыто малое всплывающее окно истории, оно удаляется через DeleteSmallHistoryPopup и выводится заново, когда боковая панель свернута и чаты еще есть; координата Y кнопки истории получается через ObjectGetInteger. Большое окно истории и окно поиска обновляются аналогично: удаляются и выводятся заново, если чаты существуют. В конце через UpdateSidebarDynamic обновляется боковая панель, обновляются области ответа и запроса, а график перерисовывается функцией ChartRedraw. Далее доработаем функцию динамической боковой панели, добавив создание кнопок удаления.
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_SearchButton") == 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_SearchLabel") == 0 || StringFind(name, "ChatGPT_SearchIcon") == 0 || StringFind(name, "ChatGPT_ToggleButton") == 0 || StringFind(name, "ChatGPT_SideDelete_") == 0) { ObjectDelete(0, name); } } ArrayResize(side_chat_bgs, 0); ArrayResize(side_delete_btns, 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 + 5; createButton("ChatGPT_SearchButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, "", clrWhite, 11, search_original_bg, clrDarkSlateGray); ObjectSetInteger(0, "ChatGPT_SearchButton", OBJPROP_ZORDER, 1); int iconX = sidebarExpanded ? sidebarX + 5 + 10 : sidebarX + (g_sidebarWidth - 20)/2-6; createLabel("ChatGPT_SearchIcon", iconX, itemY + (g_buttonHeight - 20)/2-6, "L", clrWhite, 24, "Webdings", CORNER_LEFT_UPPER); ObjectSetInteger(0, "ChatGPT_SearchIcon", OBJPROP_ZORDER, 2); ObjectSetInteger(0, "ChatGPT_SearchIcon", OBJPROP_SELECTABLE, false); if (sidebarExpanded) { createLabel("ChatGPT_SearchLabel", sidebarX + 5 + 10 + 20 + 5, itemY + (g_buttonHeight - 20)/2, "Search", clrWhite, 11, "Arial", CORNER_LEFT_UPPER); ObjectSetInteger(0, "ChatGPT_SearchLabel", OBJPROP_ZORDER, 2); ObjectSetInteger(0, "ChatGPT_SearchLabel", OBJPROP_SELECTABLE, false); } itemY += g_buttonHeight + 5; 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; 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 + 5; 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); string deleteName = "ChatGPT_SideDelete_" + hashed_id; createButton(deleteName, sidebarX + g_sidebarWidth - 30, itemY + 3, 0, 20, "V", clrBeige, 15, clrBeige, clrBeige, "Wingdings 2"); ObjectSetInteger(0, deleteName, OBJPROP_ZORDER, 2); int size = ArraySize(side_chat_bgs); ArrayResize(side_chat_bgs, size + 1); side_chat_bgs[size] = bgName; int del_size = ArraySize(side_delete_btns); ArrayResize(side_delete_btns, del_size + 1); side_delete_btns[del_size] = deleteName; itemY += 25 + 3; } } itemY += 5; string toggle_text = sidebarExpanded ? "9" : ":"; createButton("ChatGPT_ToggleButton", sidebarX + 5, itemY, g_sidebarWidth - 10, g_buttonHeight, toggle_text, clrBlack, 15, toggle_original_bg, clrGray,"Webdings"); ObjectSetInteger(0, "ChatGPT_ToggleButton", OBJPROP_ZORDER, 1); ChartRedraw(); }
Здесь добавляется кнопка поиска как первая кнопка боковой панели; в свернутом состоянии она отображается только значком. Значки удаления появляются при наведении. Для удобства мы отдельно выделили внесенные изменения. В результате получаем следующее.

Кнопки удаления уже добавлены в развернутое состояние боковой панели, поэтому добавим их и в малое всплывающее окно для свернутого состояния. Их нужно добавить к каждому отображаемому чату.
void ShowSmallHistoryPopup(int button_y) { ArrayResize(small_popup_objects, 0); ArrayResize(small_chat_bgs, 0); ArrayResize(small_delete_btns, 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); string deleteName = "ChatGPT_SmallDelete_" + hashed_id; createButton(deleteName, popup_x + popup_w - 20 - 15, item_y + 3, 0, 20, "V", clrBeige, 15, clrBeige, clrBeige, "Wingdings 2"); size = ArraySize(small_popup_objects); ArrayResize(small_popup_objects, size + 3); small_popup_objects[size] = bgName; small_popup_objects[size + 1] = labelName; small_popup_objects[size + 2] = deleteName; int bg_size = ArraySize(small_chat_bgs); ArrayResize(small_chat_bgs, bg_size + 1); small_chat_bgs[bg_size] = bgName; int del_size = ArraySize(small_delete_btns); ArrayResize(small_delete_btns, del_size + 1); small_delete_btns[del_size] = deleteName; 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); ArrayResize(small_delete_btns, 0); showing_small_history_popup = false; }
Логика здесь достаточно проста и не требует подробных пояснений. Добавляем кнопки удаления к каждому чату, а при закрытии всплывающего окна удаляем созданные объекты. То же самое делаем для большого всплывающего окна. Для создания окна поиска понадобятся новые функции, но логика будет похожа на логику большого окна истории чатов. Ниже приведена необходимая логика.
void ShowSearchPopup() { current_search_query = ""; ArrayResize(search_popup_objects, 0); ArrayResize(search_chat_bgs, 0); ArrayResize(search_delete_btns, 0); search_scroll_pos = 0; search_scroll_visible = false; int popup_w = g_mainWidth - 20; int item_height = 25; int num_chats = ArraySize(chats); search_total_height = num_chats * (item_height + 5) - 5; int max_h = g_displayHeight - 20; int content_h = max_h - 40 - 40 - 10; // extra for search header search_visible_height = content_h - 35; int popup_h = max_h; search_popup_w = popup_w; search_popup_h = popup_h; search_popup_x = g_mainContentX + 10; search_popup_y = g_mainY + g_headerHeight + g_padding + 10; string popup_bg = "ChatGPT_SearchBg"; createRecLabel(popup_bg, search_popup_x, search_popup_y, popup_w, popup_h, C'250,250,250', 1, clrDodgerBlue); int size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 1); search_popup_objects[size] = popup_bg; string close_button = "ChatGPT_SearchCloseButton"; createButton(close_button, search_popup_x + popup_w -40 -10, search_popup_y + 5, 40, 30, "r", clrRed, 13, search_close_original_bg, clrGray,"Webdings"); size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 1); search_popup_objects[size] = close_button; string search_edit = "ChatGPT_SearchInput"; createEdit(search_edit, search_popup_x + 10, search_popup_y + 5, popup_w - 20 - 40 - 10, 30, "", clrBlack, 16, clrGainsboro, clrLightGray, "Calibri"); ObjectSetInteger(0, search_edit, OBJPROP_BORDER_TYPE, BORDER_FLAT); size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 1); search_popup_objects[size] = search_edit; string search_placeholder = "ChatGPT_SearchPlaceholder"; int lineHeight = TextGetHeight("A", "Arial", 11); int labelY = search_popup_y + 5 + (30 - lineHeight) / 2 - 3; createLabel(search_placeholder, search_popup_x + 10 + 2, labelY, "Search Chats", clrGray, 11, "Arial", CORNER_LEFT_UPPER); size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 1); search_popup_objects[size] = search_placeholder; int content_y = search_popup_y + 40 + 40; string content_bg = "ChatGPT_SearchContentBg"; createRecLabel(content_bg, search_popup_x + 10, content_y, popup_w - 20, content_h, clrWhite, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID); size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 1); search_popup_objects[size] = content_bg; bool need_search_scroll = search_total_height > search_visible_height; int reserved_w = need_search_scroll ? 16 : 0; int item_w = popup_w - 20 - 20 - reserved_w; UpdateSearchDisplay(); if (need_search_scroll) { CreateSearchScrollbar(); search_scroll_visible = true; UpdateSearchSliderPosition(); UpdateSearchButtonColors(); } showing_search_popup = true; just_opened_search = true; ChartRedraw(); } void CreateSearchScrollbar() { int scrollbar_x = search_popup_x + search_popup_w - 10 - 16; int scrollbar_y = search_popup_y + 40 + 40 + 16; int scrollbar_width = 16; int scrollbar_height = search_popup_h - 40 - 40 - 10 - 2 * 16; int button_size = 16; createRecLabel(SEARCH_SCROLL_LEADER, scrollbar_x, scrollbar_y, scrollbar_width, scrollbar_height, C'220,220,220', 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); createRecLabel(SEARCH_SCROLL_UP_REC, scrollbar_x, search_popup_y + 40 + 40, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); createLabel(SEARCH_SCROLL_UP_LABEL, scrollbar_x + 2, search_popup_y + 40 + 40 + -2, CharToString(0x35), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER); createRecLabel(SEARCH_SCROLL_DOWN_REC, scrollbar_x, search_popup_y + 40 + 40 + (search_popup_h - 40 - 40 - 10) - button_size, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); createLabel(SEARCH_SCROLL_DOWN_LABEL, scrollbar_x + 2, search_popup_y + 40 + 40 + (search_popup_h - 40 - 40 - 10) - button_size + -2, CharToString(0x36), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER); search_slider_height = CalculateSearchSliderHeight(); createRecLabel(SEARCH_SCROLL_SLIDER, scrollbar_x, search_popup_y + 40 + 40 + (search_popup_h - 40 - 40 - 10) - button_size - search_slider_height, scrollbar_width, search_slider_height, clrSilver, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); int size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 6); search_popup_objects[size] = SEARCH_SCROLL_LEADER; search_popup_objects[size + 1] = SEARCH_SCROLL_UP_REC; search_popup_objects[size + 2] = SEARCH_SCROLL_UP_LABEL; search_popup_objects[size + 3] = SEARCH_SCROLL_DOWN_REC; search_popup_objects[size + 4] = SEARCH_SCROLL_DOWN_LABEL; search_popup_objects[size + 5] = SEARCH_SCROLL_SLIDER; } int CalculateSearchSliderHeight() { int scroll_area_height = search_popup_h - 40 - 40 - 25 - 2 * 16; int slider_min_height = 20; if (search_total_height <= search_visible_height) return scroll_area_height; double visible_ratio = (double)search_visible_height / search_total_height; int height = (int)MathFloor(scroll_area_height * visible_ratio); return MathMax(slider_min_height, height); } void UpdateSearchSliderPosition() { int scrollbar_x = search_popup_x + search_popup_w - 10 - 16; int scrollbar_y = search_popup_y + 40 + 40 + 16; int scroll_area_height = search_popup_h - 40 - 40 - 25 - 2 * 16; int max_scroll = MathMax(0, search_total_height - search_visible_height); if (max_scroll <= 0) return; double scroll_ratio = (double)search_scroll_pos / max_scroll; int scroll_area_y_max = scrollbar_y + scroll_area_height - search_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, SEARCH_SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); } void UpdateSearchButtonColors() { int max_scroll = MathMax(0, search_total_height - search_visible_height); if (search_scroll_pos == 0) { ObjectSetInteger(0, SEARCH_SCROLL_UP_LABEL, OBJPROP_COLOR, clrSilver); } else { ObjectSetInteger(0, SEARCH_SCROLL_UP_LABEL, OBJPROP_COLOR, clrDimGray); } if (search_scroll_pos == max_scroll) { ObjectSetInteger(0, SEARCH_SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrSilver); } else { ObjectSetInteger(0, SEARCH_SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrDimGray); } } void SearchScrollUp() { if (search_scroll_pos > 0) { search_scroll_pos = MathMax(0, search_scroll_pos - 30); UpdateSearchDisplay(); if (search_scroll_visible) { UpdateSearchSliderPosition(); UpdateSearchButtonColors(); } } } void SearchScrollDown() { int max_scroll = MathMax(0, search_total_height - search_visible_height); if (search_scroll_pos < max_scroll) { search_scroll_pos = MathMin(max_scroll, search_scroll_pos + 30); UpdateSearchDisplay(); if (search_scroll_visible) { UpdateSearchSliderPosition(); UpdateSearchButtonColors(); } } } void UpdateSearchDisplay() { 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_SearchChatBg_") == 0 || StringFind(name, "ChatGPT_SearchChatLabel_") == 0 || StringFind(name, "ChatGPT_SearchDelete_") == 0) { ObjectDelete(0, name); } } ArrayResize(search_chat_bgs, 0); ArrayResize(search_delete_btns, 0); Chat filtered[]; ArrayResize(filtered, 0); for (int i = 0; i < ArraySize(chats); i++) { string lower_title = chats[i].title; StringToLower(lower_title); string lower_history = chats[i].history; StringToLower(lower_history); string lower_query = current_search_query; StringToLower(lower_query); if (StringFind(lower_title, lower_query) >= 0 || StringFind(lower_history, lower_query) >= 0 || lower_query == "") { int fsize = ArraySize(filtered); ArrayResize(filtered, fsize + 1); filtered[fsize] = chats[i]; } } int num_chats = ArraySize(filtered); search_total_height = num_chats * (25 + 5) - 5; bool need_search_scroll = search_total_height > search_visible_height; bool prev_search_scroll_visible = search_scroll_visible; search_scroll_visible = need_search_scroll; if (search_scroll_visible != prev_search_scroll_visible) { if (search_scroll_visible) { CreateSearchScrollbar(); } else { DeleteSearchScrollbar(); } } int reserved_w = search_scroll_visible ? 16 : 0; int item_w = search_popup_w - 20 - 20 - reserved_w; int item_y = search_popup_y + 40 + 40 + 10 - search_scroll_pos; int end_y = search_popup_y + 40 + 40 + (search_popup_h - 40 - 40 - 10) - 25; int start_idx = 0; int current_h = 0; for (int i = 0; i < num_chats; i++) { if (current_h >= search_scroll_pos) { start_idx = i; item_y = search_popup_y + 40 + 40 + 10 + (current_h - search_scroll_pos); break; } current_h += 25 + 5; } int visible_count = 0; current_h = 0; for (int i = start_idx; i < num_chats; i++) { if (current_h + 25 > search_visible_height) break; current_h += 25 + 5; visible_count++; } for (int i = 0; i < visible_count; i++) { int chatIdx = num_chats - 1 - (start_idx + i); string hashed_id = EncodeID(filtered[chatIdx].id); string fullText = filtered[chatIdx].title + " > " + hashed_id; string labelText = fullText; if (StringLen(fullText) > 35) { labelText = StringSubstr(fullText, 0, 32) + "..."; } string bgName = "ChatGPT_SearchChatBg_" + hashed_id; if (item_y >= search_popup_y + 40 + 40 + 10 && item_y < end_y) { createRecLabel(bgName, search_popup_x + 20, item_y, item_w, 25, clrBeige, 1, DarkenColor(clrBeige, 0.9), BORDER_FLAT, STYLE_SOLID); } string labelName = "ChatGPT_SearchChatLabel_" + hashed_id; if (item_y >= search_popup_y + 40 + 40 + 10 && item_y < end_y) { createLabel(labelName, search_popup_x + 30, item_y + 3, labelText, clrBlack, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); } string deleteName = "ChatGPT_SearchDelete_" + hashed_id; if (item_y >= search_popup_y + 40 + 40 + 10 && item_y < end_y) { createButton(deleteName, search_popup_x + 20 + item_w - 25, item_y + 3, 0, 20, "V", clrBeige, 15, clrBeige, clrBeige, "Wingdings 2"); } int size = ArraySize(search_popup_objects); ArrayResize(search_popup_objects, size + 3); search_popup_objects[size] = bgName; search_popup_objects[size + 1] = labelName; search_popup_objects[size + 2] = deleteName; int bg_size = ArraySize(search_chat_bgs); ArrayResize(search_chat_bgs, bg_size + 1); search_chat_bgs[bg_size] = bgName; int del_size = ArraySize(search_delete_btns); ArrayResize(search_delete_btns, del_size + 1); search_delete_btns[del_size] = deleteName; item_y += 25 + 5; } if (search_scroll_visible) { search_slider_height = CalculateSearchSliderHeight(); ObjectSetInteger(0, SEARCH_SCROLL_SLIDER, OBJPROP_YSIZE, search_slider_height); UpdateSearchSliderPosition(); UpdateSearchButtonColors(); } ChartRedraw(); } void DeleteSearchScrollbar() { ObjectDelete(0, SEARCH_SCROLL_LEADER); ObjectDelete(0, SEARCH_SCROLL_UP_REC); ObjectDelete(0, SEARCH_SCROLL_UP_LABEL); ObjectDelete(0, SEARCH_SCROLL_DOWN_REC); ObjectDelete(0, SEARCH_SCROLL_DOWN_LABEL); ObjectDelete(0, SEARCH_SCROLL_SLIDER); } void DeleteSearchPopup() { for (int i = 0; i < ArraySize(search_popup_objects); i++) { ObjectDelete(0, search_popup_objects[i]); } DeleteSearchScrollbar(); ArrayResize(search_popup_objects, 0); ArrayResize(search_chat_bgs, 0); ArrayResize(search_delete_btns, 0); showing_search_popup = false; search_scroll_visible = false; }
Сначала реализуем функцию ShowSearchPopup, которая создает отдельное окно поиска чатов. current_search_query сбрасывается в пустую строку, очищаются массивы search_popup_objects, search_chat_bgs и search_delete_btns, позиции прокрутки присваивается значение 0, а флагу видимости – false. Размеры окна поиска рассчитываются по g_mainWidth и g_displayHeight; оно размещается с отступом от границ основной области. Через createRecLabel создается светло-серый фон ChatGPT_SearchBg с рамкой цвета dodger blue, а через createButton – кнопка закрытия ChatGPT_SearchCloseButton с красным символом r шрифта Webdings. Поле ввода ChatGPT_SearchInput создается через createEdit в цвете gainsboro, а метка-заполнитель ChatGPT_SearchPlaceholder с текстом Search Chats выводится серым шрифтом Arial. Добавляется белый фон содержимого ChatGPT_SearchContentBg с рамкой gainsboro. По общему числу чатов определяется, нужна ли прокрутка, затем UpdateSearchDisplay заполняет результаты. Если прокрутка требуется, вызывается CreateSearchScrollbar, включается ее видимость, обновляются положение и цвета. После этого showing_search_popup и just_opened_search получают значение true, а график перерисовывается через ChartRedraw.
Функция CreateSearchScrollbar создает полосу прокрутки окна поиска. Через createRecLabel создаются светло-серая дорожка полосы прокрутки с рамкой gainsboro, прямоугольники кнопок вверх и вниз цвета gainsboro, метки со стрелками Webdings цвета dim gray и серебристый ползунок с рамкой gainsboro. Позиции рассчитываются по search_popup_x, search_popup_y, search_popup_w и search_popup_h, а имена объектов добавляются в search_popup_objects для последующего удаления. Функция CalculateSearchSliderHeight вычисляет высоту ползунка поиска пропорционально отношению видимой высоты к общей. Минимальная высота составляет 20 пикселей; если прокрутка не нужна, возвращается высота всей области. Функция UpdateSearchSliderPosition корректирует координату Y ползунка поиска по отношению search_scroll_pos к максимальной прокрутке. Значение ограничивается пределами области прокрутки через MathMax и MathMin, а затем задается с помощью ObjectSetInteger. Функция UpdateSearchButtonColors окрашивает недоступную стрелку вверх или вниз в серебристый цвет, если достигнут соответствующий предел прокрутки, и в цвет dim gray, если прокрутка в этом направлении доступна. Функции SearchScrollUp и SearchScrollDown уменьшают или увеличивают search_scroll_pos на 30, ограничивая значение диапазоном от 0 до максимума. Затем через UpdateSearchDisplay обновляются результаты, а если полоса прокрутки видима, через UpdateSearchSliderPosition и UpdateSearchButtonColors обновляются положение ползунка и цвета кнопок.
В UpdateSearchDisplay через ObjectsTotal и ObjectDelete с помощью функции StringFind удаляются существующие фоны чатов, метки и кнопки удаления в окне поиска. Затем сбрасываются массивы. Из массива chats формируется массив filtered: заголовок, история и запрос приводятся к нижнему регистру через StringToLower. Совпадения ищутся через StringFind; если запрос пуст, включаются все чаты. По числу отфильтрованных чатов пересчитывается search_total_height, определяется необходимость прокрутки и резервируемая для нее ширина, после чего задается ширина элемента. По search_scroll_pos вычисляются начальный индекс и координата Y, а в пределах search_visible_height подсчитываются видимые элементы. Затем цикл создает фоны ChatGPT_SearchChatBg_, метки ChatGPT_SearchChatLabel_ с сокращенными заголовками и кнопки удаления ChatGPT_SearchDelete_ с символом V шрифта Wingdings бежевого цвета; изначально размер кнопок равен 0. Имена сохраняются в массивах, а объекты размещаются только в границах видимой области. Если search_scroll_visible имеет значение true, через CalculateSearchSliderHeight обновляется высота ползунка, через UpdateSearchSliderPosition – его размер и положение, а затем обновляются цвета кнопок прокрутки.
Функция DeleteSearchScrollbar удаляет все объекты полосы прокрутки окна поиска. Функция DeleteSearchPopup удаляет все объекты из search_popup_objects, включая полосу прокрутки через DeleteSearchScrollbar. Затем сбрасываются массивы, а showing_search_popup и search_scroll_visible получают значение false. В результате будет создан следующий интерфейс.

Функции интерфейса готовы, поэтому их можно вызывать в обработчике событий графика для создания нужных объектов и обработки событий. Здесь используется тот же подход, что и для основного списка чатов, но применительно к обработке поиска.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- variables for elements 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(); } } if (showing_search_popup) { if (just_opened_search) { just_opened_search = false; } else if (clickX < search_popup_x || clickX > search_popup_x + search_popup_w || clickY < search_popup_y || clickY > search_popup_y + search_popup_h) { DeleteSearchPopup(); ChartRedraw(); } } return; } if (id == CHARTEVENT_OBJECT_CLICK) { if (StringFind(sparam, "ChatGPT_ChatLabel_") == 0 || StringFind(sparam, "ChatGPT_SmallChatLabel_") == 0 || StringFind(sparam, "ChatGPT_BigChatLabel_") == 0 || StringFind(sparam, "ChatGPT_SearchChatLabel_") == 0) { string prefix = ""; if (StringFind(sparam, "ChatGPT_ChatLabel_") == 0) prefix = "ChatGPT_ChatLabel_"; else if (StringFind(sparam, "ChatGPT_SmallChatLabel_") == 0) prefix = "ChatGPT_SmallChatLabel_"; else if (StringFind(sparam, "ChatGPT_BigChatLabel_") == 0) prefix = "ChatGPT_BigChatLabel_"; else if (StringFind(sparam, "ChatGPT_SearchChatLabel_") == 0) prefix = "ChatGPT_SearchChatLabel_"; string hashed_id = StringSubstr(sparam, StringLen(prefix)); int new_id = DecodeID(hashed_id); int idx = GetChatIndex(new_id); if (idx >= 0 && new_id != current_chat_id) { UpdateCurrentHistory(); current_chat_id = new_id; current_title = chats[idx].title; conversationHistory = chats[idx].history; if (showing_small_history_popup) DeleteSmallHistoryPopup(); if (showing_big_history_popup) DeleteBigHistoryPopup(); if (showing_search_popup) DeleteSearchPopup(); UpdateResponseDisplay(); UpdateSidebarDynamic(); ChartRedraw(); } return; } else if (StringFind(sparam, "ChatGPT_SideDelete_") == 0 || StringFind(sparam, "ChatGPT_SmallDelete_") == 0 || StringFind(sparam, "ChatGPT_BigDelete_") == 0 || StringFind(sparam, "ChatGPT_SearchDelete_") == 0) { string prefix = ""; if (StringFind(sparam, "ChatGPT_SideDelete_") == 0) prefix = "ChatGPT_SideDelete_"; else if (StringFind(sparam, "ChatGPT_SmallDelete_") == 0) prefix = "ChatGPT_SmallDelete_"; else if (StringFind(sparam, "ChatGPT_BigDelete_") == 0) prefix = "ChatGPT_BigDelete_"; else if (StringFind(sparam, "ChatGPT_SearchDelete_") == 0) prefix = "ChatGPT_SearchDelete_"; string hashed_id = StringSubstr(sparam, StringLen(prefix)); int del_id = DecodeID(hashed_id); DeleteChat(del_id); return; } } //--- else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SearchButton") { ShowSearchPopup(); just_opened_search = true; } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SearchCloseButton") { DeleteSearchPopup(); ChartRedraw(); } else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == SEARCH_SCROLL_UP_REC || sparam == SEARCH_SCROLL_UP_LABEL)) { SearchScrollUp(); } else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == SEARCH_SCROLL_DOWN_REC || sparam == SEARCH_SCROLL_DOWN_LABEL)) { SearchScrollDown(); } else if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "ChatGPT_SearchInput") { current_search_query = (string)ObjectGetString(0, sparam, OBJPROP_TEXT); if (StringLen(current_search_query) == 0) { if (ObjectFind(0, "ChatGPT_SearchPlaceholder") < 0) { int lineHeight = TextGetHeight("A", "Arial", 11); int labelY = search_popup_y + 5 + (30 - lineHeight) / 2 - 3; createLabel("ChatGPT_SearchPlaceholder", search_popup_x + 10 + 2, labelY, "Search Chats", clrGray, 11, "Arial", CORNER_LEFT_UPPER); } } else { if (ObjectFind(0, "ChatGPT_SearchPlaceholder") >= 0) { ObjectDelete(0, "ChatGPT_SearchPlaceholder"); } } UpdateSearchDisplay(); ChartRedraw(); } // mouse move logic same implementation }
Обработчик событий OnChartEvent обеспечивает функционал удаления чатов и поиска. Он обрабатывает клики и события редактирования в новом окне поиска, а также клики по кнопкам удаления. При обычных кликах (CHARTEVENT_CLICK) сохраняются координаты clickX и clickY, а затем проверяется, открыто ли одно из всплывающих окон: большое окно истории, малое окно истории или окно поиска. Если окно открыто, но не только что открыто – это определяется флагами наподобие just_opened_search – проверяется, находится ли клик за его границами. В таком случае соответствующее окно закрывается; для окна поиска вызывается DeleteSearchPopup, затем ChartRedraw, после чего обработчик завершает работу.
При кликах по объектам CHARTEVENT_OBJECT_CLICK выбор чата обрабатывается во всех представлениях по префиксам наподобие ChatGPT_SearchChatLabel_. Извлекается хешированный ID, после чего закрываются все открытые всплывающие окна. Через UpdateResponseDisplay и UpdateSidebarDynamic обновляются область ответа и боковая панель, график перерисовывается функцией ChartRedraw, затем обработчик завершает работу. Для удаления проверяются префиксы наподобие ChatGPT_SideDelete_ и ChatGPT_SearchDelete_. Затем ID извлекается и декодируется, вызывается DeleteChat для удаления чата, после чего обработчик завершает работу.
При клике по кнопке поиска ChatGPT_SearchButton вызывается ShowSearchPopup, а just_opened_search получает значение true. При клике по кнопке закрытия окна поиска ChatGPT_SearchCloseButton вызывается DeleteSearchPopup, затем график перерисовывается. Для элементов полосы прокрутки поиска SEARCH_SCROLL_UP_REC / SEARCH_SCROLL_UP_LABEL вызывается SearchScrollUp, а для SEARCH_SCROLL_DOWN_REC / SEARCH_SCROLL_DOWN_LABEL – SearchScrollDown. При завершении редактирования поля ChatGPT_SearchInput, то есть при событии CHARTEVENT_OBJECT_ENDEDIT, введенный текст сохраняется в current_search_query. Если поле пусто и метки-заполнителя ChatGPT_SearchPlaceholder нет, она создается через createLabel с текстом Search Chats, выполненным серым шрифтом Arial. Если поле не пусто, существующая метка-заполнитель удаляется. Затем через UpdateSearchDisplay обновляются результаты поиска, а график перерисовывается. Логика перемещения мыши для эффектов наведения и прокрутки повторяет предыдущие реализации и здесь опущена для краткости. Она обеспечивает единообразную интерактивность интерфейса. После этого реализацию обработки событий графика можно считать завершенной. Теперь нужно также задать приоритет окну поиска, чтобы избежать мерцания.
void UpdateResponseDisplay() { if (showing_small_history_popup || showing_big_history_popup || showing_search_popup) return; //--- rest of the function logic }
Здесь достаточно расширить условие в начале функции, чтобы блокировать обновления при выводе окна поиска. Пока оно открыто, обновление области ответа пропускается, поскольку окно создается в ее пределах. После компиляции получаем следующий результат для окна поиска.

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

На визуализации видно, что целостность журналов чата сохраняется, поскольку мы не вносим в них изменений. Если вы часто пользуетесь чатом и хотите удалять также записи журнала, ниже приведена необходимая логика.
void ClearLogsForChat(int del_id) { if (logFileHandle != INVALID_HANDLE) { FileClose(logFileHandle); } string tempFile = "Temp_Log.txt"; int readHandle = FileOpen(LogFileName, FILE_READ | FILE_TXT); if (readHandle == INVALID_HANDLE) { Print("Failed to open log for reading: ", GetLastError()); ReopenLogHandle(); return; } int writeHandle = FileOpen(tempFile, FILE_WRITE | FILE_TXT); if (writeHandle == INVALID_HANDLE) { Print("Failed to open temp log: ", GetLastError()); FileClose(readHandle); ReopenLogHandle(); return; } bool skipBlock = false; while (!FileIsEnding(readHandle)) { string line = FileReadString(readHandle); if (StringFind(line, "Chat ID: ") == 0) { // Reset skip for new block skipBlock = false; // Parse ID int commaPos = StringFind(line, ", Title: "); if (commaPos > 0) { string idStr = StringSubstr(line, 9, commaPos - 9); int chatId = (int)StringToInteger(idStr); if (chatId == del_id) { skipBlock = true; } } // If parse fails, don't skip (safe default) } if (!skipBlock) { FileWrite(writeHandle, line); } } FileClose(readHandle); FileClose(writeHandle); FileDelete(LogFileName); FileMove(tempFile, 0, LogFileName, 0); ReopenLogHandle(); } void ReopenLogHandle() { logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT); if (logFileHandle != INVALID_HANDLE) { FileSeek(logFileHandle, 0, SEEK_END); } else { Print("Failed to reopen log: ", GetLastError()); } } void DeleteChat(int id) { //--- rest of the logic ArrayRemove(chats, idx, 1); ClearLogsForChat(id); //--- we call the function after removing the chats SaveChats(); //--- rest of the logic }
Начнем с функции ClearLogsForChat, которая удаляет все записи журнала, относящиеся к чату с ID del_id. Сначала проверяется, является ли logFileHandle корректным хэндлом, то есть открыт ли файл журнала. Если файл открыт, он закрывается через FileClose, чтобы безопасно подготовить его к операциям чтения и записи. Затем переменной tempFile присваивается имя временного файла "Temp_Log.txt", используемого как промежуточное хранилище при очистке. Далее через FileOpen открывается основной файл журнала с именем из LogFileName в режиме чтения, с флагами FILE_READ и FILE_TXT. Если файл не удается открыть, вызывается ReopenLogHandle для восстановления доступа к файлу журнала, после чего функция завершает работу.
Затем временный файл открывается через FileOpen в режиме записи, с флагами FILE_WRITE и FILE_TXT. Если открыть временный файл не удается, перед завершением функции вызывается ReopenLogHandle. После успешного открытия обоих файлов создается булева переменная skipBlock, которой присваивается значение false. Эта переменная определяет, нужно ли пропускать запись отдельных блоков журнала. Затем запускается цикл while (!FileIsEnding(readHandle)), который выполняется до конца файла журнала. Внутри цикла каждая строка читается через FileReadString и сохраняется в line. Если строка начинается с "Chat ID: " – это определяется через StringFind – она считается началом новой записи чата. Переменной skipBlock снова присваивается значение false, затем извлекается числовой ID чата. Для этого через StringFind определяется позиция текста "Title: " в строке, через StringSubstr извлекается часть с ID чата, а затем StringToInteger преобразует ее в целое число. Если извлеченный chatId совпадает с удаляемым del_id, переменной skipBlock присваивается значение true, поэтому все строки этого чата пропускаются. Все остальные строки через FileWrite записываются во временный файл, благодаря чему сохраняются записи журнала только других чатов.
После обработки всех строк оба хэндла файлов закрываются через FileClose. Затем старый файл журнала удаляется через FileDelete, а временный файл через FileMove переименовывается в его имя. Так записи удаленного чата исключаются из журнала. В конце вызывается ReopenLogHandle, чтобы снова открыть основной файл журнала и продолжить обычную работу. Логика функции ReopenLogHandle уже знакома из предыдущих частей. В дальнейшем эта функция вызывается при удалении чата и выполняет основную операцию очистки. Если удалять записи журнала не требуется, это изменение можно не применять. Следует учитывать, что большие файлы журналов могут замедлить работу системы из-за повторного открытия и перезаписи файлов. Для управления очисткой журнала добавлен входной параметр, которому по умолчанию присвоено значение true. См. ниже.
input bool DeleteLogsOnChatDelete = true; // Clear logs when deleting chats? //--- added conditional log clearing if (DeleteLogsOnChatDelete) { ClearLogsForChat(id); }
После компиляции получаем следующий результат.

Заключение
В этой части мы добавили в торговую систему с ИИ на MQL5 функционал удаления чатов с помощью кнопок, появляющихся при наведении на боковой панели и во всплывающих окнах. Также реализовано окно поиска с фильтрацией заголовков и истории чатов в реальном времени без учета регистра. Это позволяет эффективно управлять сохраненными между сеансами чатами в зашифрованном виде, сохраняя многострочный ввод и возможность генерировать торговые сигналы с помощью ИИ по данным графика. Эти возможности позволяют убрать лишнее из истории и быстро находить нужную информацию. Интуитивно понятные элементы управления и плавное обновление интерфейса повышают продуктивность в динамичной рыночной среде. В следующих частях мы добавим расширенные возможности исполнения сделок на основе сигналов ИИ и рассмотрим мониторинг нескольких символов для полностью автоматизированного помощника. Оставайтесь с нами.
Приложения
| № | Имя | Тип | Описание |
|---|---|---|---|
| 1 | AI_JSON_FILE.mqh | Библиотека классов JSON | Класс для сериализации и десериализации JSON |
| 2 | AI_CREATE_OBJECTS_FNS.mqh | Библиотека функций объектов | Функции для создания объектов визуализации, таких как метки и кнопки |
| 3 | AI_ChatGPT_EA_Part_6.mq5 | Советник | Основной советник для интеграции ИИ |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20254
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 85): Использование паттернов стохастического осциллятора и FrAMA с обучением на основе инференса β-VAE
Создание и использование Скиллов для создания артефактов MQL
Изучение Стандартной библиотеки MQL5 (Часть 5): Мультисигнальный советник
Моделирование рынка: Position View (XIV)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования