English Deutsch
preview
MQL5でインタラクティブなグラフィカルユーザーインターフェイスを作成する(第2回):コントロールと応答性の追加

MQL5でインタラクティブなグラフィカルユーザーインターフェイスを作成する(第2回):コントロールと応答性の追加

MetaTrader 5トレーディング | 5 9月 2024, 10:27
20 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事では、MetaQuotes Language 5(MQL5)のグラフィカルユーザーインターフェイス(GUI)パネルの基本的なグラフィック要素を構築し、その基盤を固めました。GUI要素が静的な集合体であり、単なるスナップショットとして機能していました。そのため、応答性に欠けるものでした。静的で不屈でした。今回は、そのスナップショットの凍結を解き、インターフェイスに活気を与える方法を探っていきます。この待望の続編では、パネルを次のレベルに引き上げ、インターフェイスに生命を吹き込む方法を紹介します。

  • レイアウトと応答性:静的コンポーネントの概念を超え、相対的な配置や柔軟なレイアウトを採用します。クリック可能でレスポンシブな、編集可能なコンポーネントを取り入れ、ユーザーのインタラクションに対応するパネルを作成します。
  • ダイナミックなアップデート:リアルタイムのデータは、取引アプリケーションの中核です。ライブ価格フィードを取得し、パネルが最新の市場情報を即座に反映するようにします。。
  • コンポーネントの機動性:ドラッグ可能な要素を取り入れ、ユーザーがインターフェイスを直感的に操作できるようにします。特定のコンポーネントを移動可能にし、ユーザー体験の向上を図ります。

以下のトピックでは、応答性が高くインタラクティブなパネルを実現するための方法について解説します。

  1. 自動化される要素の図解
  2. MQL5 GUIの自動化
  3. 結論


自動化される要素の図解

7つのコンポーネントを自動化します。最初のコンポーネントは、クローズボタンがクリックされたときにパネルを閉じる機能です。このボタンが押されると、すべてのパネル要素が削除されるようにします。2番目のコンポーネントは、ポジション管理ボタンがクリックされた際に、ボタンの指示に従って各ポジションと注文をクローズします。例えば、[Profit]ボタンやラベルをクリックすることで、利益が出ているポジションのみをすべてクローズできます。3番目のコンポーネントは取引量に関するもので、エンティティがクリックされると、ユーザーが取引オプションを選択するためのドロップダウンリストが表示されます。

4番目のコンポーネントは、取引ボタンの横にある増減ボタンで、値を直接入力するのではなく、フィールドの値を増減させるためのものです。ユーザーが希望する値を直接入力する場合、編集フィールドはその値を取り込む必要があります。これが5番目のコンポーネントです。6番目のコンポーネントは、ホバー効果の作成です。マウスがホバーされたボタン領域内にある場合、ボタンが大きくなり、マウスがボタンの近接範囲内にあることが示されます。マウスがボタン領域から離れると、ボタンはデフォルトの機能にリセットされます。最後に、7番目のコンポーネントは、価格ティックごとに価格相場をリアルタイムで更新する機能です。 

これらの自動化プロセスとコンポーネントの理解を深めるために、以下に各マイルストーンの詳細な説明を示します。

ステップ表現

何をするのかがわかったところで、さっそく自動化を始めましょう。静的なGUI要素アセンブリを作成した前回の記事を参照してください。では始めます。


MQL5 GUIの自動化

単純なプロセスから複雑なプロセスへと、時系列に沿った構成にしていきます。これにより、ティックや価格提示の変更ごとに価格を更新することが可能になります。これを実現するには、OnTickイベントハンドラが必要です。この関数はMQL5に組み込まれており、通常、価格相場が変化した際に呼び出されます。この関数データ型で、実行を直接処理し、出力を返す必要はありません。関数の実装は以下のようになります。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

  ...

}
//+------------------------------------------------------------------+

これは価格の更新をおこなうイベントハンドラであり、ロジックの中心となります。以下のように、この関数に制御ロジックを追加します。

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, Bid());

ボタンラベルのテキストを変更するためには、ObjectSetStringを使用してオブジェクトのプロパティ(この場合はtext)を設定します。現在のチャートのウィンドウチャートIDは0として指定します。また、ChartID()関数を使用して、現在のチャートウィンドウのチャート識別インデックスを取得することもできます。そして、更新するオブジェクトプロパティがオブジェクトのテキスト文字列値であることを示すために、更新対象のオブジェクト名としてLABEL_SELL_PRICEを、更新するオブジェクトプロパティとしてOBJPROP_TEXTを指定します。最後に、表示するテキストを提供します。買値は更新が必要な値であるため、それを記入しますが、それだけではありません。入力する必要のあるプロパティタイプは文字列データ値であり、入札価格はdouble形式です。したがって、double型の値を文字列型の値に変換する必要があります。そうしないと、コンパイル時に「implicit conversion from 'number' to 'string」という警告が表示されます。

警告説明

この時点で、以下のようにdoubleの値を直接文字列に型キャストすることもできますが、これは慎重に使うべきで、通常は推奨されません。

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, (string)Bid());

MQL5の型キャストには、数値を文字列に変換するのと同じように、いくつかのニュアンスがあります。最も一般的なものの1つが精度の低下です。たとえば、買値が浮動小数点数であり、価格が11.77900の場合、最後の2つのゼロが無視され、最終的な出力値は11.779となります。技術的にはこれらの値に論理的な違いはありませんが、視覚的には一方が5桁、もう一方が3桁といった数学的な違いがあります。以下に、その例を示します。

型キャストのニュアンス

