English
preview
MQL5でのAI搭載取引システムの構築(第5回):チャットポップアップを備えた折りたたみ可能なサイドバーの追加

MQL5でのAI搭載取引システムの構築(第5回):チャットポップアップを備えた折りたたみ可能なサイドバーの追加

MetaTrader 5トレーディングシステム |
26 2
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第4回)では、ChatGPT統合型MetaQuotes Language 5 (MQL5)プログラムの改良として、複数行入力の改善、AES256およびZIPによる暗号化チャットストレージの追加、チャートデータからの売買シグナルの生成をおこないました。今回の第5回では、AI搭載の取引システムに折りたたみ可能なサイドバーを追加します。この改良により、画面管理を容易にする動的サイドバーを実装し、大小の履歴ポップアップでチャットへの素早いアクセスを可能にします。同時に、複数行入力、暗号化ストレージ、シグナル生成機能も引き続きサポートされます。本記事では以下のトピックを扱います。

  1. AI取引インターフェースにおける折りたたみ可能サイドバーの重要性
  2. MQL5での実装
  3. 折りたたみ可能なサイドバーのテスト
  4. 結論

この記事を読み終える頃には、UIの柔軟性が最適化されたMQL5 AI取引アシスタントを構築できるようになり、自由にカスタマイズできる状態になります。


AI取引インターフェースにおける折りたたみ可能サイドバーの重要性

AI取引インターフェース(たとえばChatGPT統合型システム)における折りたたみ可能サイドバーは、画面スペースの最適化とユーザー体験の向上に不可欠です。サイドバーを展開状態と縮小状態でトグルすることで、必要に応じてチャート表示を優先したり、分析時にナビゲーション情報を十分に参照したりでき、機能性を損なうことなく作業効率を高められます。縮小モードではホバーで小さな履歴ポップアップを表示し、すべてのチャットをスクロール可能な大きな履歴ポップアップで確認できるようにすることで、過去のAIによる市場分析結果やシグナルを素早く参照でき、シンプルで直感的なインターフェースを保ったまま、迅速で情報に基づく取引判断をサポートします。

今回のアプローチは、既存システムの強みをさらに活かす形で構築されています。既にご存じの方も多いと思いますが、もし前のバージョンをご覧になっていない場合は、シリーズの前の回を参照してください。折りたたみ可能なサイドバーは、この既存システムの強みを拡張し、柔軟性を提供します。展開モードではチャットナビゲーションが明確に表示され、縮小モードでは画面の邪魔にならないよう最小化されます。縮小モードではホバーで小さな履歴ポップアップを表示し、過去のチャットを素早く参照できます。一方、大きな履歴ポップアップでは包括的にチャットを選択できるため、モード間の切り替えがスムーズで直感的になります。このデザインは、コンパクト表示と詳細表示の両方を必要とするトレーダーに対応しており、インターフェースが状況に応じて柔軟に変化する一方で、トレンド分析や反転などの市場機会に対するAIが生成した分析結果を引き続き活用できます。さらに、動的検索機能のためにより大きなチャットビューも追加する予定ですが、現時点ではシンプルな構成に留めています。以下は意図した画面構成の視覚的なイメージです。

縮小サイドバーと小さな履歴ポップアップ

縮小サイドバーと小さな履歴ポップアップ

大きな履歴ポップアップ

スクロールバー付きの大きな履歴ポップアップ


MQL5での実装

アップグレードを実装するために、まず追加するオブジェクトを宣言します。対象は、縮小サイドバー、[History]ボタンにホバーしたときに表示される小さな履歴ポップアップ、折りたたみボタン、そしてすべてのチャット履歴を表示する大きなポップアップです。中でも最も重要なのは、大きなポップアップ用のスクロールバーです。その他のオブジェクトは、既存のプレフィックスを使って作成可能なので、まずスクロールバーの実装から着手します。

#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を用意し、toggle_hoverはfalseに初期化します。[History]ボタン用にhistory_original_bgを白に、history_darker_bgをホバー用に用意し、history_hoverはfalseにします。詳細ボタン用にはsee_more_original_bgをロイヤルブルーに、see_more_darker_bgをホバー用に用意し、see_more_hoverはfalseにします。大きなポップアップの[Close]ボタンにはbig_close_original_bgをライトグレーに、big_close_darker_bgをホバー用に用意し、big_close_hoverはfalseにします。サイドバーの幅は展開時をexpandedSidebarWidth=150、縮小時をcontractedSidebarWidth=50とし、sidebarExpandedをtrueにして初期状態を展開状態にします。

