
GUI:MQLで独自のグラフィックライブラリを作成するためのヒントとコツ
はじめに
GUIライブラリの開発は、AI、(優れた) ニューラルネットワーク、... 自分が開発していないGUIライブラリの使用に習熟するなどの非常に高度なものは別として、MetaTrader 5のコンテキストで誰もが考えることができる最大の非具体的なプロジェクトの1つです。
自分が開発していないGUIライブラリについては半分冗談でした。もちろん、すでに作られたライブラリの使い方を学ぶ方が簡単です(本当に大きなGUIライブラリがあるとはいえ)。ただし、自分で作るよりも優れたライブラリの使い方を学べるなら、わざわざゼロから作る必要はないでしょう。
実は、作る理由はいくつかあります。自分の特定のプロジェクトには低速すぎるようだ、ライブラリに含まれていない非常に特殊なもの(ライブラリによっては拡張が難しいものもある)や、その実装では不可能な機能が必要な場合に拡張する必要がある、バグがある可能性(ライブラリの悪用から発生するものを除く)...あるいは、単にそのライブラリについて知りたいなどです。これらの問題のほとんどは、特定のライブラリの作者が解決することができますが、(機能拡張の場合)作者がそれに気づくか、それを喜んでやってくれるかに依存することになります。
この記事では、インターフェイスの作り方を教えたり、完全に機能するライブラリを開発する手順を示したりすることが目的ではありません。その代わりに、GUIライブラリを作るための出発点として、あるいは読者が見つけたかもしれない特定の問題を解決するために、あるいはすでに完成されたGUIライブラリの巨大なコードベースの内部で起こっていることについての最初の理解を得るために、GUIライブラリの特定の部分がどのように作られるかの例を提供します。
結論から言うと...「GUIライブラリ」を作ります。
プログラムの構造とオブジェクト階層
GUIライブラリを作り始める前に、GUIライブラリとは何かと尋ねるべきです。要するに、他の(チャートの)オブジェクトを追跡し、そのプロパティを変更してさまざまな効果を発生させたり、移動、クリック、色の変化などのイベントをトリガーしたりする、オブジェクトの見栄えのする階層構造です。この階層がどのように構成されているかは実装によって異なるかもしれませんが、最も一般的な(そして私が最も好きな)ものは、1つの要素が他のサブ要素を持つことができる、要素のツリー構造です。
これを作るために、まずは要素の基本的な実装から始めます。
class CElement { private: //Variable to generate names static int m_element_count; void AddChild(CElement* child); protected: //Chart object name string m_name; //Element relations CElement* m_parent; CElement* m_children[]; int m_child_count; //Position and size int m_x; int m_y; int m_size_x; int m_size_y; public: CElement(); ~CElement(); void SetPosition(int x, int y); void SetSize(int x, int y); void SetParent(CElement* parent); int GetGlobalX(); int GetGlobalY(); void CreateChildren(); virtual void Create(){} };
基本要素クラスは今のところ、位置、大きさ、他の要素との関係に関する情報しか含んでいません。
位置変数m_xとm_yは、親オブジェクトのコンテキスト内でのローカルの位置です。そのため、オブジェクトが画面内で実際にあるべき位置を決定するグローバル位置関数が必要になります。以下に、グローバル位置を再帰的に(この場合はXに対して)取得するメソッドを示します。
int CElement::GetGlobalX(void) { if (CheckPointer(m_parent)==POINTER_INVALID) return m_x; return m_x + m_parent.GetGlobalX(); }
コンストラクタでは、各オブジェクトに固有の名前を決める必要があります。そのためには、静的変数を使うことができます。この記事では説明しない理由から、私は後で説明するプログラムクラス内にその変数を含めることを好みますが、簡単にするために要素クラス内にインクルードします。
メモリリークを避けるために、デストラクタで子要素を削除することを忘れないようにすることが非常に重要です。
int CElement::m_element_count = 0; //+------------------------------------------------------------------+ //| Base Element class constructor | //+------------------------------------------------------------------+ CElement::CElement(void) : m_child_count(0), m_x(0), m_y(0), m_size_x(100), m_size_y(100) { m_name = "element_"+IntegerToString(m_element_count++); } //+------------------------------------------------------------------+ //| Base Element class destructor (delete child objects) | //+------------------------------------------------------------------+ CElement::~CElement(void) { for (int i=0; i<m_child_count; i++) delete m_children[i]; }
最後に、要素間の通信に両方の参照が必要になるため、リレーション関数AddChildとSetParentを定義します。例えば、グローバル位置を取得するために、子は親の位置を知る必要がありますが、ポジションを変更するときには、親はそのことを子に通知する必要があります(この最後の部分は後で実装)。重複を避けるため、AddChildをprivateとしました。
create関数では、チャートオブジェクトそのものを作成します(また、他のプロパティを変更)。親の後に子が作成されるようにすることが重要です。そのため、この目的のために別の関数が使用されます (createはオーバーライドでき、実行順序が変更される可能性があるため)。要素の基本クラスでは、createは空です。
//+------------------------------------------------------------------+ //| Set parent object (in element hierarchy) | //+------------------------------------------------------------------+ void CElement::SetParent(CElement *parent) { m_parent = parent; parent.AddChild(GetPointer(this)); } //+------------------------------------------------------------------+ //| Add child object reference (function not exposed) | //+------------------------------------------------------------------+ void CElement::AddChild(CElement *child) { if (CheckPointer(child)==POINTER_INVALID) return; ArrayResize(m_children, m_child_count+1); m_children[m_child_count] = child; m_child_count++; }
//+------------------------------------------------------------------+ //| First creation of elements (children after) | //+------------------------------------------------------------------+ void CElement::CreateChildren(void) { for (int i=0; i<m_child_count; i++) { m_children[i].Create(); m_children[i].CreateChildren(); } }
これからプログラムクラスを作成します。今のところは、ホルダーとして使用される空の要素と間接的にやりとりするためのプレースホルダーに過ぎませんが、将来的には、すべての要素に影響する(そして複数回実行されたくない)他の集中操作を実行するようになるでしょう。他の要素を格納するために空の要素ホルダーを使うことで、子要素を再帰的に反復する必要がある関数を作り直す必要がなくなります。今のところ、ホルダーをポインタとして保存しないので、このクラスのコンストラクタ/デストラクタは必要ありません。
class CProgram { protected: CElement m_element_holder; public: void CreateGUI() { m_element_holder.CreateChildren(); } void AddMainElement(CElement* element) { element.SetParent(GetPointer(m_element_holder)); } };
最初のテストをおこなう前に、Elementクラスを拡張する必要があります。そうすれば、異なるタイプのオブジェクトを異なる方法で管理することができます。まず、CCanvas(実際はビットマップラベル)を使ってキャンバス要素を作成します。キャンバスオブジェクトはカスタムGUIを作るのに最も汎用性の高いオブジェクトで、ほとんどすべてのものをキャンバスで作ることができます。
#include <Canvas/Canvas.mqh> //+------------------------------------------------------------------+ //| Generic Bitmap label element (Canvas) | //+------------------------------------------------------------------+ class CCanvasElement : public CElement { protected: CCanvas m_canvas; public: ~CCanvasElement(); virtual void Create(); }; //+------------------------------------------------------------------+ //| Canvas Element destructor (destroy canvas) | //+------------------------------------------------------------------+ CCanvasElement::~CCanvasElement(void) { m_canvas.Destroy(); } //+------------------------------------------------------------------+ //| Create bitmap label (override) | //+------------------------------------------------------------------+ void CCanvasElement::Create() { CElement::Create(); m_canvas.CreateBitmapLabel(0, 0, m_name, GetGlobalX(), GetGlobalY(), m_size_x, m_size_y, COLOR_FORMAT_ARGB_NORMALIZE); }
Createの最後にこの2行を追加して、キャンバスをランダムな色で塗りつぶします。より正しい方法は、この特定のタイプのオブジェクトを作成するためにキャンバスクラスを拡張することですが、今はそこまで複雑にする必要はありません。
m_canvas.Erase(ARGB(255, MathRand()%256, MathRand()%256, MathRand()%256)); m_canvas.Update(false);
また、編集オブジェクトクラスも作成します。しかし、なぜ特にこれなのでしょうか。キャンバスに直接描画し、キーボードイベントをトラッキングして書き込むという編集方法はどうでしょうか。実は、canvas では実行できないことが 1 つあります。それは、テキストのコピー&ペーストだ(少なくとも、DLLを使わずにアプリケーションの外部との間で)。ライブラリにこの機能を必要としない場合は、要素クラスに直接キャンバスを追加して、すべてのタイプのオブジェクトに使用することができます。お気づきのように、キャンバスとの関係でいくつかのことが異なっています。
class CEditElement : public CElement { public: ~CEditElement(); virtual void Create(); string GetEditText() { return ObjectGetString(0, m_name, OBJPROP_TEXT); } void SetEditText(string text) { ObjectSetString(0, m_name, OBJPROP_TEXT, text); } }; //+------------------------------------------------------------------+ //| Edit element destructor (remove object from chart) | //+------------------------------------------------------------------+ CEditElement::~CEditElement(void) { ObjectDelete(0, m_name); } //+------------------------------------------------------------------+ //| Create edit element (override) and set size/position | //+------------------------------------------------------------------+ void CEditElement::Create() { CElement::Create(); ObjectCreate(0, m_name, OBJ_EDIT, 0, 0, 0); ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX()); ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY()); ObjectSetInteger(0, m_name, OBJPROP_XSIZE, m_size_x); ObjectSetInteger(0, m_name, OBJPROP_YSIZE, m_size_y); }
この場合、位置とサイズのプロパティを明示的に設定する必要があります(Canvasでは、これらはCreateBitmapLabelの内部でおこなわれる)。
これらすべての変更によって、ようやく最初のテストができるようになりました。
#include "Basis.mqh" #include "CanvasElement.mqh" #include "EditElement.mqh" input int squares = 5; //Amount of squares input bool add_edits = true; //Add edits to half of the squares CProgram program; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MathSrand((uint)TimeLocal()); //100 is element size by default int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100; for (int i=0; i<squares; i++) { CCanvasElement* drawing = new CCanvasElement(); drawing.SetPosition(MathRand()%max_x, MathRand()%max_y); program.AddMainElement(drawing); if (add_edits && i<=squares/2) { CEditElement* edit = new CEditElement(); edit.SetParent(drawing); edit.SetPosition(10, 10); edit.SetSize(80, 20); } } program.CreateGUI(); ChartRedraw(0); return(INIT_SUCCEEDED); }
このプログラムはいくつかの正方形を生み出し、チャートから外れるとそれらを削除します。各編集要素は、親要素と同じ位置に相対的に配置されていることにご注目ください。
今のところ、これらの正方形は大した役割を果たさず、ただ「そこにある」だけです。次のセクションでは、これらの動作にどのような工夫を加えるかについて説明します。
マウス入力
チャートイベントを扱ったことがある方なら、クリックイベントを使うだけでは複雑な動作を作るには不十分だということがおわかりでしょう。より良いインターフェイスを作るコツは、マウスの移動イベントを使うことです。これらのイベントはエキスパートアドバイザー(EA)の起動時に有効にする必要があるので、GUIを作成する前に有効にします。
void CProgram::CreateGUI(void) { ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); m_element_holder.CreateChildren(); }
これらは、マウスの位置(x,y)とマウスボタンの状態(ctrlとshift)を提供します。状態または位置の少なくとも1つが変化するたびに、イベントがトリガーされます。
まず、ボタンが通過できるフェーズを定義します。使われていないときは非アクティブ、クリックされたときはアクティブになります。また、それぞれ最初のアクティブ状態と非アクティブ状態(クリック状態の変化)を表すdownとupも追加します。マウスイベントだけで各フェーズを検出できるので、クリックイベントを使う必要もないはずです。
enum EInputState { INPUT_STATE_INACTIVE=0, INPUT_STATE_UP=1, INPUT_STATE_DOWN=2, INPUT_STATE_ACTIVE=3 };
作業を容易にするために、各オブジェクト内でマウスイベントの処理を行うのではなく、マウスイベントの処理を一元化します。そうすることで、より簡単にマウスデータにアクセスして追跡できるようになります。これにより、他の種類のイベントでもマウスイベントを使用できるようになり、計算の繰り返しを回避できるようになります。このクラスは、マウス入力とは別に、ctrlボタンとshiftボタンも含むので、「CInputs」と呼ぶことにします。
//+------------------------------------------------------------------+ //| Mouse input processing class | //+------------------------------------------------------------------+ class CInputs { private: EInputState m_left_mouse_state; EInputState m_ctrl_state; EInputState m_shift_state; int m_pos_x; int m_pos_y; void UpdateState(EInputState &state, bool current); public: CInputs(); void OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); EInputState GetLeftMouseState() { return m_left_mouse_state; } EInputState GetCtrlState() { return m_ctrl_state; } EInputState GetShiftState() { return m_shift_state; } int X() { return m_pos_x; } int Y() { return m_pos_y; } }; //+------------------------------------------------------------------+ //| Inputs constructor (initialize variables) | //+------------------------------------------------------------------+ CInputs::CInputs(void) : m_left_mouse_state(INPUT_STATE_INACTIVE), m_ctrl_state(INPUT_STATE_INACTIVE), m_shift_state(INPUT_STATE_INACTIVE), m_pos_x(0), m_pos_y(0) { } //+------------------------------------------------------------------+ //| Mouse input event processing | //+------------------------------------------------------------------+ void CInputs::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id!=CHARTEVENT_MOUSE_MOVE) return; m_pos_x = (int)lparam; m_pos_y = (int)dparam; uint state = uint(sparam); UpdateState(m_left_mouse_state, ((state & 1) == 1)); UpdateState(m_ctrl_state, ((state & 8) == 8)); UpdateState(m_shift_state, ((state & 4) == 4)); } //+------------------------------------------------------------------+ //| Update state of a button (up, down, active, inactive) | //+------------------------------------------------------------------+ void CInputs::UpdateState(EInputState &state, bool current) { if (current) state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN; else state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE; } //+------------------------------------------------------------------+
UpdateStateでは、現在の状態(ブール値)と最後の状態をチェックし、入力がアクティブ/非アクティブであるかどうか、また状態(アップ/ダウン)の変化後の最初のイベントであるかどうかを判断します。sparamでは、ctrlとshiftの情報が「ただ」で手に入ります。また、中央、右、さらに 2 つの追加のマウス ボタンも手に入ります。コードには追加していませんいが、もし使いたいのであれば、追加するために必要な変更を加えるのは簡単です。
マウス入力のインスタンスをプログラムに追加し、ポインタを持つ各オブジェクトにアクセスできるようにする。イベントごとに、入力インスタンスが更新されます。イベントの種類は、inputsクラスの内部でフィルタリングされます(マウスの移動イベントだけが使用されます)。
class CProgram { //... protected: CInputs m_inputs; //... public: void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); //... }; //+------------------------------------------------------------------+ //| Program constructor (pass inputs reference to holder) | //+------------------------------------------------------------------+ CProgram::CProgram(void) { m_element_holder.SetInputs(GetPointer(m_inputs)); } //+------------------------------------------------------------------+ //| Process chart event | //+------------------------------------------------------------------+ void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); }
次のセクションでは、OnChartEventがどのように使われるかについて、より詳しく説明します。
グローバル要素に入力を設定すると、その子要素に再帰的に渡されます。しかし、後で(最初のSetInputs呼び出しの後で)子プロセスを追加するときにも、子プロセスを受け渡す必要があることを忘れてはなりません。
//+------------------------------------------------------------------+ //| Set mouse inputs reference | //+------------------------------------------------------------------+ void CElement::SetInputs(CInputs* inputs) { m_inputs = inputs; for (int i = 0; i < m_child_count; i++) m_children[i].SetInputs(inputs); } //+------------------------------------------------------------------+ //| Add child object reference (function not exposed) | //+------------------------------------------------------------------+ void CElement::AddChild(CElement *child) { if (CheckPointer(child) == POINTER_INVALID) return; ArrayResize(m_children, m_child_count + 1); m_children[m_child_count] = child; m_child_count++; child.SetInputs(m_inputs); }
次のセクションでは、イベント処理関数を作り、各オブジェクトにいくつかのマウスイベントを追加します。
イベント処理
イベントそのものを処理する前に、インターフェイスでスムーズな動きを実現するために必要なことがひとつあります。チャートの再描画です。オブジェクトの位置や再描画など、オブジェクトのプロパティが変更されるたびに、その変更を即座に表示するためにチャートを再描画する必要があります。とはいえ、ChartRedrawを頻繁に呼び出すとGUIがちらつく可能性があります。そのため、私はその実行を一元化することを好みます。
class CProgram { private: bool m_needs_redraw; //... public: CProgram(); void OnTimer(); //... void RequestRedraw() { m_needs_redraw = true; } }; void CProgram::OnTimer(void) { if (m_needs_redraw) { ChartRedraw(0); m_needs_redraw = false; } }
ご想像の通り、OnTimerは同じ名前のイベント処理関数で呼び出される必要があり、各要素はRequestRedrawを呼び出せるようにプログラムへの参照を持つ必要があります。この関数はフラグをセットし、このフラグがアクティブになると、次のタイマー呼び出しですべての要素を再描画します。まずタイマーをセットする必要があります。
#define TIMER_STEP_MSC (16) void CProgram::CreateGUI(void) { ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); m_element_holder.CreateChildren(); ::EventSetMillisecondTimer(TIMER_STEP_MSC); }
16ミリ秒は、タイマーを確実に実行できる限界(あるいは限界に近い)間隔です。しかし、重いプログラムはタイマーの実行をブロックする可能性があります。
次に、以下では各要素にどのようにチャートイベントが実装されているかがわかります。
//+------------------------------------------------------------------+ //| Send event recursively and respond to it (for this element) | //+------------------------------------------------------------------+ void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) m_children[i].OnChartEvent(id, lparam, dparam, sparam); OnEvent(id, lparam, dparam, sparam); }
このライブラリでは、イベントを親から子へ再帰的に渡すことにしました。これは設計上の選択です(例えば、リスナーを使ったり、すべてのオブジェクトを順次呼び出すなど、別の方法もある)が、後で見るように、これにはいくつかの利点があります。OnEventはオーバーライド可能なprotected関数です。OnChartEvent(これはpublicです)から分離されているので、ユーザーは子オブジェクトへのイベントの受け渡しをオーバーライドすることができないし、親クラスのOnEventの実行方法(実行前、実行後、実行しない)を選択することもできません。
イベント処理の例として、正方形のドラッグ機能を実装します。クリックイベントやホバーイベントももっと簡単に実装できますが、この例では必要ないでしょう。今のところ、イベントがオブジェクトに渡されるだけで、何もしません。しかし、現状では問題があります。オブジェクトをドラッグしようとすると、まるでそれがなかったかのように後ろのチャートが動いてしまうのです。今のところ動くことは期待されていませんが、チャートも動くべきではありません。
この問題を避けるために、まずマウスがオブジェクトに合わせられているかどうかを確認します。合わせられている場合は、チャートのスクロールをブロックします。パフォーマンス上の理由から、グローバルホルダーの直接の子オブジェクトだけを確認することにします(他のサブオブジェクトが親の境界内に保たれていれば、プログラムは常に動作します)。
//+------------------------------------------------------------------+ //| Check if mouse is hovering any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { for (int i = 0; i < m_child_count; i++) { if (m_children[i].IsMouseHovering()) return true; } return false; } //+------------------------------------------------------------------+ //| Process chart event | //+------------------------------------------------------------------+ void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); if (id == CHARTEVENT_MOUSE_MOVE) EnableControls(!m_element_holder.CheckHovers()); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Enable/Disable chart scroll responses | //+------------------------------------------------------------------+ void CProgram::EnableControls(bool enable) { //Allow or disallow displacing chart ::ChartSetInteger(0, CHART_MOUSE_SCROLL, enable); }
これで動きを回避します...ただし、マウスがオブジェクト上にある場合のみです。枠外にドラッグしても、チャートはまだ動lきます。
そのためにCElement (m_dragging)に2つのチェックとprivateのbool変数を追加する必要があります。
//+------------------------------------------------------------------+ //| Send event recursively and respond to it (for this element) | //+------------------------------------------------------------------+ void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) m_children[i].OnChartEvent(id, lparam, dparam, sparam); //Check dragging start if (id == CHARTEVENT_MOUSE_MOVE) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Check if mouse hovers/drags any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { for (int i = 0; i < m_child_count; i++) { if (m_children[i].IsMouseHovering() || m_children[i].IsMouseDragging()) return true; } return false; }
オブジェクトをドラッグするときはすべてうまくいくのですが、微妙な修正が欠けています...チャートをドラッグするとき、マウスがオブジェクトの上に行くとドラッグが止まってしまうのです。オブジェクトの外側から始まるドラッグをフィルタリングする必要があります。
幸いなことに、それを修正するのはそれほど難しくありません。
//+------------------------------------------------------------------+ //| Check if mouse hovers/drags any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { EInputState state = m_inputs.GetLeftMouseState(); bool state_check = state != INPUT_STATE_ACTIVE; //Filter drags that start in chart for (int i = 0; i < m_child_count; i++) { if ((m_children[i].IsMouseHovering() && state_check) || m_children[i].IsMouseDragging()) return true; } return false; }
さて、マウスがオブジェクトをホバーしていてもマウスがアクティブであれば、チャートをドラッグしている可能性があるので無視されます(オブジェクトをドラッグしていれば、そのオブジェクトがtrueを返すことになります)。マウスホバリングチェックがないと、1フレーム遅れてイベントが無効になってしまうので、マウスホバリングチェックはまだ必要です。
すべての準備が整ったので、正方形クラスにドラッグ機能を追加しましょう。ただし、CCanvasElementに追加するのではなく、継承でクラスを拡張します。また、前回の例から描画線を抽出し、デフォルトで空になるようにします。すでにドラッグチェックを追加しているので、それを使ってイベントを処理し、オブジェクトを動かすことができます。チャート上のオブジェクトの位置を変更するには、そのオブジェクトの変数を変更し、位置プロパティを更新し、子オブジェクトの位置を更新し、チャートを再描画する必要があります。
class CElement { //... public: void UpdatePosition(); }; //+------------------------------------------------------------------+ //| Update element (and children) position properties | //+------------------------------------------------------------------+ void CElement::UpdatePosition(void) { ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX()); ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY()); for (int i = 0; i < m_child_count; i++) m_children[i].UpdatePosition(); }
class CCanvasElement : public CElement { protected: CCanvas m_canvas; virtual void DrawCanvas() {} //... }; //+------------------------------------------------------------------+ //| Create bitmap label (override) | //+------------------------------------------------------------------+ void CCanvasElement::Create() { //... DrawCanvas(); }
//+------------------------------------------------------------------+ //| Canvas class which responds to mouse drag events | //+------------------------------------------------------------------+ class CDragElement : public CCanvasElement { private: int m_rel_mouse_x, m_rel_mouse_y; protected: virtual void DrawCanvas(); protected: virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); }; //+------------------------------------------------------------------+ //| Check mouse drag events | //+------------------------------------------------------------------+ bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id != CHARTEVENT_MOUSE_MOVE) return false; if (!IsMouseDragging()) return false; if (m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) //First click { m_rel_mouse_x = m_inputs.X() - m_x; m_rel_mouse_y = m_inputs.Y() - m_y; return true; } //Move object m_x = m_inputs.X() - m_rel_mouse_x; m_y = m_inputs.Y() - m_rel_mouse_y; UpdatePosition(); m_program.RequestRedraw(); return true; } //+------------------------------------------------------------------+ //| Custom canvas draw function (fill with random color) | //+------------------------------------------------------------------+ void CDragElement::DrawCanvas(void) { m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256)); m_canvas.Update(false); }
オブジェクトの位置をそのグローバル位置に設定する必要があることに注意してください。チャートのオブジェクトは、その中の階層について何も知りません。今オブジェクトを動かそうとすると、それは機能し、子オブジェクトの位置も更新されますが、クリックしたオブジェクトの後ろにあるオブジェクトを動かすことができます。
すべてのイベントは再帰的にすべてのオブジェクトに送信されるため、他のオブジェクトの後ろに位置していても、オブジェクトはそれを受信します。あるオブジェクトが先にイベントを受信した場合、それをフィルタリングする方法を見つける必要があります。言い換えれば、それらをオクルージョンする必要があります。
オブジェクトがオクルージョンされているかどうかを追跡するブーリアン変数を作成します。
class CElement { private: //... bool m_occluded; //... public: //... void SetOccluded(bool occluded) { m_occluded = occluded; } bool IsOccluded() { return m_occluded; } //... };
そして、OnChartEventをオブジェクト間の「通信」手段として使うことができます。そのために、オブジェクトがイベントを受け取った場合にtrueとなるboolを返します。オブジェクトがドラッグされた場合(それに反応しなくても)、またはオブジェクトが(例えば子オブジェクトによって)オクルージョンされている場合も、下のオブジェクトのイベントをブロックするので、trueを返します。
bool CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) { m_children[i].SetOccluded(IsOccluded()); if (m_children[i].OnChartEvent(id, lparam, dparam, sparam)) SetOccluded(true); } //Check dragging start if (id == CHARTEVENT_MOUSE_MOVE && !IsOccluded()) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } return OnEvent(id, lparam, dparam, sparam) || IsMouseDragging() || IsOccluded(); }
オブジェクトのOnEventは、オクルージョンを考慮し、子オブジェクトのイベントの後に実行されます。最後に、カスタムドラッガブルオブジェクトにイベントフィルタリングを追加します。
bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (IsOccluded()) return false; if (id != CHARTEVENT_MOUSE_MOVE) return false; //... }
イベントを送信する際にフィルターをかけていないことにご注目ください。というのも、要素の中には、オクルージョンされていてもイベントに反応するものがあるからです(例えば、ホバーやマウスの位置だけに反応するイベント)。
これらの要素をすべて配置することで、この2つ目の例では望ましい動作が実現されました。各オブジェクトはドラッグ可能で、同時に上にある他のオブジェクトによってブロックされます。
ただし、この構造が今回の場合には有効であったとしても、より複雑な他のインターフェイスでは、いくつかの改良が必要になる可能性があることに注意すべきです。このような特殊な場合のためにコードが複雑になるのは割に合わないので、この記事では割愛します。例えば、子オブジェクトと兄弟オブジェクト(階層は同じだが順番は前)からのオクルージョンを区別したり、(予期せぬオクルージョンを避けるために)どのオブジェクトがイベントを受け取ったかを追跡し、次のフレームでそれを最初に確認するのも有効でしょう。
オブジェクトの表示と非表示
グラフィックライブラリに必要なもう1つの重要な機能は、オブジェクトを表示および非表示にする機能です。これは、例えば、ウィンドウを開いたり閉じたりするときや、ナビゲータタブのようにコンテンツを変更するとき、あるいは特定の条件下で使用できないボタンを削除するときなどに使用します。
これを実現する素朴な方法は、隠したり見せたりするたびにオブジェクトを削除したり作成したりするか、あるいはこの機能を完全に避けることでしょう。しかし、プロパティを使ってオブジェクトを非表示にする方法が1つだけあります(名前からはわかりませんが)。
ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); //Show ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //Hide
このプロパティが意味するのは、「オブジェクトがどの時間枠で表示され、どの時間枠で表示されないか」ということです。ある時間枠でのみオブジェクトを表示することもできますが、この文脈ではあまり必要ないと思います。
上述したObjectSetInteger関数呼び出しがあれば、GUIオブジェクト階層にオブジェクトを表示したり非表示にしたりする機能を実装できます。
class CElement { private: //... bool m_hidden; bool m_hidden_parent; void HideObject(); void HideByParent(); void HideChildren(); void ShowObject(); void ShowByParent(); void ShowChildren(); //... public: //... void Hide(); void Show(); bool IsHidden() { return m_hidden || m_hidden_parent; } }; //+------------------------------------------------------------------+ //| Display element (if parent is also visible) | //+------------------------------------------------------------------+ void CElement::Show(void) { if (!IsHidden()) return; m_hidden = false; ShowObject(); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } //+------------------------------------------------------------------+ //| Hide element | //+------------------------------------------------------------------+ void CElement::Hide(void) { m_hidden = true; if (m_hidden_parent) return; HideObject(); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } //+------------------------------------------------------------------+ //| Change visibility property to show (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowObject(void) { if (IsHidden()) //Parent or self return; ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); ShowChildren(); } //+------------------------------------------------------------------+ //| Show object when not hidden and parent is shown (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowByParent(void) { m_hidden_parent = false; ShowObject(); } //+------------------------------------------------------------------+ //| Show child objects recursively (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowChildren(void) { for (int i = 0; i < m_child_count; i++) m_children[i].ShowByParent(); } //+------------------------------------------------------------------+ //| Change visibility property to hide (not exposed) | //+------------------------------------------------------------------+ void CElement::HideObject(void) { ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); HideChildren(); } //+------------------------------------------------------------------+ //| Hide element when parent is hidden (not exposed) | //+------------------------------------------------------------------+ void CElement::HideByParent(void) { m_hidden_parent = true; if (m_hidden) return; HideObject(); } //+------------------------------------------------------------------+ //| Hide child objects recursively (not exposed) | //+------------------------------------------------------------------+ void CElement::HideChildren(void) { for (int i = 0; i < m_child_count; i++) m_children[i].HideByParent(); }
隠されるオブジェクトと、親によって隠されるオブジェクトを区別することは重要です。もしこれを作らなければ、デフォルトで非表示になっているオブジェクトは、親が非表示になったときに表示され、その後表示されることになります(これらの関数は再帰的に適用されるため)。または、オブジェクトをその親を非表示にして表示することもできることになります(背後にウィンドウのないボタンなど)。
このデザインでは、ShowとHideは、オブジェクトの可視性を変更するための外部から見える唯一の関数です。基本的に、関数は可視フラグを変更し、必要に応じてObjectSetPropertyを呼び出すために使用されます。また、子オブジェクトは再帰的にフラグを変更されます。不要な関数呼び出しを避けるためのガードチェックは他にもあります(例えば、子オブジェクトがすでに隠されているのに隠すなど)。最後に、可視性の変更を表示するにはチャートの再描画が必要なので、両方のケースでRequestRedrawを呼び出すことに注意すべきです。
また、作成時にオブジェクトを非表示にする必要があります。理論的には、オブジェクトは作成前に非表示としてマークすることができるからです。
void CElement::CreateChildren(void) { for (int i = 0; i < m_child_count; i++) { m_children[i].Create(); m_children[i].CreateChildren(); } if (IsHidden()) HideObject(); }
これらのコンポーネントがすべて揃ったので、非表示と表示機能をテストするための小さなデモを作ることができます。前回のカスタムクラス(ドラッグ可能なオブジェクト)を利用し、そこから新しいクラスを派生させます。この新しいクラスは、先ほどのドラッグイベントだけでなく、非表示状態を切り替えるキーボードイベントにも反応します。
class CHideElement : public CDragElement { private: int key_id; protected: virtual void DrawCanvas(); protected: virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); public: CHideElement(int id); }; //+------------------------------------------------------------------+ //| Hide element constructor (set keyboard ID) | //+------------------------------------------------------------------+ CHideElement::CHideElement(int id) : key_id(id) { } //+------------------------------------------------------------------+ //| Hide element when its key is pressed (inherit drag events) | //+------------------------------------------------------------------+ bool CHideElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) return drag; if (lparam == '0' + key_id) //Toggle hide/show { if (IsHidden()) Show(); else Hide(); } return true; } //+------------------------------------------------------------------+ //| Draw canvas function (fill with color and display number ID) | //+------------------------------------------------------------------+ void CHideElement::DrawCanvas(void) { m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256)); m_canvas.FontSet("Arial", 50); m_canvas.TextOut(25, 25, IntegerToString(key_id), ColorToARGB(clrWhite)); m_canvas.Update(false); }
オブジェクトを作成するときに、そのキーを押したときに可視性が切り替わるように、固有の番号ID(0から9まで)を設定します。また、物事を簡単にするために、オブジェクト自体にもIDを表示します。ドラッグイベントも最初に呼び出されます(呼び出さない場合、完全にオーバーライドされてしまいます)。
int OnInit() { MathSrand((uint)TimeLocal()); //100 is element size by default int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100; for (int i = 0; i < 10; i++) { CHideElement* drawing = new CHideElement(i); drawing.SetPosition(MathRand() % max_x, MathRand() % max_y); program.AddMainElement(drawing); } program.CreateGUI(); ChartRedraw(0); return(INIT_SUCCEEDED); }
さて、このプログラムを実行すると、キーボードで対応する番号を押したときに、オブジェクトが正しく隠れたり現れたりすることが確認できます。
Zオーダー(および再オーダー)
Zオーダーとは、オブジェクトが表示される順番のことです。簡単に言えば、X-Y座標がスクリーン上のオブジェクトの位置を決定するのに対し、Z座標はその深さや積み重ねの順番を決定します。Z値が小さいオブジェクトは、Z値が大きいオブジェクトの上に描画されます。
MetaTrader 5にZオーダーを自由に変更する方法がないことは、すでにご存知かもしれません。なぜなら、この名前のプロパティは、オブジェクトが重なっているときに、どのオブジェクトがクリックイベントを受け取るかを決定するために使用されるからです(しかし、視覚的なZオーダーとは何の関係もありません)。MetaTrader 5では、最近作成されたオブジェクトが常に上に配置されます(バックグラウンドに設定されている場合を除く)。
しかし、最後の例をいじってみると、あることに気づくかもしれません......。
オブジェクトを非表示にしたり表示したりすると、他のオブジェクトの上に再び表示されます。つまり、オブジェクトを瞬時に隠したり見せたりすることができ、それが他のオブジェクトの上に表示されるということでしょうか。そのとおりです。
これをテストするには、最後の派生クラス(CHideElement)を少し修正するだけでよくなります。そうすれば、可視性を切り替える代わりに、キーボード入力があるたびに、その特定のオブジェクトのZオーダーを上げるようになります。クラス名も変更します...
bool CRaiseElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) return drag; if (lparam == '0' + key_id) { ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } return true; }
そして、いつものことですが、Zオーダーが変更されると再描画が必要になることを忘れてはいけません。テストを実行すると、こうなります。
ご覧のように、私たちはシームレスにオブジェクトを自由に上げることができます。記事中のコードがさらに複雑になるのを避けるため、この特定の関数はライブラリに実装しません(また、その効果は、代わりにHideを呼び出し、その直後にShowを呼び出すことで得られます)。さらに、Zオーダーを使ってできることは他にもあります。オブジェクトを1レベルだけ上げたい場合はどうすればいいのでしょうか。あるいは、Zオーダーを一番下に落としたい場合はどうでしょう。どのような場合でも、解決策はraiseZorderを必要なオブジェクトの数だけ、期待される正しい順序(下から上へ)で呼び出すことです。最初の場合、レベルを1つ上げたいオブジェクトの関数を呼び出し、その上のすべてのオブジェクトの関数を、並び替えられた順番に呼び出します。2つ目の場合、すべてのオブジェクトに対してそうすることになります(Zオーダーで最下位になったものを無視することはできますが)。
とはいえ、このようなZオーダーシステムを実装する際には、ここで明示的に解決されるわけではありませんが(「読者のための練習として残しておく」と言う人もいるでしょう)、言及すべき引っ掛かりがあります。非表示になっているオブジェクトを表示して、同じフレーム内でそのZオーダーを変更することはできません。例えば、ウィンドウの中にボタンを表示し、同時にそのボタンを含むウィンドウを上げたいとします。ボタンに対してshowを呼び(そのボタンに対してOBJPROP_TIMEFRAMESをすべての期間に設定)、その後ウィンドウに対してraiseZを呼んだ(そのウィンドウに対してOBJPROP_TIMEFRAMESを期間なしに設定し、次にすべての期間に設定し、次にウィンドウ内のすべてのオブジェクトに対して正しい順序で設定)場合、ボタンはウィンドウの後ろに残ります。その理由は、OBJPROP_TIMEFRAMESプロパティの最初の修正だけが効果を持つため、ボタンオブジェクトが表示されたときだけ(次のraiseZでは表示されない)有効に上げられるからだと思われます。
この問題に対するひとつの解決策として、オブジェクトのキューを維持し、可視性やZオーダーの変化を確認し、それらをすべて1フレームにつき1回だけ実行することが考えられます。その場合、オブジェクトを直接表示するのではなく、「マーク」するようにShow関数を変更する必要があります。まだ始めたばかりであれば、このようなことはあまり起こらないし、致命的なことでもないので、あまり気にしないことをお勧めします(その時点でこの問題が発生する可能性のある状況は避けるべきですが)。
結論
この記事では、GUIライブラリを効果的に組み合わせるために知っておく必要があるいくつかの主要な機能を説明し、それぞれのポイントを証明するための小さな例を示しました。一般的にGUIライブラリの内部でどのように物事が動くかについての基本的な理解を提供するものでなければなりません。出来上がったライブラリは、決して完全に機能するものではなく、他の多くのGUIライブラリで使われている機能の一部を実証するために必要な最小限のものです。
出来上がったコードは比較的シンプルですが、GUIライブラリは、機能を追加したり、オブジェクトタイプを用意したりし始めると(特に、サブオブジェクトがあり、相互にイベント関係がある場合)、もっと複雑になる可能性があることに注意すべきです。また、他のライブラリの構造は、設計上の決定や希望する性能特定の機能によって、ここで説明したものとさまざまな違いがあるかもしれません。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13169




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