これまで見てきたように、型キャストは警告を取り除きますが、精度が重要な場合に使うには最良の方法ではありません。したがって、別の機能が必要になります。変換にはMQL5内蔵のDoubleToString関数を使用します。この関数は、浮動小数点を持つ数値をテキスト文字列に変換するために使用されます。この関数は、ターゲットの浮動小数点数と精度形式の2つの入力パラメータを取ります。この場合、買値を目標値とし、精度形式には_Digits(小数点以下の桁数を格納する変数)を使用します。これにより、現在のチャートの銘柄の価格精度を定義します。Digits()関数も使用できます。この関数は、0から8の範囲内で桁数を指定し、指定しない場合はデフォルトで8桁になります。例えば、銘柄がGOLD (XAUUSD)の場合、価格精度は3桁です。そのため、精度として3を指定しますが、通貨ペアに応じて自動的に桁数を取得する関数を使用することで、コードをより一般化し、適応性を持たせることができます。もし固定の小数点以下の桁数を指定したい場合は、静的な値を使用します。以下は、この買値設定の最終コードです。 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));

これで正しい変換ロジックが完成したので、MQL5の優れた機能のおかげで、次のような結果が得られます。

正しい買値表示

買いボタンの提示価格とスプレッドを設定する場合も、同じロジックが適用されます。以下はそのコードです。 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, (string)Spread());

以前、精度を維持する観点からこのアプローチを批判しましたが、スプレッドに関しては、文字列値への型変換を直接行っていることに気づかれたかもしれません。ここでは、スプレッドが整数型データであるため、精度は重要ではありません。どちらにしても形式は正しくなります。しかし、IntegerToString関数を使用して変換することも可能で、その場合でも同じ値になります。

    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));

この関数は3つの引数を取りますが、精度の形式を指定していないので、目標値だけで十分です。違いは出ます。GIF (Graphic Interchange Format)では、現在こんな感じです。

価格GIF

イベントハンドラで必要なことは以上です。価格の更新を担当するソースコードの全文は以下の通りです。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));
}
//+------------------------------------------------------------------+

これで、最初の自動化コンポーネントが完成しました。簡単だったでしょう。次にGUIパネルの他のコンポーネントに進みます。残りの要素の自動化はOnChartEvent関数ハンドラ内でおこなわれるので、その入力パラメータと関数について深く見てみましょう。

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

        ...

}

この関数の目的は、ユーザーまたはMQL5プログラムによるチャートの変更を処理することです。したがって、マウスを動かしたり、ボタンフィールドを編集したり、ラベルやボタンをクリックしたりといったユーザーのインタラクションは、このイベントハンドラによって捕捉され、処理されます。その論点を分解して解釈してみましょう。

  • id:このパラメータはイベント IDを表し、11の定義済みイベントタイプのうちの1つに対応します。これらのイベントには、キーの押下、マウスの移動、オブジェクトの作成、チャートの変更、カスタムイベントなどが含まれます。カスタムイベントでは、CHARTEVENT_CUSTOMからCHARTEVENT_CUSTOM_LASTまでのIDを使用できます。イベントの種類は以下の11種類です。

チャートイベントタイプ

  • lparam:long型のイベントパラメータ。その値は、処理される特定のイベントに依存します。例えば、キー押下イベント中のキーコードを表すことができます。
  • dparam:double型のイベントパラメータ。lparamと同様、その値はイベントタイプによって異なります。例えば、マウスの移動イベント中、マウスカーソルの位置を伝えます。
  • sparam:文字列型のイベントパラメータ。繰り返しになりますが、その意味はイベントによって異なります。例えば、オブジェクトの作成時に、新しく作成されたオブジェクトの名前を保持することができます。

よりわかりやすく示すために、関数内部で、操作ログへの4つの引数すべてを出力してみましょう。

// Print the 4 function parameters    
Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

この関数は、チャートイベントID、long型のイベント値、double型のイベント値、string型の値を表示します。参照しやすいように次のGIFを見てみましょう。

イベントチャートGIF

GIFからすべてが明らかになったはずです。次に、GUIパネル要素のチャートクリックイベントのキャプチャに移ります。したがって、IDはCHARTEVENT_OBJECT_CLICKとなります。 

   //Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

   if (id==CHARTEVENT_OBJECT_CLICK){
        
        ...

   }

無関係な情報で操作ログをスパムしたくないので、まず前の行をコメントアウトします。2つのスラッシュ(//)はシングルラインコメントと呼ばれ、行頭から行末までコードをコメントアウトします。コメントは、特に実行中にコンピュータによって無視されます。オブジェクトがクリックされたかどうかを確認するために、if文を使用します。これは、チャートイベントIDをオブジェクトクリックの列挙値と比較することで実現します。オブジェクトがクリックされた場合、引数を表示してどのような値が得られるか確認しましょう。以下のコードを使用します。 

   if (id==CHARTEVENT_OBJECT_CLICK){
      Print("ID = ",id,", LM = ",lparam,", DM = ",dparam,", SPARAM = ",sparam);

      ...

   }

printout関数では、LPARAMをLPに、DPARAMをDPに変更するだけで、チャートイベントIDとクリックされたオブジェクトの名前だけに集中できるようになります。以下はそのロジックの図解です。

オブジェクトクリックGIF

1番目のコンポーネントの自動化機能は、救急車のアイコンがクリックされた場合にGUIパネルを終了することです。上のGIFから、オブジェクトがクリックされると、そのオブジェクトの名前がstring-event型変数に格納されることがわかります。したがって、この変数から、クリックされたオブジェクトの名前を取得し、それが目的のオブジェクトであるかどうかを確認し、もしそうであれば、アクションを起こすことができます。 

      //--- if icon car is clicked, destroy the panel
      if (sparam==ICON_CAR){
         Print("BTN CAR CLICKED. DESTROY PANEL NOW");
         destroyPanel();
         ChartRedraw(0);
      }

車のアイコンがクリックされた場合を確認するために、もう1つのif文を使用します。この場合、アイコンがクリックされたことを通知し、パネルの破棄が適切なアイコンによって実行できることを伝えます。その後、destroyPanel関数を呼び出し、パネルのすべての要素を削除します。この関数は、前回の記事(第1回)で私用したので、すでにおなじみのはずです。最後に、ChartRedraw関数を呼び出します。この関数は、指定したチャートを強制的に再描画するために使用されます。チャートプロパティやオブジェクト(指標、ライン、シェイプなど)をプログラムで変更した場合、その変更がすぐにチャートに反映されないことがあります。これを呼び出すことで、チャートが確実に更新され、最新の変更が表示されます。視覚的に表現すると、以下のようになります。

パネル破壊GIF

ロジックはいたってシンプルであることがおわかりいただけると思います。他のオブジェクトのクリックでも同じ方法が採用されます。次に、クローズボタンのラベルがクリックされたときのイベントに進んでみましょう。この場合、すべての未決済ポジションをクローズし、未決注文をすべて削除する必要があります。これにより、成行注文がないことを確認します。クローズボタンがクリックされたかどうかを確認するには、else-if文が必要です。

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);

          ...

      }