履歴ポップアップは2種類管理します。小さなポップアップでは、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フラグでポップアップの開閉直後に意図せず閉じてしまわないよう制御します。これにより、折りたたみナビゲーションとチャット履歴へのアクセスを動的に管理するUIを実現できます。これらの変数を準備した上で、新しいインターフェースを初期化します。

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関数内では、新しい要素を初期化します。具体的には、新しいボタンの色を設定し、縮小サイドバー用のロゴをスケーリングし、小さなサイドバー用の設定も初期化します。まず、追加したボタン(トグル、[History]、詳細、大きなポップアップの[Close]ボタン)に対して、元の背景色に対してDarkenColorを使い、ホバー時の濃い色を設定します。その後、ResourceCreateを使ってARGB形式でg_scaled_image_resourceとして「::ChatGPT_HeaderImageScaled」を作成し、成功と失敗の結果をログに出力します。

同様に、サイドバーのロゴであるresourceImgLogoを81×81にスケーリングしてg_scaled_sidebar_resourceを作成し、さらに30×30にスケーリングして小さなバージョンのg_scaled_sidebar_smallを作成します。これが今回追加した主な変更点です。作成時にはリソースの生成結果をログに出力します。新しく追加したリソースについても同様に作成し、必要に応じて後で使用します。ここまでの変更箇所は分かりやすくするためにハイライト表示しています。次に、サイドバーの動的関数を更新し、縮小状態のロジックに対応させ、toggleボタンを追加する必要があります。

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などのプレフィックスを持つボタンやトグルボタンをObjectsTotalObjectDelete、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」で中央配置し、z-orderは1に設定します。

ボタン(New Chat、Clear、History)はそれぞれcreateButtonで作成します。幅は「g_sidebarWidth - 10」、高さはg_buttonHeight、z-orderは1、色はnew_chat_original_bg、clear_original_bg、history_original_bgなどを使用します。アイコンはcreateBitmapLabelで配置し、iconXの座標を展開時は左寄せ、縮小時は中央寄せに計算し、スケーリング済みリソースやresourceNewChatなどのデフォルトを使用、z-orderは2、選択不可に設定します。

展開状態では各ボタンに対して「New Chat」「Clear」「History」のテキストラベルをcreateLabelで追加します。フォントはArial、サイズ11/12、z-orderは2、選択不可でアイコンの右に配置します。展開時には、これまでと同様にchats配列から最新7件までのチャットを表示します。最後に、新しいトグルボタンChatGPT_ToggleButtonをcreateButtonで追加します。展開時は「<<」、縮小時は「>>」を表示し(次のバージョンで、より見やすい表示に変更する予定です)、フォントはArialサイズ12、z-orderは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と呼びます。まず、ダッシュボード背景ChatGPT_DashboardBgの幅をg_dashboardWidthに、サイドバー背景ChatGPT_SidebarBgの幅をg_sidebarWidth-2-1に設定し、ObjectSetIntegerOBJPROP_XSIZEを適用します。

水平シフトdiffは、g_mainContentXの変化量に対してダッシュボード開始位置とサイドバーの反対幅(展開時は縮小、縮小時は展開)を考慮して計算します。次にdashboardObjects内のobjCount個のオブジェクトをループし、ObjectFindで存在を確認し、ObjectGetInteger でOBJPROP_XDISTANCEを取得、diffを加えてメインコンテンツを移動します。ここで特に重要なのは、既存のメインコンテンツを削除および再作成するのではなく位置だけを変更するため、フリッカーが発生しない点です。

日付ラベルはTimeToStringでサーバー時刻を取得し、フォントArialサイズ12でTextGetSizeによりサイズを計算、g_mainContentXとg_mainWidthに合わせて中央配置し、X位置を設定します。[Close]ボタンはg_mainContentXとg_mainWidthからパディングと幅を引いた位置に再計算して設定します。プロンプトは幅promptW、編集フィールドX/幅g_editWを更新し、ObjectSetIntegerでOBJPROP_XSIZEを適用します。チャートや送信ボタンもg_mainContentXとg_sidePaddingから新しいX位置を設定します。

レスポンスとプロンプトの背景はX位置を更新し、displayXとdisplayWに合わせて幅を調整します。フッター背景もg_mainContentXに移動し、幅をg_mainWidthに設定します。プロンプトのプレースホルダーが存在する場合は、編集フィールドに合わせてXを更新します。メインスクロールバーが表示されている場合、X位置を「displayX + displayW - 16」で再計算し、リーダー、上下ボタン、スライダーなど全てのコンポーネントのX位置を更新、UpdateSliderPositionを呼び出します。プロンプトスクロールバーも同様に「promptX + promptW - 16」でXを計算し、全要素を調整して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関数では、縮小サイドバーで[History]ボタンにホバーした際に最近のチャット履歴を表示するコンパクトなポップアップを作成します。small_popup_objectsとsmall_chat_bgs配列を初期化します。ポップアップの位置は[History]ボタンのX座標と幅をObjectGetIntegerで取得して右側に設定し、幅は200ピクセル、アイテム高さは25、最大7件の最近チャットと[See All]ボタンから動的に高さを計算します。背景ChatGPT_SmallHistoryBgはcreateRecLabelで白、境界線はライトグレーで作成します。次に最新チャットから順にループし、背景ChatGPT_SmallChatBg_とラベルChatGPT_SmallChatLabel_をcreateRecLabelとcreateLabelで作成し、タイトルは必要に応じて22文字で省略表記、オブジェクト名は後片付け用に配列に格納します。最後に下部にSee MoreボタンをcreateButtonでロイヤルブルーにて追加します。

