
インジケーターを便利に扱うためのシンプルなソリューション
インジケーターは、長年にわたりあらゆる取引プラットフォームにおいて不可欠な存在となっています。ほとんどのトレーダーが何らかの形でインジケーターを利用しており、取引システムでは通常、1つのインジケーターだけでなく複数のインジケーターを組み合わせたシステムが使用されます。そのため、インジケーターの設定を簡単かつ迅速に調整できることは、取引における重要な要素となります。
この記事では、チャート上からインジケーターの設定を直接変更できるシンプルなパネルの作成方法と、そのパネルをインジケーターに接続するために必要な変更点について解説します。この記事はMQL5初心者向けに書かれているため、コードの各行について丁寧に説明します。上級者にとっては、特に目新しい情報はないかもしれません。
パネル実装
このWebサイトには、記事やコードベースにさまざまな種類のパネルが多数紹介されています。ですので、既存のコードを活用するという選択肢もあります。世の中には非常に優れたコードが多く存在しており、複雑なパネルを作成できる素晴らしいライブラリもたくさんあります。
しかし私は、そうした汎用性の高いコードは、使いやすさが犠牲になっていると感じています。そこで今回は、インジケーター専用のシンプルなパネルを開発することにしました。これは基本的に、フォントサイズに応じてセルの幅と高さがテキストに合わせて自動調整される、一般的なテーブル形式のパネルです。
パネルの上部には、パネル全体をドラッグできる行を設けます。この行には、インジケーターの名前と、パネルの固定や折りたたみをおこなうためのアイコンを配置します。各テーブルセルは1行のコードで定義されるため、必要に応じて行ごとに異なる幅の列を作成することも可能です。
パネル(インジケーター)名[固定][折りたたむ] |
---|
設定名1 | 入力ボックス |
設定名2 | 入力ボックス |
セルを記述するコードには、オブジェクト名、オブジェクトタイプ、行番号、セルのテキスト、パネル幅に対するセル幅(%)を含めます。パネルの幅とセルの幅は相互に依存しており、1行内のすべてのセルのパーセンテージの合計は100%になる必要があります。
3つのオブジェクトを一列に指定する必要がある場合、3つのオブジェクトの行番号はすべて同じになり、幅は、たとえば30%+30%+40%=100%になります。ほとんどの場合、行を2つの部分に分割するだけで十分です。設定パラメータの名前に50%、入力フィールドに50%を使用します。
すでに述べたように、パネルのコードは可能な限りシンプルになるように設計しました。そのため、オブジェクト指向プログラミング(OOP)は使用しない方針でしたが、完全に排除することはできませんでした。というのも、あるインジケーターから別のインジケーターへ大量のコードをコピーするのは不便だからです。そこで、パネルのコードはクラスとしてインクルードファイル内に定義することにしました。
通常の個別の関数ではなくクラスを使用した主な理由は、デストラクタ内でパネルオブジェクトを削除できるため便利だからです。そうしない場合、インジケーターのOnDeinit()内で削除処理をおこなう必要があり、それは少し複雑になります。
また、オブジェクトを描画するためのメソッドをまとめたObject.mqhインクルードファイルも用意しており、関数へのアクセスをより便利にするためにゲッターとセッターも実装しています。ゲッターやセッターについての詳細な説明は省略しますが、必要であればご自身で調べてみてください。
パネルのアイデアは、記事1および記事2から一部を参考にしています。
この記事の最後には、ここで説明したすべてのコードファイルが添付されています。まずはそれらをダウンロードし、各ファイルを対応するフォルダに配置してから、コードの学習を始めることをおすすめします。Object.mqhファイル用には「include」フォルダ内に「Object」フォルダを作成し、Panel.mqh ファイル用には同じく「include」フォルダ内に「Panel」フォルダを作成しています。そのため、コード内ではこれらのファイルパスをネストされたフォルダ構成に合わせて記述しています。
まずはObject.mqhファイルをインクルードし、変数の「input」を宣言します。ここでは、パネル、テキスト、ボタン、枠線の色、インジケーターが非表示になったときに使用する補助カラー、フォントサイズ、フォントスタイル、チャートの端からのパネルの余白などを指定するための変数を宣言します。
以下は入力設定です。
//+------------------------------------------------------------------+ #include <Object\\Object.mqh> //+------------------------------------------------------------------+ input group "--- Input Panel ---" input int shiftX = 3; // Panel offset along the X axis input int shiftY = 80; // Panel offset along the Y axis input bool NoPanel = false; // No panel input int fontSize = 9; // Font size input string fontType = "Arial"; /* Font style*/ //"Arial", "Consolas" input string PanelHiddenShown = "❐"; // Panel hidden/displayed input string PanelPin = "∇"; /* Pin the panel*/ // ⮂ ↕ ↔ ➽ 🖈 ∇ input string PanelUnpin = "_"; // Unpin the panel input color clrTitleBar = C'109,117,171'; // Panel title background color (1) input color clrTitleBar2 = clrGray; // Panel title background color (2) input color clrDashboard = clrDarkGray; // Panel background color input color clrTextDashboard = clrWhite; // Text color on the panel input color clrBorder = clrDarkGray; // Border color input color clrButton1 = C'143,143,171'; // Button background color (1) input color clrButton2 = C'213,155,156'; // Button background color (2) input color clrButton3 = clrGray; // Button background color (3) input color clrTextButton1 = clrBlack; // Button text color (1) input color clrTextButton2 = clrWhite; // Button text color (2) input color clrEdit1 = C'240,240,245'; // Input field background color (1) input color clrEdit2 = clrGray; // Input field background color (2) input color clrTextEdit1 = C'50,50,50'; // Input field text color (1) input color clrTextEdit2 = clrWhite; // Input field text color (2) //+------------------------------------------------------------------+
次はCPanelクラス自体です。
//+------------------------------------------------------------------+ class CPanel { private: enum ENUM_FLAG //flags { FLAG_PANEL_HIDDEN = 1, // panel hidden FLAG_PANEL_SHOWN = 2, // panel displayed FLAG_IND_HIDDEN = 4, // indicator hidden FLAG_IND_SHOWN = 8, // indicator displayed FLAG_PANEL_FIX = 16, // panel pinned FLAG_PANEL_UNPIN = 32 // panel unpinned }; int sizeObject; int widthPanel, heightPanel; int widthLetter, row_height; int _shiftX, _shiftY; long mouseX, mouseY; long chartWidth, chartHeight; string previousMouseState; long mlbDownX, mlbDownY, XDistance, YDistance; string _PanelHiddenShown, _PanelPin, _PanelUnpin; struct Object { string name; string text; ENUM_OBJECT object; int line; int percent; int column; int border; color txtColr; color backClr; color borderClr; }; Object mObject[]; int prefixInd; string Chart_ID; string addedNames[]; long addedXDisDiffrence[], addedYDisDiffrence[]; int WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL); void Add(string name); // save the object name and anchor point void HideShow(bool hide = false); // hide//show void DestroyPanel(); // delete all objects public: CPanel(void); ~CPanel(void); string namePanel; // panel name string indName; // indicator name should match indicator short name string prefix; // prefix for panel object names bool hideObject; // To be used as a flag in indicators where graphical objects need to be hidden int sizeArr; double saveBuffer[]; // array for storing the coordinates of the panel anchor point, panel properties (flag states), and the latest indicator settings enum ENUM_BUTON // flags for allowing button creation { BUTON_1 = 1, BUTON_2 = 2 }; void Init(string name, string indName); void Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);}; void Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0); bool OnEvent(int id, long lparam, double dparam, string sparam); int Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();} bool Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);} void Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1); void ApplySaved(); void HideShowInd(bool hide); }; //+------------------------------------------------------------------+ CPanel::CPanel(void) {} //+------------------------------------------------------------------+ CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();} //+------------------------------------------------------------------+
以下では例を使用してクラスメソッドについて説明します。
例として空のインジケーターを書いてみましょう。
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param; //+------------------------------------------------------------------+ int OnInit() { string short_name = "Ind Pnl(" + (string)param + ")"; mPanel.Init("Ind Pnl", short_name); mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60); mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40); mPanel.Create(0); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
インジケーターを起動すると、チャート上に次のようなパネルが表示されます。
ここで、このインジケーターを例にしてパネルコードを詳しく調べてみましょう。
インジケーター入力パラメータの直後に、パネルクラスを含むファイルをインクルードし、パネルクラスを宣言します。
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param;
通常、クラスはコードの最初に宣言されます。しかし、パネルクラスはインクルードファイル内にあり、そこにはinputも含まれているため、もしそのクラスをinputコードの先頭に書いてしまうと、インジケーターのパラメータがパネルの入力項目の下に表示されてしまいます。これではインジケーターを起動して設定する際に少し不便です。
input変数は定数であるため、変更することはできませんが、input変数のコピーを作成し、それをパネルの入力フィールドから編集することは可能です。
次に、パネルのコードをインジケーターのOnInit()関数に追加します。
ただしその前に、パネルが正しく動作するためには、インジケーターに短い名前をコード内で指定しておく必要があることに注意してください。この名前には、主な入力パラメータを含めるようにします。
string short_name = "Ind Pnl(" + (string)_param + ")";
これは、異なる設定でインジケーターを複数起動できるようにするためです。
インジケーター名には使用できない記号もあることを思い出してください。たとえばパラメータをコロン(:)で区切りたい場合は、代わりにセミコロン(;)を使ったほうがよいでしょう。
パネル名はインジケーター名と同じにしても構いませんが、インジケーターのパラメータを含めない形にしたほうが、扱いやすくなります。
CPanel クラスで最初にインジケーターに追加するメソッドはInit()メソッドです。このメソッドには、パネル名とインジケーター名の2つの名前を引数として渡します。
mPanel.Init("Ind Pnl", short_name);
Init()メソッドが最初におこなうことは、設定でパネルが無効になっていないことを確認することです。
void CPanel::Init(string name, string short_name) { if(NoPanel) return;
次に、変数を初期化します。
namePanel = name; indName = short_name; MovePanel = true; sizeObject = 0; Chart_ID = (string)ChartID(); int lastX = 0, lastY = 0;
チャート上のすべてのMQL5プログラムにマウスの動きとボタンの押下イベント(CHARTEVENT_MOUSE_MOVE)に関するメッセージを送信する権限を設定し、グラフィカルオブジェクトの作成イベント(CHARTEVENT_OBJECT_CREATE)に関するメッセージの送信も許可します。
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
パネルの幅を計算するためには、まずフォントの種類とサイズを設定し、1文字あたりのサイズを取得する必要があります。
これにより、後でそのサイズを使ってテキストのパネル枠からのインデントを計算できるようになります。セルの高さは1.5文字の高さと同じになります。
// set the font type and size TextSetFont(fontType, fontSize * -10); // get the width and height of one character TextGetSize("0", widthLetter, row_height); // calculate the cell height row_height += (int)(row_height / 2);
パネル設定には、パネルの表示/非表示を切り替えるアイコン(❐)、パネルを固定するアイコン(∇)と固定解除するアイコン(_)があります。
アイコンはインターネットで見つけたもので、設定で変更できます。
パネルの端からアイコンを適切な位置に配置するために、記号にスペースを追加します。スペースを追加しないと、アイコン同士が近すぎて表示され、マウスでクリックしづらくなってしまいます。
string space = " "; _PanelHiddenShown = space + PanelHiddenShown + space; _PanelPin = space + PanelPin + space; _PanelUnpin = space + PanelUnpin + space;
パネルはグラフィカルオブジェクトで構成されています。これらのオブジェクトの名前が一意になるように、プレフィックスを作成しましょう。
MathSrand((int)GetMicrosecondCount()); prefixInd = MathRand(); prefix = (string)prefixInd;
チャート上に複数のパネル付きインジケーターがある場合、プレフィックスの生成にGetTickCount()関数を使用することは避けるべきです。というのも、時間枠の切り替えにかかる時間が非常に短いため、ミリ秒単位ではプレフィックスが重複する可能性があるからです。マイクロ秒レベルでの識別が必要になります。
パネルをドラッグする際は、OnChartEvent()関数によってチャート上およびオブジェクト上でのマウス位置が検出されます。このとき、パネルが他のパネルと重なって衝突し、競合が発生する可能性があります。この問題を回避するために、グローバル変数を用意し、パネル上でマウスが押されたときに、その変数にプレフィックスを書き込む「最初の」パネルのみがマウス操作を許可されるようにします。これは「早い者勝ち」の方式です。
初期化時には、この変数に0を書き込みます。変数の値が0の間は、まだ誰も使用していない、つまり「空き」であると見なされます。
GlobalVariableTemp("CPanel"); GlobalVariableSet("CPanel", 0);
パネルを移動、折りたたみ、固定、またはインジケーターのパラメータを変更する場合、これらの変更内容をどこかに保存しておく必要があります。これにより、時間枠を切り替えたり、端末を再起動した際に、パネルやインジケーターを直前の設定状態で再読み込みできるようになります。パネルおよびインジケーターの最新設定を保存するコードは現時点ではインジケーターに組み込んでいませんが、たとえそのコードがなくても、インジケーターはパネル設定の変更を記録するようになっています。そのためには、パネル設定を保持する配列のためにメモリ領域を確保する必要があります。
sizeArr = ArraySize(saveBuffer); if(sizeArr == 0) Resize(0);
インジケーター設定数として0を渡し(Resize(0);)ますが、関数内ではパネル設定を保存するために3つのセルが追加されます。つまり、saveBuffer配列の3つのセルを使用して、チャート上のパネルの位置、パネルの状態(固定/固定解除、折りたたみ/展開)、およびインジケーターの状態(表示/非表示)を記憶します。
続いて、パネルのアンカーポイントの初期座標を定義するコードがあります。重要な点は、パネルがすでにこのチャートに描画されている場合、そのアンカーポイントの位置は、入力設定または保存済みの設定から取得できるということです。もうひとつの方法としては、パネル付きインジケーターを含むテンプレートを使用することも可能です。
ただし、テンプレートの取り扱いはやや複雑であることが分かりました。パネルを含むインジケーターをテンプレートとして保存しても、テンプレート作成時点でのパネルの座標は保存されません。
しかし、インジケーターをチャートに追加し、その状態でテンプレートを保存・適用すると、OBJ_LABELテキストラベルオブジェクトがテンプレートに含まれていることが確認できます。
テンプレートを保存します。
テンプレートを適用します。
テンプレートの作成時にパネルの位置を決定するために、これらのテキストラベルを使用します。
string delPrefix = ""; int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL); for(int i = 0; i < total; i++) { string nameObject = ObjectName(0, i, 0, OBJ_LABEL); if(StringFind(nameObject, "TitleText " + indName) >= 0) // if the template contains objects with the name of this indicator { lastX = (int)GetXDistance(nameObject);// define the X coordinates of the panel in the template lastY = (int)GetYDistance(nameObject);// define the Y coordinates of the panel in the template StringReplace(nameObject, "TitleText " + indName, ""); // remember the object prefix for its subsequent deletion delPrefix = nameObject; } }
lastXおよびlastY変数は、オブジェクトのアンカーポイントの座標を設定します。このアンカーポイントは、プレフィックスの後にインジケーター名(パネル名のテキスト座標)が付いた名前のテキストラベルです。
なお、パネル名はインジケーター名と異なる場合があることに注意してください。目的のテキストが見つかったら、プレフィックスを取得して保存します。
続くコードでは、保存しておいたプレフィックスプレフィックスを使って、テンプレートに保存されていた古いテキストラベルをチャートから削除します。
if(delPrefix != "")// delete obsolete objects saved in the template ObjectsDeleteAll(0, delPrefix);
次に、パネルアンカーポイントに必要なオプションを確認して選択します。
if(lastX != 0 || lastY != 0)// if we use a template { lastX = lastX - widthLetter / 2; lastY = lastY - (int)(row_height / 8); saveBuffer[sizeArr - 1] = _shiftX = lastX; saveBuffer[sizeArr - 2] = _shiftY = lastY; } else// if data from the file is used if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0) { _shiftX = (int)saveBuffer[sizeArr - 1]; _shiftY = (int)saveBuffer[sizeArr - 2]; } else// if this is the first launch of the indicator { saveBuffer[sizeArr - 1] = _shiftX = shiftX; saveBuffer[sizeArr - 2] = _shiftY = shiftY; }
Init()メソッドの最後に、編集の必要がないパネルオブジェクトを構造体の配列に追加します。これらのオブジェクトは、すべてのパネルに共通で変わることはありません。
対象となるのは、2つの矩形、パネル名のテキスト、そしてパネルの表示/非表示 や固定/固定解除をおこなうアイコンです。
Record("TitleBar"); Record("MainDashboardBody"); Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100); Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0); Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);
それでは次のメソッドであるRecord()に移りましょう。
将来のオブジェクトの構造体は、Record()メソッド内で設定されます。 通常、構造体のほとんどのフィールドはデフォルト値で初期化されます。ただし、この関数に渡されるパラメータによって、一部のデフォルト値を変更することが可能であり、たとえばオブジェクトに異なる色を設定することもできます。
//+------------------------------------------------------------------+ void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0) { if(NoPanel) return; int column = WidthHidthCalc(line + 1, text, percent, object); ArrayResize(mObject, sizeObject + 1); mObject[sizeObject].column = column; // column mObject[sizeObject].name = prefix + name; // object name mObject[sizeObject].object = object; // object type mObject[sizeObject].line = line + 1; // line index mObject[sizeObject].text = text; // text (if any) mObject[sizeObject].percent = percent; // percentage of panel width mObject[sizeObject].txtColr = txtColr; // text color mObject[sizeObject].backClr = backClr; // base color mObject[sizeObject].borderClr = borderClr; // border color mObject[sizeObject].border = 0; // offset from the panel edge sizeObject++; } //+------------------------------------------------------------------+
Record()メソッドの冒頭では、WidthHidthCalc()メソッドを呼び出し、ここでパネルの幅と高さが計算されます。
次に、WidthHidthCalc()メソッドについて詳しく見ていきましょう。
このメソッドは、パネルの幅を計算する際に最も幅の広い要素を考慮します。例えば、先述のように、Ind Pnlインジケーターに長い名前を付けた場合、その名前を基に計算がおこなわれます。
変更前:
mPanel.Init("Ind Pnl", short_name);
変更後:
mPanel.Init("Ind Pnl 0000000000000000000", short_name);
結果
あるいは、たとえば、インジケーター設定の名前を変更すると、次のようになります。
変更前:
mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
変更後:
mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);
結果
パネルはテキストのサイズに合わせて自動的に調整されます。パネルの幅と高さのすべての計算は、WidthHidthCalc()関数で実行されます。
まず、セルのテキスト幅を取得します。
ここでは、他のセルと比較して、パネル名と表示/非表示アイコンを使用してテキスト幅を見つけるという、少し異なる方法を適用します。
int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL) { static int lastLine = -1, column = 0; int width, height; if(line == 1) TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); // get the width and height of the text for the line with the panel name else TextGetSize(text, width, height); // get the text width and height
テキストはセルの境界からインデントされる必要があり、このインデントを文字幅の半分に設定します。文字幅はすでにInit()関数内で取得しており、widthLetter変数に設定されています。
両端にインデントを設けるためには、計算されるテキストの幅にもう1文字分の幅を追加する必要があります。さらに、テキストが OBJ_BUTTONの場合は、ボタンの両端にもインデントを確保するために、さらに1文字分の幅を加えます。
これで、インデントを含めたセル内の1行あたりのサイズがわかったので、セル下に指定されたパーセンテージを考慮してパネル全体のサイズを計算できます。
最後に、計算された中で最も大きなパネル幅の値を保存します。今後は、すべてのセルサイズの計算をこの最大パネル幅に基づいておこないます。
double indent = 0; if(object == OBJ_BUTTON) indent += widthLetter; if(text != "" && percent != 0) { // calculate the width of the panel based on the text size and the percentage allocated for this text int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent); if(widthPanel < tempWidth) widthPanel = tempWidth; }
テストインジケーターにおけるパネル幅の計算は、次のようにおこなわれます。
まず、アイコンを含めたタイトルの幅を計算します。「Ind Pnl」+「∇」+「❐」の場合、文字列部分の幅が71ピクセル、そこに記号1つ分(7ピクセル)を加えると、合計で78ピクセルとなります。これがパネル幅の100%に相当します。
次に、セルテキストが「param」であると仮定すると、その文字幅は36ピクセルで、左右のインデント(7ピクセル)を加えて合計43ピクセルになります。このセルにはパネル幅の60%が割り当てられるため、パネル全体の幅は 43 × 100 ÷ 60 = 72 ピクセルとなります。しかしこれは、パネルタイトルに必要な幅よりも小さいため、最終的なパネル幅はタイトルセルに合わせて決定されます。
続いて、列インデックスを設定し、新しい行であればパネルの高さを加算します。
if(lastLine != line)// if this is a new row in the panel, then increase the height of the entire panel { heightPanel = row_height * line; lastLine = line; column = 0; // reset the number of columns in the new row } else column++; // add a new column return column; }
CPanelクラスにある10個のメソッドのうち、2つの動作を詳しく確認しました。
プログラムがパネルのサイズを決定し、オブジェクトのパラメータを構造体配列mObject[]に設定した後、次のメソッドであるCreate()に進みます。このメソッドは、先に取得した寸法情報に基づいてパネルを構築します。
例によって、メソッドの最初ではパネルを表示する必要があるかどうかを確認します。その後、2つの事前定義されたボタンを作成するコードが続きます。1つはインジケーターの表示/非表示を切り替えるボタン、もう1つはインジケーター自体を削除するボタンです。設定されたフラグに応じて、0 - ボタンなし、1 - 表示/非表示ボタンのみ、2 - 削除ボタンのみ、3 - 両方のボタンを作成の動作を選択できます。
これらのボタンがインジケーターのコードではなくパネル内に実装されている理由は、インジケーター側に記述するコードを最小限に抑えるためです。
続いて、変数の初期化をおこないます。このコードは、パネルを本来の目的とは異なる用途、たとえばオブジェクトのパラメータを変更するためのポップアップパネルとして使う場合に必要です。つまり、チャート上の任意のクリック位置にパネルを表示できるようにする必要があります。
void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1) { if(NoPanel) return; if((Button & BUTON_1) == BUTON_1)// if we need to create buttons Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50); if((Button & BUTON_2) == BUTON_2)// if we need to create buttons Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2); ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER; int X = 0, Y = 0, xSize = 0, ySize = 0; if(shiftx != -1 && shifty != -1) { _shiftX = shiftx; _shiftY = shifty; }
このパネルは、情報パネルや取引パネルとしてだけでなく、本パネルのようにチャートオブジェクトの設定用パネルとしても柔軟に対応できます。すべてがシンプルに設計されており、純粋に機能性に重点を置いています。
しかし、話が逸れてしまいました。Create()メソッドの分析を続けましょう。次は、ヘッダーの長方形とパネル本体の長方形の2つの長方形を作成するコードです。
// header rectangle RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName); Add(mObject[0].name);// remember the object's anchor point // panel rectangle RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName); Add(mObject[1].name);
各オブジェクトが作成されると、Add()関数が呼び出され、チャートの左上隅を基準としたオブジェクトの名前と座標が配列に書き込まれます。
//+------------------------------------------------------------------+ void CPanel::Add(string name)// save the object name and anchor point { int size = ArraySize(addedNames); ArrayResize(addedNames, size + 1); ArrayResize(addedXDisDiffrence, size + 1); ArrayResize(addedYDisDiffrence, size + 1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name); } //+------------------------------------------------------------------+
これらの座標配列は、後でパネルを移動する際に使用されます。
それでは、Create()メソッドのコードに戻りましょう。 すべてのオブジェクトは、構造体配列mObject[]に記録された順序でループ処理によって作成されます。 各オブジェクトの座標とサイズを計算したうえで、オブジェクトを作成します。
このパネルは用途が明確に特化されているため、使用するオブジェクトの種類は3種類のみですが、これだけで十分な機能性を備えています。
ヘッダー部分の矩形にテキストを表示する際には、例外的な処理が必要でした。それは、パネルの固定アイコンや折りたたみアイコンのアンカーポイントを、他のパネル要素とは異なるものとして設定するためです。これにより、これらのアイコンはパネルの右上隅を基準に配置され、パネル上での配置が容易になります。
for(int i = 2; i < sizeObject; i++) { // calculate the coordinates of the object anchor point if(mObject[i].column != 0) { X = mObject[i - 1].border + widthLetter / 2; mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100); } else { X = _shiftX + widthLetter / 2; mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100); } Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8); //--- switch(mObject[i].object) { case OBJ_LABEL: ap = ANCHOR_LEFT_UPPER; // unlike all other objects, the "pin" and "collapse" objects' anchor points are implemented in the upper right corner. if(i == 3) { int w, h; TextGetSize(_PanelHiddenShown, w, h); X = _shiftX + widthPanel - w; ap = ANCHOR_RIGHT_UPPER; } if(i == 4) { X = _shiftX + widthPanel; ap = ANCHOR_RIGHT_UPPER; } LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1); break; case OBJ_EDIT: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER, (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1); break; case OBJ_BUTTON: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1); break; } Add(mObject[i].name); }
すべてのパネルオブジェクトが構築された後、mObject[]構造体配列は不要になったため削除します。
ArrayFree(mObject); ApplySaved(); ChartRedraw();
私は通常、コードが複数回使用される場合には、別の関数として切り出します。ただし、処理の本質が意味ごとにまとまっている場合には、それを個別のメソッドとして分離するようにしています。ApplySaved()関数もそのようにして作成しました。この関数は、保存されたパネルデータが既に存在するかどうかを確認し、存在すればそれを適用し、存在しなければ新たなデータを保存します。
ApplySaved()
このチャートでインジケーターを初めて実行する際、saveBuffer[]配列に初期設定が入ります。
saveBuffer[]配列にすでに保存されたデータが含まれている場合は、初期設定の代わりにそれを適用します。
//+------------------------------------------------------------------+ void CPanel::ApplySaved() { // collapse the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN) CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN; // hide the indicator immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN) { HideShowInd(true); SetButtonState(prefix + "hideButton", true); hideObject = true; } else { saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN; hideObject = false; } // pin the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX) SetText(addedNames[3], _PanelUnpin); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN; int Err = Save(); if(Err != 0) Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName); } //+------------------------------------------------------------------+
お気づきかもしれませんが、ApplySaved()関数はSave()、HideShowInd()、OnEvent()関数も使用します。もしこの説明を読んでいる方がいましたら、コメント欄に「noticed(気づいた)」と書いてください。誰かがこうした説明を実際に読んでくれているのかを知るのはとても興味深いことです。
それでは、これらの関数について説明していきましょう。 まずSave()関数では、取得した設定を保存します。Filesフォルダを散らかさないように、保存されたパネル設定用にpnlフォルダを別途割り当てます。
Save()関数は次のようになります。
int Save() { ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError(); }
HideShowInd()
この関数の目的は、パネルヘッダーの色、およびボタンの色とテキストを変更することだけです。また、saveBuffer配列から以前のフラグを削除し、新しいフラグを設定します。
hideObject変数は、矢印・アイコン・テキストなどのオブジェクトを描画に使うインジケーターでのみ必要となります。インジケータに新しいオブジェクトを作成するときに、この変数の状態を確認し、状態に応じて、新しく作成されたオブジェクトをすぐに非表示にするか、何もせずにオブジェクトを表示します。
//+------------------------------------------------------------------+ void CPanel::HideShowInd(bool hide) { // change the color and text of the buttons depending on the state of the panel, hidden/displayed, as well as the header color if(hide) { SetColorBack(prefix + "TitleBar", clrTitleBar2); SetColorBack(prefix + "hideButton", clrButton3); SetText(prefix + "hideButton", "Ind Show"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN; hideObject = true; } else { SetColorBack(prefix + "TitleBar", clrTitleBar); SetColorBack(prefix + "hideButton", clrButton1); SetText(prefix + "hideButton", "Ind Hide"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN; hideObject = false; } Save(); ChartRedraw(); } //+------------------------------------------------------------------+
この関数は、IndShow/IndHideボタンが使用されている場合、クリックした場合にのみ適用されます。
2つのRSI指標のうち1つを非表示にする例
コードには、オブジェクトの表示/非表示を制御するHideShow()関数も含まれています。この関数は、パネルを折りたたんだり展開したりする操作や、パネルのオブジェクトを前面に表示する処理を担当します。
この関数は、パネルが折りたたまれているかどうかを示す引数(trueまたはfalse)を受け取ります。パネルが折りたたまれている場合は、タイトルの付いた矩形、タイトル自体、および2つのアイコン(固定と折りたたみ)の4つのオブジェクトのみをフォアグラウンドに表示する必要があります。
フラグがtrue(パネルが折りたたまれている)の場合、5つのオブジェクトを順番に非表示にしてから表示します。なぜ4つではなく5つなのでしょうか。必要なオブジェクトの中には、パネル自体の矩形が含まれており、この矩形は名前やアイコンよりも先に作成されるため、別途非表示にする必要があります。
フラグがfalseの場合、すべてのパネルオブジェクトが順番に非表示にされ、その後表示されます。これにより、すべてのオブジェクトがフォアグラウンドに表示されることになります。
//+------------------------------------------------------------------+ void CPanel::HideShow(bool hide = false) // hide and immediately display objects to bring to the foreground { int size = hide ? 5 : ArraySize(addedNames); for(int i = 0; i < size; i++) { SetHide(addedNames[i]); SetShow(addedNames[i]); } if(hide) SetHide(addedNames[1]); } //+------------------------------------------------------------------+
折りたたまれたパネルを通常のパネルの横に表示すると次のようになります。
次に見ていく関数はOnEvent()です。
関数の冒頭で、設定においてパネルが許可されているかどうかを確認します。
bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam) { if(NoPanel) return false;
次に、パネルを移動するコードを見てみましょう。コードがどのように動作するかをできるだけ詳しく説明したいと思います。
「マウス移動」イベントが発生し、パネル移動を許可するフラグがメモリに設定されると、マウス座標を保存します。
左クリックの場合、sparamは1になり、マウスボタンが以前にクリックされていない場合は、グローバル変数の値を読み取ります。
このグローバル変数は、ターミナルで実行されているすべてのパネルに共通です。パネルを移動する際に、他のパネルのプレフィックス値がこの変数に設定されているかを確認し、設定されていなければ、パネルのプレフィックスがこのグローバル変数に設定されます。これにより、他のパネル(移動しているパネルの上下にある場合)は移動できなくなります。
グローバル変数にゼロまたはこのパネルのプレフィックスが含まれている場合、パネルの現在のアンカーポイント座標を読み取ります。
次に、これがパネルヘッダー部分の矩形上でのマウスクリックであったかどうかを確認します。もしそうであれば、チャートのサイズをピクセル単位で取得し、チャートのスクロール機能を無効化します。その後、グローバル変数にパネルのプレフィックスを設定します。プレフィックスエントリは、パネルの移動を許可するフラグとして機能するだけでなく、このパネルだけが移動できることを保証します。
最後に、パネルが現在非表示か完全に表示されているかを判断する関数にp(true/false)引数を渡し、パネルを非表示または表示してフォアグラウンドに移動させます。
if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { mouseX = (long)lparam; mouseY = (long)dparam; if(previousMouseState != "1" && sparam == "1") { int gvg = (int)GlobalVariableGet("Panel"); if(gvg == prefixInd || gvg == 0) { XDistance = GetXDistance(addedNames[0]); YDistance = GetYDistance(addedNames[0]); mlbDownX = mouseX; mlbDownY = mouseY; if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height) { chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); ChartSetInteger(0, CHART_MOUSE_SCROLL, false); GlobalVariableSet("Panel", prefixInd); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // hide/display the panel so that it is in the foreground } } }
ユーザーがパネル名の付いた矩形をクリックしてマウスを動かし始めると、次のコードが実行されます。
まず、パネルが移動できるかどうかを確認します。グローバル変数にパネルのプレフィックスが設定されている場合、そのパネルは移動できます。 次に、パネルのアンカーポイントの座標を取得します。マウス座標に基づいてパネルを移動した後、新しい位置を確認します。もしパネルがチャートの範囲を越えてしまう場合は、パネルがチャートの外に出ないようにアンカーポイント座標をわずかに調整します。
その後、ループ内ですべてのパネルオブジェクトを移動させます。
ファイルに後で書き込むために、パネルのアンカーポイントの新しい座標を配列に設定し、その後、チャートを再描画します。
if((int)GlobalVariableGet("Panel") == prefixInd) { // disable the ability to go beyond the chart for the panel long posX = XDistance + mouseX - mlbDownX; if(posX < 0) posX = 0; else if(posX + widthPanel > chartWidth) posX = chartWidth - widthPanel; long posY = YDistance + mouseY - mlbDownY; if(posY < 0) posY = 0; else if(posY + row_height > chartHeight) posY = chartHeight - row_height; // move the panel int size = ArraySize(addedNames); for(int i = 0; i < size; i++) { SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]); SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]); } saveBuffer[sizeArr - 1] = (double)(posX); saveBuffer[sizeArr - 2] = (double)(posY); ChartRedraw(0); }
パネルを移動する際の最後のアクションは、マウスボタンが解放されたときに発生し、その時点でsparamは1ではなくなります。
チャートのスクロール機能を復元し、グローバル変数をリセットした後、パネルアンカーポイントの新しい座標をファイルに書き込みます。
if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd) { ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); } previousMouseState = sparam; }
パネルをドラッグするメカニズムを詳細に調べました。次に、パネルの固定/固定解除や折りたたみ/展開のアイコンをクリックしたときのアクションを調べます。
これらの操作はすべて同じOnEvent()関数内で実行されます。
マウスボタンがグラフィックオブジェクト上でクリックされると、sparam変数にはクリックされたオブジェクトの名前が格納されます。その名前が❐オブジェクト名と一致する場合、次のオブジェクトを確認し、それが表示されていればパネルオブジェクトを非表示にし、非表示であればすべてのパネルオブジェクトを表示します。パネルの可視性フラグを変更し、その情報を後でファイルに保存するために配列に書き込みます。
オブジェクトをクリックすると、パネルを移動する際の以前のコードがトリガーされることがあり、❐ アイコンがパネルのドラッグ領域にあるため、チャートスクロールが無効になることがあります。このため、スクロールを有効にします。同じ理由で、グローバル変数もリセットする必要があります。
変更内容をファイルに保存すると、チャートに変更が反映され、再描画されます。
else if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == addedNames[4]) // prefix+"CollapseExpand" { if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// if the panel is visible, hide it { SetHide(addedNames[1]); for(int i = 5; i < sizeObject; i++) SetHide(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN; } else// if the panel is hidden, display it { for(int i = 0; i < sizeObject; i++) SetShow(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN; } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
次のコードは上記のコードと似ていますが、唯一の違いは∇オブジェクト名です。
else if(sparam == addedNames[3]) // prefix+"PinUnpin" { if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX; SetText(addedNames[3], _PanelUnpin); } else { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN; SetText(addedNames[3], _PanelPin); } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
コードはインジケーターの削除ボタンで終わります。
else if(sparam == prefix + "delButton") // handle the indicator deletion button ChartIndicatorDelete(0, ChartWindowFind(), indName); }
最後に処理する必要があるイベントは、グラフィカルオブジェクトの作成です。
新しく作成されたオブジェクトは通常、フォアグラウンドに配置され、パネルと重なってしまう可能性があります。そのため、まずオブジェクトの選択状態を記憶し、その後パネルを一度非表示にして再表示することでフォアグラウンドに移動させ、最後にオブジェクトの選択状態を復元します。なぜわざわざこのようなことをするのでしょうか。実は、プログラムでパネルオブジェクトを非表示して表示すると、新しく作成されたオブジェクトの選択状態が解除されてしまうためです。
この現象については、こちらでさらに詳しく説明されています。
else if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179 "Making a dashboard to display data in indicators and EAs" { bool select = GetSelect(sparam); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// hide/display the panel so that it is in the foreground SetSelect(sparam, select);// restore the state of the extreme object } return true; }
これでパネルコードの説明は完了です。
インジケーターコードにどのような編集を加える必要があるか
インジケーターによって必要な変更内容は異なります。インジケーターにそのまま挿入して即座に使える汎用的なコントロールパネル用コードを作成することはできませんでした。各インジケーターごとに個別対応が必要です。
ここでは、他のインジケーターにも同様の方法で変更を加える際に参考になるいくつかの例を紹介します。
なお、一貫性を持たせるために、変更を加えたインジケーターの名前には「Pnl」という語を付け加えています。
カスタム移動平均インジケーター
パネルからインジケーターを操作するには、入力変数を変更できる必要があります。しかし、入力変数は定数であり、コード内で直接変更することはできません。
この問題を解決するために、入力変数の値を変更可能な通常の変数にコピーします。インジケーターコードの変更を最小限に抑えるために、新しい変数には元の入力変数と同じ名前を使用し、末尾にアンダースコア(_)を追加して区別します。
変更前:
変更後:
入力パラメータの直後に、Panel.mqhファイルをインクルードし、CPanelmPanelクラスのインスタンスを宣言します。
#includeディレクティブが入力パラメータの前に設定されている場合、インクルードファイルで設定されているすべての入力パラメータがインジケーターの入力パラメータよりも上位に配置されるため、インジケーターの起動時に不便が生じます。
すべてを正しく実行すると、次のような画像が表示されます。
パネル設定が必要ない場合は、Panel.mqhインクルードファイル内の「input」という単語をすべて削除し、デフォルト設定を使用します。
OnInit()関数に次のコードを追加します。
次に、インジケーターでパネルが有効になっているかどうかを確認し、このチャートでインジケーターを初めて起動する場合は、以前に保存したパネル設定をダウンロードします。初めて起動する場合は、入力パラメータの数(3つあります)に応じて配列のサイズを変更し、これらの入力パラメータの値を配列に設定します。
if(!NoPanel) { if(mPanel.Load(short_name)) { InpMAPeriod = (int)mPanel.saveBuffer[0]; InpMAShift = (int)mPanel.saveBuffer[1]; InpMAMethod = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = InpMAPeriod; mPanel.saveBuffer[1] = InpMAShift; mPanel.saveBuffer[2] = InpMAMethod; }
パネル名、インジケーター名
次に、オブジェクト名、オブジェクトタイプ、パネル行インデックス、オブジェクト自体、パネル幅のパーセントなど、すべてが同じ方法で入力されます。
mPanel.Init("Moving Average", short_name); mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50); mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50); mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50); mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50); mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50); mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50); mPanel.Create(); }
この段階でインジケーターを実行すると、次のようなパネルが表示されます。
パネルは作成できたので、あとはユーザーとのコミュニケーションのためのコードを記述するだけです。
インジケーターにもう1つの関数OnChartEvent()を追加します。
パネルを移動するためのメソッドは上記で説明されています。 「グラフィックオブジェクトのテキスト編集完了」イベントが発生した場合、編集が完了したEditオブジェクトプレフィックスを確認します。プレフィックスがパネルのプレフィックスと一致する場合、変更されたインジケーターのパラメータがどれかを確認し、変更内容を変数と配列に設定して後で保存できるようにします。
次に、すべての変更をファイルに保存し、インジケーターを再起動します。
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "MAPeriod") { mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam)); } else if(sparam == mPanel.prefix + "MAShift") { mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam)); PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift); } else if(sparam == mPanel.prefix + "MAMethod") { mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam)); } mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); }
MAインジケーターの非表示/表示ボタンの操作は非常に簡単です。
移動線を非表示にするには、DRAW_NONEのグラフィック構造のスタイルを設定し、表示する場合はDRAW_LINEに設定するだけです。
if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); mPanel.HideShowInd(false); } }
これで、カスタム移動平均インジケーターの変更が完了します。変更されたインジケーターは、カスタム移動平均Pnlと呼ばれます。
パラボリックSARインジケーター
パラボリックSARインジケーターの変更は、非常に小さなニュアンスを除いて、カスタム移動平均インジケーターとまったく同じです。
パラボリックSARインジケーターでは、入力変数と同じ名前の新しい変数が既に存在するため、それらを作成する必要はありません。
したがって、すぐにインクルードファイルを追加します。
OnInit()に次のコードを追加します。
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtSarStep = mPanel.saveBuffer[0]; ExtSarMaximum = mPanel.saveBuffer[1]; } else { mPanel.Resize(2); mPanel.saveBuffer[0] = ExtSarStep; mPanel.saveBuffer[1] = ExtSarMaximum; } mPanel.Init("ParabolicSAR", short_name); mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50); mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50); mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50); mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50); mPanel.Create(); }
インジケーターコードにOnChartEvent()関数を追加します。
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "SARStep") mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam)); else if(sparam == mPanel.prefix + "SARMaximum") mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 159); mPanel.HideShowInd(false); } } //+------------------------------------------------------------------+
パラボリックSARインジケーターへの変更はこれですべてです。
RSIインジケーター
RSIインジケーターでは、すべてが前の2つのインジケーターとまったく同じように実行されます。
入力設定の後に以下を挿入します。
#include <Panel\\Panel.mqh>
CPanel mPanel;
次に、OnInit()で以下をおこないます。
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtPeriodRSI = (int)mPanel.saveBuffer[0]; } else { mPanel.Resize(1); mPanel.saveBuffer[0] = ExtPeriodRSI; } mPanel.Init("RSI", short_name); mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60); mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40); mPanel.Create(); }
OnChartEvent()は以前のインジケーターとは少し異なります。
入力フィールドオブジェクトも同様に処理されます。ただし、インジケーターの非表示/表示は異なる方法で処理されます。これまで、メインチャートのインジケーターを検討してきましたが、RSIはサブウィンドウインジケーターです。
[Ind Hide]をクリックすると、インジケーターウィンドウの高さが0に設定されます。パネルの色、ボタンの色、テキストを変更します。
ボタンをもう一度押すと(今度は名前が異なり、[IndShow]となります)、CHART_HEIGHT_IN_PIXELSの値が-1に設定されます。 パネルの色、ボタンの色、テキストを変更します。
以下はマニュアルからの引用です。
「CHART_HEIGHT_IN_PIXELSプロパティをプログラムで設定すると、ユーザーがウィンドウ/サブウィンドウのサイズを編集できなくなります。 サイズの固定を無効にするには、プロパティ値を「-1」に設定します。」
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(mPanel.OnEvent(id, lparam, dparam, sparam)) { if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) if(sparam == mPanel.prefix + "PeriodRSI") { mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") // hide the subwindow indicator { if(GetButtonState(sparam)) { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0); mPanel.HideShowInd(true); } else { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1); mPanel.HideShowInd(false); } } } } //+------------------------------------------------------------------+
さらにもう一つの指標
グラフィカルスタイルをまったく使用せず、代わりにグラフィカルオブジェクト(通常は矢印)を描画するインジケーターもあります。これは、「インジケータの非表示/表示」ボタンを処理するための別のオプションです。詳しく見てみましょう。
矢印付きのインジケーターを探す代わりに、フラクタルインジケーターを作成しました。このインジケーターでは、上部のアイコンはPLOT_ARROWグラフィカル構造を使用して表示され、下部のアイコンはOBJ_ARROWオブジェクトをプロットして表示されます。
ここでインジケーターのコードをすべて提供します。
設定には、フラクタルショルダーのサイズと、OBJ_ARROWを描画する日数が含まれます。オブジェクトの数が多すぎるとチャートの表示速度が大幅に低下する可能性があるため、日数を制限する必要がありました。
前のインジケーターと同様に、入力変数の直後にPanel.mqhファイルをインクルードし、CPanelクラスのインスタンスを宣言します。
入力変数を通常の変数と複製します。
#property indicator_chart_window #property indicator_plots 1 #property indicator_buffers 1 #property indicator_type1 DRAW_ARROW #property indicator_color1 clrRed #property indicator_label1 "Fractals" input int _day = 10; // day input int _barLeft = 1; // barLeft input int _barRight = 1; // barRight #include <Panel\\Panel.mqh> CPanel mPanel; double buff[]; int day = _day, barLeft = _barLeft, barRight = _barRight; datetime limitTime = 0;
OnInit()では、すべてが前のインジケーターと同じです。
//+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, buff, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); PlotIndexSetInteger(0, PLOT_ARROW, 217); PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5); string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight); IndicatorSetString(INDICATOR_SHORTNAME, short_name); if(!NoPanel) { if(mPanel.Load(short_name)) { day = (int)mPanel.saveBuffer[0]; barLeft = (int)mPanel.saveBuffer[1]; barRight = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = day; mPanel.saveBuffer[1] = barLeft; mPanel.saveBuffer[2] = barRight; } mPanel.Init("Fractals", short_name); mPanel.Record("dayText", OBJ_LABEL, 1, "Days:", 50); mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50); mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50); mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50); mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50); mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50); mPanel.Create(); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
以前のインジケーターのOnChartEvent()との主な違いは、インジケーターのパラメータが変更されるたびに、インジケーターによって描画されたオブジェクトをチャートから削除する必要があることです。
インジケーターを非表示にするボタンを押すと、ループ内でインジケーターによって描画されたすべてのオブジェクトが非表示になります。さらに、DRAW_NONEグラフィカル構造体の型を設定します。
逆の場合、DRAW_ARROWグラフィカル構造体の型を設定するだけでなく、Wingdingsから矢印インデックスを設定する必要があります。次に、ループ内ですべての非表示オブジェクトを可視オブジェクトに変換します。
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "day") mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barLeft") mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barRight") mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") { if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetHide(name); } mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 217); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetShow(name); } mPanel.HideShowInd(false); } } } //+------------------------------------------------------------------+
また、新しく描画されたオブジェクトごとに、インジケーターを非表示にする必要があることを示すフラグのチェックを追加する必要があります。このフラグがtrueの場合、新しく描画されたオブジェクトは非表示になります。
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
int limit = prev_calculated - 1;
if(prev_calculated <= 0)
{
ArrayInitialize(buff, 0);
datetime itime = iTime(_Symbol, PERIOD_D1, day);
limitTime = itime <= 0 ? limitTime : itime;
if(limitTime <= 0)
return 0;
int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
limit = MathMax(rates_total - shift, barRight + barLeft);
}
for(int i = limit; i < rates_total && !IsStopped(); i++)
{
bool condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(high[j - 1] >= high[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(high[j - 1] <= high[j])
{
condition = false;
break;
}
if(condition)
buff[i - barRight] = high[i - barRight];
condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(low[j - 1] <= low[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(low[j - 1] >= low[j])
{
condition = false;
break;
}
if(condition)
{
string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
if(mPanel.hideObject)
SetHide(name);
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
}
//+------------------------------------------------------------------+
この記事の執筆中に、オブジェクト設定へのクイックアクセスとしてパネルを使用する例を示しました。インジケーターコードを説明する価値があると思います。
オブジェクトPnlインジケーターの設定
このインジケーターでは、パネルはさまざまなオブジェクトに対して呼び出される必要があるため、OnInit()でパネルクラスのオブジェクトを作成する必要はありません。つまり、新しい演算子を使用して動的に作成することになります。
クラスオブジェクトハンドルを宣言します。チャートのグラフィックオブジェクトがクリックされ、Shiftキーが押された場合は、クラスオブジェクトの以前に作成されたハンドルを初期化します。
インジケーターの場合と同じ方法でパネルを作成しますが、1つの微妙な違いは、Create()メソッドが現在のマウスチャートの座標を引数として受け取ることです。
入力フィールドの変更はインジケーターの場合と同じ方法で処理されますが、ファイルに加えられた変更を保存する必要がないという唯一の違いがあります。
編集が完了したら、ハンドルが削除された状態でDelPnlボタンを押してパネルを削除できます。
オブジェクトによってプロパティが異なる場合があるので、パネルを描画する際にはこの点に留意する必要があります。トレンドラインを編集する場合、オブジェクトの入力を担当するパネル内のフィールドは必要ありません。
つまり、トレンドラインに対してはこのようなフィールドを作成せず、代わりに塗りつぶされるように設計されたオブジェクトに対してのみフィールドを作成します。ここでは、パネル内の行の正確な数を事前に知ることができないため、行変数を導入し、現在の行インデックスをそこに書き込み、必要に応じて増やします。
#property indicator_chart_window #property indicator_plots 0 #define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P) #include <Panel\\Panel.mqh> CPanel * mPl; //+------------------------------------------------------------------+ int OnCalculate(const int, const int, const int, const double &price[]) {return(0);} //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { static bool panel = false; if(panel) mPl.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_CLICK) if(!panel) { if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0) { int line = 1; mPl = new CPanel(); ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam); mPl.Init(EnumToString(ObjectType), sparam); mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50); mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50); line++; mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50); mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50); line++; mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50); mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50); line++; if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE) { mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50); mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50); line++; } mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100); mPl.Create(0, (int)lparam, (int)dparam); panel = true; } } else if(sparam == mPl.prefix + "delButton") { FREE(mPl); panel = false; } if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPl.prefix) >= 0) { if(sparam == mPl.prefix + "Color") SetColor(mPl.indName, StringToColor(GetText(sparam))); else if(sparam == mPl.prefix + "Style") SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Width") SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Fill") SetFill(mPl.indName, (int)StringToInteger(GetText(sparam))); ChartRedraw(); } } //+------------------------------------------------------------------+ void OnDeinit(const int reason) {FREE(mPl);} //+------------------------------------------------------------------+
オブジェクト設定パネルは、Shiftキーを押しながらオブジェクトを左クリックすると呼び出されます。
結論
長所
- ユーザーフレンドリーなソリューション
短所
- CPanelクラスでインジケーターを「非表示」に設定したかったのですが、うまくいきませんでした
- インジケーターの短縮名に入力変数を追加しないと、名前が一致するため、パネルで複数のインジケーターを呼び出すことができなくなります
- チャート上でインジケーターを起動し、パネルを使用して設定を変更し、テンプレートを保存すると、テンプレートを読み込むときに、最新のパラメータではなく、インジケーターの起動時に設定で指定されたパラメータが読み込まれます
- すべてのインジケーターにパネルを装備できるわけではありません
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14672





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索