ここでは、イベントインスタンスに少し手を加えます。ボタンがクリックされたら、ボタンの色を変えて、ボタンがクリックされたことを示し、成行注文の決済処理を開始させます。完全に閉じたら、ボタンの色をデフォルトの色プロパティにリセットする必要があります。ボタンラベルの元の色を取得するために、originalColorという名前の長いデータ型変数を宣言し、その中にボタンのデフォルトの色を格納します。ボタンの色を取得するには、ObjectGetInteger関数を使用し、チャートID、ボタン名、ボタンのプロパティ(ここではcolor)を渡します。元の色を保存した後、元の値がすでに確保されているため、ボタンラベルの色を変更できます。ObjectSetIntegerを使用して、オブジェクトの色を赤に設定します。その状態で、注文のクローズプロセスを開始します。

          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }

forループを使用してすべての未決済ポジションを反復処理して、クローズします。すべての未決済ポジションを取得するには、MQL5に組み込まれている関数PositionsTotalを使用します。この関数は、特定の取引口座の未決済ポジション数を返します。次に、PositionGetTicket関数でポジションのインデックスを指定してそのポジションのチケットを取得し、ulongデータ型変数「ticket」に格納します。この関数は指定された位置のチケットを返し、失敗した場合は0を返します。先に進むためには、チケットを持っていることを確認する必要があります。これは、if文を使用してチケットの値が0より大きいことを確認することで実現します。もしそうなら、チケットを持っているということなので、そのチケットで仕事ができるように、引き続きチケットを選択します。チケットの選択に成功すれば、ポジションの情報を取り出すことができます。特定の取引口座にはいくつかのポジションがある可能性があるため、特定の通貨ペアに関連するポジションのみをクローズするようにします。最後に、そのポジションをチケット番号でクローズし、他の未決済ポジションがあれば同様に処理を進めます。

しかし、ポジションをクローズするには、obj_Trade の後にドット演算子を使用します。これをクラスオブジェクトと呼びます。ポジションのクローズ操作を簡単におこなうには、そのプロセスを補助するクラスのインスタンスを含める必要があります。このように、ソースコードの先頭に#includeを使用することで、取引インスタンスをインクルードします。これでCTradeクラスにアクセスできるようになり、それを使用して取引オブジェクトを作成します。取引操作に必要なので、これは非常に重要です。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

プリプロセッサは#include<Trade/Trade.mqh>という行をTrade.mqhというファイルの内容に置き換えます。角括弧は、Trade.mqhファイルが標準ディレクトリ(通常、terminal_installation_directory\MQL5\Include)から取得されることを示します。カレントディレクトリは検索に含まれません。この行はプログラム中のどこにでも配置できますが、通常は、より良いコード構造と参照を容易にするために、すべてのインクルージョンはソースコードの先頭に置かれます。CTradeクラスのobj_Tradeオブジェクトを宣言すると、MQL5開発者のおかげで、そのクラスに含まれるメソッドに簡単にアクセスできるようになります。

CTRADEクラス

未決注文を削除するには、同じ反復ロジックを使用します。

          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }

反復ロジックの大きな違いは、注文の合計を得るためにOrdersTotal関数を使用し、それ以外はすべて注文に関連付けられていることです。すべてのポジションがクローズされ、注文が削除された後、ボタンラベルの色を元の色に戻す必要があります。 

          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);

ObjectSetInteger関数は、チャートID、ボタン名、カラープロパティ、オリジナルカラーを渡して使用します。ここで先行変数が威力を発揮します。オブジェクトの元の色を常に詰め込む必要はありません。すべての未決済ポジションをクローズし、すべての未決済注文を削除するコードは以下のとおりです。

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

パネルにロジックを追加するたびに、コンパイルしてコードを実行し、すべてが予想通りに動作していることを確認してから、別の制御ロジックに移行することを常にお勧めします。以下がこれまでの成果です。

クローズGIF