DeleteSmallHistoryPopup関数では、small_popup_objects内のすべてのオブジェクトを ObjectDeleteで削除し、配列を初期化、showing_small_history_popupをfalseに設定して小ポップアップを消去します。ShowBigHistoryPopupでは、スクロール可能な大きな履歴ポップアップを表示します。big_popup_objectsとbig_chat_bgsを初期化し、big_scroll_posを0、表示フラグをfalseに設定します。ポップアップ幅は「g_mainWidth - 20」、高さは項目の高さ25 + パディングで最大を制限し、メインコンテンツから内側に配置します。背景ChatGPT_BigHistoryBgはライトグレー、境界線はドジャーブルーで作成します。[Close]ボタンChatGPT_BigCloseButtonはcreateButtonでWebdingsの'r'、赤で作成、コンテンツ背景ChatGPT_BigContentBgは白、境界線はゲインズボローです。スクロールの必要性はbig_total_heightとbig_visible_heightで判断し、UpdateBigHistoryDisplayでチャットを配置します。スクロールが必要ならCreateBigScrollbarを呼び出し、表示フラグをtrueにし、位置と色を更新します。表示中フラグshowing_big_history_popupとjust_opened_bigをtrueにし、ChartRedraw関数で描画します。

CreateBigScrollbar関数では大ポップアップ用スクロールバーを作成します。リーダー、上下矩形をゲインズボロー、ラベルはWebdings矢印でダークグレー、スライダーはシルバーとゲインズボローの境界線、位置はbig_popup_x/y/w/hから計算し、オブジェクト名をbig_popup_objectsに追加します。CalculateBigSliderHeightは表示高さと合計高さの比率からスライダーの高さを計算、最小20ピクセル、スクロール不要なら全体高さを返します。UpdateBigSliderPositionはbig_scroll_posの比率からスライダーYを計算し、MathMaxとMathMinでスクロール範囲内に制限、ObjectSetInteger関数で設定します。UpdateBigButtonColors関数では、大ポップアップの上下矢印をスクロールが上限または下限の場合はシルバー、それ以外でスクロール可能な場合はダークグレーに変更します。BigScrollUp/BigScrollDownでは、big_scroll_posをそれぞれ30減算/加算し、0〜最大値で制限します。その後、UpdateBigHistoryDisplayで表示を更新し、ポップアップが表示中であればUpdateBigSliderPositionとUpdateBigButtonColorsでスライダー位置とボタン色を更新します。

UpdateBigHistoryDisplayでは、既存の大チャット背景とラベルをObjectsTotalとObjectDelete、StringFind関数を用いて消去します。big_total_heightをチャット数から再計算し、スクロールの必要性と確保幅を決定し、アイテム幅を設定します。開始インデックスとY座標をbig_scroll_posから計算し、big_visible_height内で表示可能なアイテム数を数え、ループでChatGPT_BigChatBg_とChatGPT_BigChatLabel_を作成します(最新チャットから表示)、タイトルは32文字で省略、配列に格納し、表示範囲内にのみ配置してChartRedrawで描画します。DeleteBigHistoryPopup関数では、big_popup_objects内のすべてのオブジェクトをObjectDeleteで削除し、配列を初期化、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がfalseの場合(誤ってすぐ閉じるのを防ぐため)、クリックがポップアップ外であれば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関数を呼んでUIを更新します。サイドバー展開中に[History]ボタンChatGPT_HistoryButtonが押されると、ShowBigHistoryPopupを呼び、just_opened_bigをtrueに設定します。

詳細ボタン(ChatGPT_SeeMoreButton)では、小ポップアップをDeleteSmallHistoryPopupで閉じ、大ポップアップを表示してjust_opened_bigをtrueに設定します。大ポップアップの[Close]ボタンChatGPT_BigCloseButtonではDeleteBigHistoryPopupを呼び、再描画します。大ポップアップのスクロールバー上部要素(BIG_SCROLL_UP_RECまたはラベル)ではBigScrollUp、下部要素ではBigScrollDownを呼びます。その他のホバー効果やマウス移動のロジックも既存の実装と同様におこない、インタラクティブ性を維持します。コンパイル後の結果は以下の通りです。