これですべてのポジションと注文を正常にクローズできます。ポジションがクローズされている間にクローズボタンがクリックされると、ボタンのラベルの色はすべてがクローズされるまで赤のままで、最後に元の色に戻ることに注目してください。繰り返しますが、EAが現在ゴールド銘柄にアタッチされているため、「AUDUSD」の買いポジションをクローズしていないことにお気づきでしょう。同じロジックを使用して、他のボタンラベルの応答性を設定することができます。 

      else if (sparam == BTN_MARKET) {
          // Button "Market" clicked. Close all positions related to the current chart symbol.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

このコードとクローズボタンのコードの違いは、反復処理でクローズするのでなく、未決済ポジションをすべてクローズすることです。利益が出ているポジションをすべてクローズするには、以下のコードスニペットを使用します。

      else if (sparam == BTN_PROFIT) {
          // Button "Profit" clicked. Close all positions in profit now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN PROFIT NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

このコードスニペットと、すべてのポジションをクローズする前のコードスニペットとの大きな違いは、ポジションの利益がゼロを超えているかどうかを確認するロジックを追加していることです。以下はその具体的なロジックです。

                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }

profit_or_lossという名前のdouble型変数を定義し、その中に選択ポジションの現在の変動損益を格納します。値が0より大きければ、すでに利益が出ているのでポジションをクローズします。同じロジックを損失ボタンに移し、損失が出た場合のみポジションをクローズします。 

      else if (sparam == BTN_LOSS) {
          // Button "Loss" clicked. Close all positions in loss now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN LOSS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss < 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

最後に、未決注文ボタンがクリックされたときに未決注文をクローズするために、注文が反復処理されます。そのコードは以下の通りです。

      else if (sparam == BTN_PENDING) {
          // Button "Pending" clicked. Delete all pending orders related to the current chart symbol.
          Print(sparam + " CLICKED. DELETE ALL PENDING ORDERS NOW");
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

以下はマイルストーンの視覚化です。

すべて閉じるボタン GIF

図のように、パネルのヘッダーボタンがクリックされると反応するようになったことがわかります。次は、取引量ボタンに命を吹き込む番です。ボタンかラベルがクリックされたとき、あるいはドロップダウンアイコンがクリックされたときに、ユーザーが選択できるさまざまなオプションのリストがある別のサブパネルを作成したいのです。以下はそのロジックです。

      else if (sparam == BTN_LOTS || sparam == LABEL_LOTS || sparam == ICON_DROP_DN1) {
          // Button "Lots," label "Lots," or dropdown icon clicked. Create a dropdown list.
          Print(sparam + " CLICKED. CREATE A DROPDOWN LIST");
      
          // Enable the button for dropdown functionality
          ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, true);
      
          // Create the dropdown list
          createDropDown();
      
          // Redraw the chart to reflect the changes
          ChartRedraw(0);
      }

ボタンがクリックされたことを通知し、ボタンの状態をtrueに設定します。これにより、ボタンがクリックされたことが示され、ボタンが暗くなります。これが完了したら、カスタムcreateDropDown関数を呼び出してドロップダウンリストを作成します。そのコード スニペットは、最初の記事で以前に提供されました。作成が完了したら、ユーザーはオプションの中から選択しなければなりません。したがって、オプションがクリックされて選択された場合、オプションパネルのドロップダウンリストを破壊するだけでなく、ボタンのラベルをユーザーの選択にキャプチャして設定しなければなりません。以下のコードスニペットを使用してこれを実現します。

      else if (sparam == LABEL_OPT1) {
          // Label "Lots" clicked.
          Print("LABEL LOTS CLICKED");
      
          // Get the text from LABEL_OPT1
          string text = ObjectGetString(0, LABEL_OPT1, OBJPROP_TEXT);
      
          // Get the state of the button (enabled or disabled)
          bool btnState = ObjectGetInteger(0, BTN_LOTS, OBJPROP_STATE);
      
          // Set the text of LABEL_LOTS to match LABEL_OPT1
          ObjectSetString(0, LABEL_LOTS, OBJPROP_TEXT, text);
      
          // Destroy the dropdown list
          destroyDropDown();
      
          // If the button was previously enabled, disable it
          if (btnState == true) {
              ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, false);
          }
      
          // Redraw the chart
          ChartRedraw(0);
      }

まず、最初のオプションがクリックされたかどうかを確認します。もしそうなら、選択されたオプションのテキスト値を取得し、それを取引量ボタンのテキスト値に設定します。ユーザーの選択した選択肢をボタンの状態に設定した後、作成されたsup-panelを取り除くために、カスタムdestroyDropDown関数を使用します。

//+------------------------------------------------------------------+
//|    Function to destroy dropdown                                  |
//+------------------------------------------------------------------+

void destroyDropDown(){
   ObjectDelete(0,BTN_DROP_DN);
   ObjectDelete(0,LABEL_OPT1);
   ObjectDelete(0,LABEL_OPT2);
   ObjectDelete(0,LABEL_OPT3);
   ObjectDelete(0,ICON_DRAG);
   ChartRedraw(0);
}

最後に、ボタンの状態が以前に有効になっていたかどうか、つまりクリックされた状態であったかどうかを確認し、有効であればstateプロパティをfalseに設定して無効にします。オプションでも同じロジックが使用されています。そのコードスニペットを以下に示します。

      else if (sparam==LABEL_OPT2){
         Print("LABEL RISK % CLICKED");
         string text = ObjectGetString(0,LABEL_OPT2,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }
      else if (sparam==LABEL_OPT3){
         Print("LABEL MONEY CLICKED");
         string text = ObjectGetString(0,LABEL_OPT3,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }

サイドボタン、つまり増減ボタンがクリックされたとき、それぞれの編集フィールドの値を増減させることで反応するようにする必要があります。手始めに、取引量増加ボタンについて見てみましょう。

      else if (sparam == BTN_P1) {
          // Button "P1" clicked. Increase trading volume.
          Print(sparam + " CLICKED. INCREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Increment the trading volume by 0.01
          trade_lots += 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

取引量増加ボタンがクリックされたことを通知し、現在値を取得してロットフィールドの値を増加させる準備をします。検索された取引量に、増分ステップ値として0.01を加えます。演算子「+=」は、処理を簡単にするために使用されます。これは通常、ロットサイズを0.01増加させるものです。これは(trade_lots = trade_lots + 0.01)と言っているのと同じです。結果がロットのフィールドに渡されます。double値は文字列に変換され、2桁の精度が適用されます。減少ボタンも同じ理屈で、値から0.01を引くだけです。 

      else if (sparam == BTN_M1) {
          // Button "M1" clicked. Decrease trading volume.
          Print(sparam + " CLICKED. DECREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Decrease the trading volume by 0.01
          trade_lots -= 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

同じ理屈が他の同様のボタンにも当てはまります。

      else if (sparam==BTN_P2){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points+=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M2){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points-=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      
      else if (sparam==BTN_P3){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points+=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M3){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points-=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }

ここでは、ストップロスとテイクプロフィットの値に10ポイントを指定します。私たちが正しい道を歩んでいることを確認するために、以下に結果をまとめ、可視化しました。

ドロップダウン INC DEC ボタン

ここまでは順調でした。残りのボタンは売りボタンと買いボタンです。それらのロジックも非常にシンプルで、先のロジックに沿ったものです。売りボタンについては、次のようなロジックになっています。

      else if (sparam==BTN_SELL){
         Print("BTN SELL CLICKED");
         ObjectSetInteger(0,BTN_SELL,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double sell_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sell_sl = Ask()+sell_sl*_Point;
         sell_sl = NormalizeDouble(sell_sl,_Digits);
         double sell_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         sell_tp = Ask()-sell_tp*_Point;
         sell_tp = NormalizeDouble(sell_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",sell_sl,", TP = ",sell_tp);
         obj_Trade.Sell(trade_lots,_Symbol,Bid(),sell_sl,sell_tp);
         ChartRedraw();
      }

クリックイベントが売りボタンに対しての場合に通知し、ボタンの状態をfalseに設定します。売りポジションを建てるには、取引量、ストップロスポイント、テイクプロフィットポイントが必要です。これらの値を取得し、取り出しやすいように指定の変数に格納します。ストップロスを計算するには、ストップロスポイントを取得し、_Pointを掛けて互換性のある通貨ペアのポイント形式に変換し、結果の値を現在の提示価格に加えます。その後、正確さと精度のために、double型の出力値を銘柄の桁数に正規化します。テイクプロフィットレベルも同様におこない、最後に売りポジションを建て、取引ロット、売値としての買気配、ストップロス、テイクプロフィットを渡します。同じロジックが買いポジションにも適用され、そのロジックは以下のようになります。

      else if (sparam==BTN_BUY){
         Print("BTN BUY CLICKED");
         ObjectSetInteger(0,BTN_BUY,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double buy_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         buy_sl = Bid()-buy_sl*_Point;
         buy_sl = NormalizeDouble(buy_sl,_Digits);
         double buy_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         buy_tp = Bid()+buy_tp*_Point;
         buy_tp = NormalizeDouble(buy_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",buy_sl,", TP = ",buy_tp);
         obj_Trade.Buy(trade_lots,_Symbol,Ask(),buy_sl,buy_tp);
         ChartRedraw();
      }

テスト結果は以下の通りです。

売買GIF

ここまでは、すべてが予想通りに進んでいます。ユーザーは、増減ボタンを使わず、編集ボタンフィールドの編集オプションを直接利用することもできます。その過程で、編集中に予期せぬミスが起こり、操作が無視されることがあります。例えば、ユーザーがロットサイズを「0.Q7」 と入力した場合、厳密には、この値は文字Qを含んでいるため、完全な数字ではありません。その結果、ロットサイズ以下の取引はおこなわれません。したがって、値が常に有効であることを確認し、そうでない場合は、エラーを修正するよう促すようにします。そのために、別のチャートイベントID CHARTEVENT_OBJECT_ENDEDITを使用します。 

   else if (id==CHARTEVENT_OBJECT_ENDEDIT){
      if (sparam==EDIT_LOTS){
         Print(sparam+" WAS JUST EDITED. CHECK FOR ANY UNFORESEEN ERRORS");
         string user_lots = ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);

         ...   

      }
   }

まず、チャートイベントIDが編集フィールドの編集終了であるかどうかを確認します。もしそうであれば、編集フィールドが取引量ボタンであるかどうかを確認し、もしそうであれば通知し、潜在的な予期せぬエラーをさらに分析するためにユーザー入力値を取得します。入力はuser_lotsという文字列変数に格納されます。分析のためには、ロットサイズを複数の部分に分割する必要があります。その区切りは、ピリオド (.) 文字(フルストップ、ポイント、ドットとも呼ばれます)によって定義されます。

         string lots_Parts_Array[];
         int splitCounts = StringSplit(user_lots,'.',lots_Parts_Array);//rep '.' = 'a' 
      
         Print("User lots split counts = ",splitCounts);ArrayPrint(lots_Parts_Array,0,"<&> ");

分割されたパーツの動的な保存配列をlots_Parts_Arrayという文字列データ型の変数として定義します。次に、3つの引数を取るStringSplit関数を使用してユーザー入力を分割します。分割対象の文字列値、この場合はユーザーロットサイズを入力し、ピリオドを区切り文字として与え、最後に結果の部分文字列を格納する配列を与えます。この関数は、ストレージ配列内の部分文字列の数を返します。指定された区切り文字が渡された文字列内に見つからない場合は、1つのソース文字列のみが配列に配置されます。これらの分割カウントは分割カウント変数に格納されます。最後に、分割カウントの結果と配列の値、つまり結果の部分文字列を表示します。ロットサイズを0.05に編集すると、こうなります。

ロット分割の編集

入力値が有効であるためには、ピリオド区切りが必要であり、その結果、2つの分割カウントになるはずです。もしそうなら、その入力には1期間の区切り文字があることになります。

         if (splitCounts == 2){

            ...

         }

分割カウントが1に等しい場合、入力にピリオドがないことを示すため、入力を受け入れることはできません。この場合、エラーを通知し、isInputValidというブール変数にfalseをセットします。

         else if (splitCounts == 1){
            Print("ERROR: YOUR INPUT MUST CONTAIN DECIMAL POINTS");
            isInputValid = false;
         }

どちらの条件も満たしていない場合は、入力に1ピリオド以上の区切りがあることを意味します。これは誤りであるため、エラーを通知して入力有効フラグをfalseに設定します。

         else {
            Print("ERROR: YOU CAN NOT HAVE MORE THAN ONE DECIMAL POINT IN INPUT");
            isInputValid = false;
         }

有効でない値を2ピリオド区切りで入力すると、これがEAの操作ログに出力されます。

入力2期間

入力に数字以外の文字が含まれていないか確認するには、2つの分割をそれぞれループして、各文字を個別に評価する必要があります。これを簡単に実現するには、forループが必要になります。

            if (StringLen(lots_Parts_Array[0]) > 0){
            
               //
... 

            }

まず、ストレージ配列のインデックス0にある最初の文字列が空でないことを確認します。StringLen関数は、文字列中の銘柄の数を取得するために使用されます。文字列内の記号の数が0以下であれば、その部分文字列は空であり、その入力値はすでに無効であることを意味します。

            else {
               Print("ERROR: PART 1 (LEFT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

エラーの視覚化のために、区切り文字の左部分を空白にした場合の結果を以下に示します。

左側が空

数字以外の文字を確認するために、以下のようにforループを利用します。

               string split = lots_Parts_Array[0];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

splitという文字列変数を定義し、そこに最初の部分文字列を格納します。次に、部分文字列内のすべての文字を反復処理します。選択した文字については、StringGetCharacter関数を使用して文字コードを取得します。この関数は、文字列の指定された位置にある銘柄の値を返し、その銘柄 コードをsymbol_codeという名前のunsigned short変数に格納します。実際の記号文字を取得するには、文字列の部分文字列関数を使用します。最後に、if文を使用して、結果のコードが数字コードの中にあるかどうかを確認し、そうでなければ、数字以外の文字があることを意味します。そこで、エラーを通知し、入力有効フラグをfalseに設定し、ループから早々に抜け出します。そうでない場合は、文字がすべて数値であることを意味し、入力の妥当性は初期化されたままtrueとなります。

         bool isInputValid = true;

48から57までの数字範囲が数字記号コード範囲とみなされていることにお気づきでしょうか。では、その理由を見てみましょう。ASCII表あるように、これらの数字記号は、記号0の48から始まり、記号9の57まで、10進数で表されています。

銘柄コード1

続きは以下の通りです。

銘柄コード2

同じロジックが、分割文字列の2番目の部分、つまり区切り文字の右側の部分文字列にも適用されます。ソースコードは以下の通りです。

            if (StringLen(lots_Parts_Array[1]) > 0){
               string split = lots_Parts_Array[1];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

            }
            else {
               Print("ERROR: PART 2 (RIGHT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

数字と数字以外の記号を区別できることを確認するために、イラストを使用します。 

非数値入力

コードが65の大文字「A」を追加すると、入力が無効であることを示すエラーが返されることがわかります。この例では、銘柄コードが提供された画像で簡単に参照できるため、「A」を使用しましたが、他の記号かもしれません。ここでもう一度、入力有効フラグを使用して、問題の編集フィールドに有効なテキスト値を設定します。 

         if (isInputValid == true){
            Print("SUCCESS: INPUT IS VALID.");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,user_lots);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrBlack);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrWhite);
            ChartRedraw(0);
         }

入力妥当性フラグがtrueの場合、成功したことを通知し、テキスト値を元のユーザー入力として設定します。ここでもテキストの色を黒に、ボタンの背景色を白に設定しました。これらは通常、編集フィールドのオリジナルのプロパティです。出力がfalseなら、ユーザー入力値に誤りがあったことを意味し、取引操作に使用することはできません。

         else if (isInputValid == false){
            Print("ERROR: INPUT IS INVALID. ENTER A VALID INPUT!");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,"Error");
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrWhite);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrRed);
            ChartRedraw(0);
         }

そこで、エラーを通知し、テキスト値を「Error」に設定します。ユーザーの最終的な注意を引くために、文字色を白に、背景色を赤に設定しました。この色の組み合わせは、ユーザーがエラーの表示に気づくような、叫び声のような色です。コンパイルすると、次のような結果が得られます。

ユーザー入力GIF

ここまでで、ほとんどのパネル部品の自動化が完了しました。唯一考慮されていないのは、ドロップダウンリストの動きと、ボタン上でのマウスのホバー効果だけです。これらすべては、チャート上でマウスが動いたときに考慮される必要があるため、CHARTEVENT_MOUSE_MOVEイベントIDが考慮されます。マウスの動きを追跡するには、EAの初期化インスタンスでチャート上のマウス移動検出ロジックを有効にする必要があります。 

   //--- enable CHART_EVENT_MOUSE_MOVE detection
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);

まず、最も簡単なものから始めましょう。マウス検出を有効にした後、チャート内でマウスが移動したときにイベントを取得します。 

   else if (id==CHARTEVENT_MOUSE_MOVE){

      ...

   }

チャート内のマウスの位置を検出するには、その座標、つまりx軸とy軸にそれぞれ沿った位置と、その状態、つまり動いているときと静止しているときの状態を取得する必要があります。

      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)

ここでは、X軸に沿った、あるいは日付と時間のスケールに沿ったマウスの距離を格納するために、整数データ型の変数mouse_Xを宣言します。ここでも、double型パラメータを取得し、その値をmouse_Yパラメータに格納し、最後に文字列パラメータをmouse_State変数に格納します。最後に整数に型キャストします。ターゲット要素の初期座標が必要なので、以下のコードスニペットで定義します。

      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      

それぞれのボタンの距離とサイズを取得し、それぞれの整数変数に格納します。型キャストの形式は、値を整数形式に変換するために使用されます。問題のボタンに関するマウス座標を追跡するために、ロジックを保持するための変数が必要です。

      static bool prevMouseInside = false;
      bool isMouseInside = false;

static prevMouseInsideブール変数は、マウスが以前にボタン領域内にあったかどうかを追跡するために宣言されています。isMouseInsideブール変数は、ボタンに関する現在のマウスの状態を保存し、すべての変数はfalseフラグに初期化されます。マウスがボタンエリア内にあるかどうかを判断するには、条件文を使用します。

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }

条件確認は、マウスカーソルが現在ボタン領域内にあるかどうかを判定します。もしそうなら、isMouseInsideはtrueにセットされ、マウスがカーソルの内側にあることを示します。技術的には、マウスカーソルがボタン領域内にあるとみなされるためには、4つの条件が満たされなければなりません。さらに理解を深めるために、それぞれの条件を分解してみましょう。

  • mouse_X >= XDistance_Hover_Btn:マウスのX座標(mouse_X)がボタンの左境界(XDistance_Hover_Btn)以上かどうかを確認します。
  • mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn:マウスのX座標がボタンの右境界(XDistance_Hover_Btnとボタンの幅XSize_Hover_Btnの合計)以下であるかどうかを確認します。
  • mouse_Y >= YDistance_Hover_Btn:同様に、マウスのY座標(mouse_Y)がボタンの上部境界(YDistance_Hover_Btn)以上かどうかを確認します。
  • mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn:マウスのY座標がボタンの下辺(YDistance_Hover_Btnとボタンの高さYSize_Hover_Btnの合計)以下かどうかを確認します。

すべての条件が満たされたら、isMouseInside変数をtrueに設定します。その結果得られた値で、マウスがボタンの中にあるかどうかを確認することができます。以下のロジックが実装されています。 

      if (isMouseInside != prevMouseInside) {

ここでは、マウスの現在の状態(ボタン領域の内側または外側)が、前回の確認から変化したかどうかを確認します。これにより、ボタンに対するマウスの位置が変化したときにのみ、後続のアクションが実行されるようになります。もう一度、条件が満たされたかどうかを確認する必要があります。

         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }

boolean変数がtrueの場合、マウスがボタン領域に入ったことを意味します。print文でこれについて通知します。次に、ボタンのラベルの色と背景を変更します。もしこの変数がfalseであれば、ボタンの領域内にあったマウスカーソルがそこから離れたことを意味します。そこで、色をデフォルトに戻した。以下はそのロジックを担当するコードスニペットです。

         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }

ボタンのプロパティを変更すると、ChartRedraw関数が呼び出され、チャート表示が更新され、更新されたボタンの外観が反映されます。 最後に、prevMouseInside 変数がマウスの現在の状態(isMouseInside)に合わせて更新されます。これにより、次にイベントがトリガーされたとき、プログラムは新しい状態と前の状態を比較することができます。

         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;

ボタンのホバー効果を作成するための完全なコードは以下の通りです:

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
   }

まとめるとこうなります。

ホバー効果GIF

それは素晴らしいことです。マウスカーソルの動きをトラッキングするだけでなく、オブジェクトやコンポーネントを一緒に移動させるのです。ここでも、マウスがクリックされたことを検出するためのstatic整数変数と、マウスカーソルの移動状態を保存するためのブーリアン変数を宣言します。これは以下のコードスニペットで実現できます。

      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;

次に、オブジェクトのサイズと距離を保持する変数を初期化する必要があります。 

      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon

ストレージ変数を初期化するために、静的データ型変数を宣言し、上記のように0に初期化します。パネルコンポーネントが動いているときに参照するために、それぞれのサイズと距離を保存する必要があるため、静的に宣言されています。繰り返しになりますが、初期要素の距離が必要で、それは以下のコードスニペットで実現できます。 

      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);

ここでは、ObjectGetInteger関数を使用して、カーソルと一緒に移動する要素の距離を取得します。ただし、パネルの動きに関わるアイコンのサイズも取得している点に注目してください。ホバー効果のロジックでおこなったように、このサイズが必要なのは、マウスカーソルがアイコンの領域内でクリックされたことを検知し、移動を開始できるようにするためです。次に、最初のマウスクリック情報を取得し、移動するオブジェクトの距離を保存する必要があります。

if (prevMouseClickState == false && mouse_State == 1) {
    // Check if the left mouse button was clicked and the mouse is in the pressed state

    // Initialize variables to store initial distances and sizes of objects
    mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
    mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click

    // Store distances for specific objects
    mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
    mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)

    mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of a specific button (BTN_DROP_DN)
    mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;

    mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of a label (LABEL_OPT1)
    mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;

    mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of another label (LABEL_OPT2)
    mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;

    mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of yet another label (LABEL_OPT3)
    mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;

    // Check if the mouse is within the drag icon area
    if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
        mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
        movingState = true; // Set the moving state to true
    }
}

条件文を使用して2つの条件を確認します。1つ目は「prevMouseClickState == false」で、これはマウスの左ボタンが以前にクリックされていないことを確認するものです。2つ目は「mouse_State == 1」で、これは現在マウスのボタンが押されている状態かどうかを確認します。この2つの条件が満たされた場合、マウスのX座標とY座標、そしてオブジェクトとの距離が保存されます。最後に、マウスがドラッグアイコンの領域内にあるかどうかを確認し、もしそうであれば移動状態をtrueに設定します。これらの4つの条件をより分かりやすくするために、各条件を順に分解して説明します。

  • mouse_X >= XDistance_Drag_Icon:マウスのX座標(mouse_X)がドラッグアイコン領域の左境界(XDistance_Drag_Icon)以上であることを確認します。
  • mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon:同様に、X座標がドラッグアイコン領域の右境界(XDistance_Drag_Iconとアイコンの幅XSize_Drag_Iconの和)以下であることを保証します。
  • mouse_Y >= YDistance_Drag_Icon:マウスのY座標(mouse_Y)がドラッグアイコン領域の上端(YDistance_Drag_Icon)以上かどうかを確認します。
  • mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon:同様に、Y座標がドラッグアイコン領域の下側の境界(YDistance_Drag_Iconとアイコンの高さYSize_Drag_Iconの合計)以下であることを確認します。

4つの条件がすべて満たされた場合(すなわち、マウスが定義されたドラッグアイコン領域内にある場合)、変数 movingStateをtrueに設定します。ここまでで、移動状態がtrueであれば、指定されたオブジェクトを移動させます。

      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ...

         ChartRedraw(0);
      }

ここでは、ChartSetInteger関数を使用して、チャートのスクロールフラグを無効にしています。これにより、マウスを動かしてもチャートが水平方向にスクロールしないようになります。したがって、マウスカーソルだけが指定されたオブジェクトと一緒に移動します。最後に、現在のマウス座標に関する新しいオブジェクトの距離を設定し、変更を有効にするためにチャートを再描画します。一言で言えば、こういうことです。

ドラッグアイコンGIF

これでアイコンをドラッグできることがわかります。しかし、他のパネルコンポーネントと一緒にドラッグする必要もあります。したがって、同じロジックが適用されます。 

         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

他の要素のドラッグロジックを追加することで、ドラッグアイコンが動いている間、他のパネルコンポーネントも動くようになります。コンパイルするとこうなります。

ドラッグアイコン

うまくいきました。すべてのパネルコンポーネントがマウスカーソルと一緒に動くのがわかります。ただ、ちょっとした不具合があるので、対処する必要があります。マウスを離しても(つまり、押されたモードではない)、カーソルの動きに合わせてコンポーネントが動き続けます。パネルを移動状態から解放するには、マウスが押されなかった場合に状態をfalseに設定する必要があります。 

      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }

マウスの状態が0であれば、これはマウスの左ボタンが離されていることを意味します。その場合、移動状態をfalseに設定し、パネルの要素をこれ以上移動させないことを示します。続いて、チャートのスクロールイベントを有効にするために、このフラグをtrueに設定します。最後に、前回のマウス状態を現在のマウス状態に更新します。

      prevMouseClickState = mouse_State;