小さなポップアップでサイドバーを更新した

ポップアップ表示中はメイン表示の更新をおこなわないようにするため、UpdateResponseDisplay関数の先頭で条件分岐を追加し、ポップアップがメイン領域に重なっている場合は更新をスキップします。これにより安定性が向上し、ポップアップ使用時のちらつきが抑えられます。

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

   //--- rest of the function logic

}

関数の冒頭で条件文を追加することで、ポップアップ表示中はレスポンス表示の更新をスキップします。コンパイル後の大ポップアップの結果は以下の通りです。

ポップアップにホバー効果とスクロールバーを追加した大チャートポップアップ

画像から、サイドバーの折りたたみ機能やチャットポップアップを追加し、新しいターゲット要素を導入することでプログラムをアップグレードできたことが確認できます。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


折りたたみ可能なサイドバーのテスト

テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

バックテストGIF


結論

MQL5のAI対応取引システムを強化し、画面管理を改善するために展開と縮小を切り替えられる折りたたみ可能なサイドバーを導入し、ホバー操作で表示される小さな履歴ポップアップとスクロール可能な大きな履歴表示を組み込み、チャットナビゲーションを効率化しました。その一方で、複数行入力、暗号化による永続保存、チャートデータからのAI生成シグナルの機能は維持しています。このアップグレードにより、インターフェースの柔軟性が最適化され、必要に応じてチャート表示を最大化したり詳細コントロールにアクセスしたりできるようになり、シームレスな遷移と直感的なホバー効果が可能になりました。次のパートでは、チャット検索機能の追加、チャット削除ボタンの追加、さらにAI支援取引を強化するシグナル実行および自動化の高度な機能について解説します。どうぞご期待ください。


添付ファイル

シリアル番号名前種類詳細
1AI_JSON_FILE.mqh
JSONクラスライブラリ
JSONのシリアライズおよびデシリアライズを扱うクラス
2AI_CREATE_OBJECTS_FNS.mqh
オブジェクト関数ライブラリ
ラベルやボタンなどの可視化オブジェクトを作成する関数
3AI_ChatGPT_EA_Part_5.mq5
EA
AI統合を扱うメインのEA

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20249

添付されたファイル |
AI_JSON_FILE.mqh (26.62 KB)
最後のコメント | ディスカッションに移動 (2)
AMIRREZA REZAZAD
AMIRREZA REZAZAD | 17 11月 2025 において 04:33
このAIポップアップチャットを作ってくれてありがとう。もしチャート画像をアップロードすることができれば(チャート上にプライスアクションインジケータのような有用なデータがあるチャート)、AIによる市場分析が強化され、非常に役立ちます。
Allan Munene Mutiiria
Allan Munene Mutiiria | 17 11月 2025 において 07:14
AMIRREZA REZAZAD # : このAIポップアップチャットを作ってくれてありがとう。もしチャート画像をアップロードすることができれば(チャート上にプライスアクションインジケータのような有用なデータがあるチャート)、AIによる市場分析が強化され、非常に役立ちます。
こんにちは。ご丁寧なフィードバックをありがとうございます。
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
MQL5で自己最適化エキスパートアドバイザーを構築する(第17回):アンサンブルインテリジェンス MQL5で自己最適化エキスパートアドバイザーを構築する(第17回):アンサンブルインテリジェンス
すべてのアルゴリズム取引戦略は、その複雑さに関係なく、構築や維持が困難です。これは初心者と専門家の双方に共通する課題です。本記事では、教師ありモデルと人間の直感を組み合わせるアンサンブルフレームワークを紹介し、それぞれの限界を相互に補完する方法を提案します。移動平均チャネル戦略とリッジ回帰モデルを同じテクニカル指標上で整合させることで、集中管理、より速い自己修正、そして本来は収益性のなかったシステムからの利益創出を実現します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5での取引戦略の自動化(第40回):カスタムレベルを使ったフィボナッチリトレースメント取引 MQL5での取引戦略の自動化(第40回):カスタムレベルを使ったフィボナッチリトレースメント取引
フィボナッチリトレースメント取引のためのMQL5エキスパートアドバイザー(EA)を構築します。日足の値幅またはルックバック配列を使用して、50%や61.8%といったカスタムレベルをエントリー用に計算し、終値と始値の比較に基づいて強気または弱気のセットアップを判断します。システムは、価格が各レベルをクロスした際に買いまたは売りをトリガーし、各レベルごとに最大取引回数を設定できます。また、新しいフィボナッチ計算時の任意決済、最小利益閾値到達後のポイントベースのトレーリングストップ、値幅に対する割合で設定されるストップロスとテイクプロバッファを備えています。