ホバー効果とパネルの移動の自動化を担当する最終的なソースコードは以下の通りです。

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
      
      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;
      
      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon
            
            
      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);
            
      if (prevMouseClickState == false && mouse_State == 1) {
          // Check if the left mouse button was clicked and the mouse is in the pressed state
      
          // Initialize variables to store initial distances and sizes of objects
          mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
          mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click
      
          // Store distances for specific objects
          mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
          mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)
      
          mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of BTN_DROP_DN
          mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;
      
          mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of LABEL_OPT1
          mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;
      
          mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of LABEL_OPT2
          mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;
      
          mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of LABEL_OPT3
          mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;
      
          // Check if the mouse is within the drag icon area
          if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
              mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
              movingState = true; // Set the moving state to true
          }
      }
            
      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

         ChartRedraw(0);
      }
      
      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }
      prevMouseClickState = mouse_State;
   }

一言で言えば、これが私たちが成し遂げたことです。

ファイナルGIF

これは素晴らしかったです。私たちはGUIパネルに命を吹き込み、パネルがインタラクティブに反応するようになりました。ホバー効果、ボタンクリック、ライブデータ更新があり、マウスの動きに反応します。


結論

結論として、この記事の実装を通して、MetaQuotes Language 5(MQL5)のGUIパネルに動的機能を統合することで、インターフェースがよりインタラクティブで機能的になり、ユーザーエクスペリエンスが大幅に向上することが分かります。ボタンにホバー効果を追加することで、ユーザーの操作に直感的に反応する視覚的に魅力的なインターフェイスを提供できます。また、リアルタイムでの買値と売値の更新により、トレーダーは常に最新の市場情報を把握し、迅速かつ的確な意思決定が可能となります。さらに、クリック操作で売買注文を実行できるボタンや、ポジションおよび注文を即座にクローズできる機能を組み込むことで、取引操作の効率が飛躍的に向上し、トレーダーは市場の変動に対して迅速に対応できます。

移動可能なサブパネルとドロップダウンリストの実装により、インターフェイスにさらなるカスタマイズ性と柔軟性が加わり、トレーダーは自身のワークスペースを自在に整理することができ、全体的な取引効率が向上します。ドロップダウンリストは、メインインターフェースを整理しながら、多彩なオプションにアクセスできる便利な方法を提供し、よりすっきりとした取引環境を作り出します。全体として、これらの機能強化により、MQL5 GUIパネルは現代のトレーダーのニーズに応える堅牢でユーザーフレンドリーなツールへと進化し、最終的にトレーダーの取引体験とパフォーマンスが向上します。トレーダーは、より複雑で魅力的なGUIパネルを作成し、取引体験を向上させるために、図解された知識を使用することができます。この記事が詳しく、客観的に説明され、わかりやすく、学びやすいものであったことを願っています。


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

添付されたファイル |
PythonとMQL5によるポートフォリオ最適化 PythonとMQL5によるポートフォリオ最適化
この記事では、MetaTrader 5を使ったPythonとMQL5による高度なポートフォリオ最適化技術を紹介します。データ分析、資産配分、売買シグナル生成のためのアルゴリズム開発方法を示し、現代の金融管理やリスク軽減におけるデータ主導の意思決定の重要性を強調します。
データサイエンスと機械学習(第26回):時系列予測における究極の戦い - LSTM対GRUニューラルネットワーク データサイエンスと機械学習(第26回):時系列予測における究極の戦い - LSTM対GRUニューラルネットワーク
前回の記事では、データの長期的な依存関係をうまく捉えられないにもかかわらず、利益を上げる戦略を構築できる単純RNNについて説明しました。この記事では、LSTM (Long-Short Term Memory)とGRU (Gated Recurrent Unit)の両方について説明します。この2つは、単純RNNの欠点を克服し、それを凌駕するために紹介されました。
初心者のためのMQL5におけるファンダメンタル分析とテクニカル分析戦略の組み合わせ 初心者のためのMQL5におけるファンダメンタル分析とテクニカル分析戦略の組み合わせ
この記事では、トレンドフォローとファンダメンタル分析の原則を1つのエキスパートアドバイザー(EA)にシームレスに統合し、より強固な取引戦略を構築する方法について説明します。MQL5を活用して、誰でも簡単にカスタマイズされた取引アルゴリズムを作成できることを紹介します。
MetaTrader 5のEMAクロスオーバーに基づくカスケード注文取引戦略 MetaTrader 5のEMAクロスオーバーに基づくカスケード注文取引戦略
この記事は、MetaTrader 5のEMAクロスオーバーに基づく自動化アルゴリズムのデモをガイドしています。価格帯の動作分析からリスク管理まで、MQL5のエキスパートアドバイザー(EA)を示し、MetaTrader 5でテストするためのあらゆる側面に関する詳細情報を含みます。