![DoEasy - コントロール(第32部):水平スクロールバー、マウスホイールスクロール](https://c.mql5.com/i/articles/overlay.png)
DoEasy - コントロール(第32部):水平スクロールバー、マウスホイールスクロール
内容
概念
前回ライブラリに関する記事を書いてから、かなりの時間が経過しました。この期間中、MetaTrader 5取引ターミナルでは、新しい約定ポリシー(パッシブ/ブックまたはキャンセル(BOC))がサポートされ、MQL5では行列およびベクトルメソッドとONNXモデルの新しいランタイムエラーコードが追加されました。今日は、これらをすべてライブラリに追加します。
前回の記事では、スクロールバーの端にある矢印ボタンを使ってコンテナの中身をスクロールできるWinFormsスクロールバーオブジェクト機能を作成しました。もちろん、これだけではこのオブジェクトを完全に扱うには不十分なので、ここではスクロールスライダーをマウスでドラッグしてコンテナの中身をスクロールできるようにしました。このために作られたメソッドによって、マウスホイールをスクロールさせることでコンテナの中身を移動させる機能を接続できるようになります。
水平スクロールバーのある領域にカーソルを合わせてマウスホイールをスクロールさせると、ホイールを上にスクロールさせたとき(自分から離れる方向)にはコンテナの内容が右に移動し、ホイールを下にスクロールさせたとき(自分に近づく方向)にはコンテナの内容が左に移動します。少なくともAdobe PhotoShopでは水平スクロールはこのように機能します。将来的には、コンテナの中身をマウスホイールで水平方向(垂直方向も)にスクロールさせる方向をライブラリで指定する機能を追加するつもりです。
ライブラリクラスの改善
しばらく前、ターミナルをアップデートした後、厄介なエラーが発生しました。ライブラリと記事のサンプルファイルがコンパイルできなくなったのです。原因は、Trading.mqhファイルでCTradingクラスのいくつかのメソッドにprivateセクションを指定する際の私の不注意でした。したがって、CTradingの派生クラスであるCTradingControlクラスは、そのようなメソッドにアクセスすることができませんでした。以前は、コンパイラは私のこのエラーに気づかずに放置していましたが、アップデート後は検出するようになりました。修正方法は簡単で、派生クラスからアクセスできないprivateメソッドにprotectedセクションを指定し、継承クラスで利用できるようにすることです。
\MQL5\Include\DoEasy\Trading.mqhファイルを開き、privateセクションにあるSetPrices()メソッドにprotectedセクションを指定します。
//--- Set the desired sound for a trading object void SetSoundByMode(const ENUM_MODE_SET_SOUND mode,const ENUM_ORDER_TYPE action,const string sound,CTradeObj *trade_obj); protected: //--- Set trading request prices template <typename PR,typename SL,typename TP,typename PL> bool SetPrices(const ENUM_ORDER_TYPE action,const PR price,const SL sl,const TP tp,const PL limit,const string source_method,CSymbol *symbol_obj); private: //--- Return the flag checking the permission to trade by (1) StopLoss, (2) TakeProfit distance, (3) order placement level by a StopLevel-based price bool CheckStopLossByStopLevel(const ENUM_ORDER_TYPE order_type,const double price,const double sl,const CSymbol *symbol_obj); bool CheckTakeProfitByStopLevel(const ENUM_ORDER_TYPE order_type,const double price,const double tp,const CSymbol *symbol_obj); bool CheckPriceByStopLevel(const ENUM_ORDER_TYPE order_type,const double price,const CSymbol *symbol_obj,const double limit=0);
メソッドを宣言した後、他のメソッドがprotectedメソッドに落ちないように、privateセクションをその場所に戻します。
同じクラスの他の2つのメソッドについても、少し前に同じことをしました。
//--- Return the error handling method ENUM_ERROR_CODE_PROCESSING_METHOD ResultProccessingMethod(const uint result_code); //--- Correct errors ENUM_ERROR_CODE_PROCESSING_METHOD RequestErrorsCorrecting(MqlTradeRequest &request,const ENUM_ORDER_TYPE order_type,const uint spread_multiplier,CSymbol *symbol_obj,CTradeObj *trade_obj); protected: //--- (1) Open a position, (2) place a pending order template<typename SL,typename TP> bool OpenPosition(const ENUM_POSITION_TYPE type, const double volume, const string symbol, const ulong magic=ULONG_MAX, const SL sl=0, const TP tp=0, const string comment=NULL, const ulong deviation=ULONG_MAX, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE); template<typename PR,typename PL,typename SL,typename TP> bool PlaceOrder( const ENUM_ORDER_TYPE order_type, const double volume, const string symbol, const PR price, const PL price_limit=0, const SL sl=0, const TP tp=0, const ulong magic=ULONG_MAX, const string comment=NULL, const datetime expiration=0, const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE); private: //--- Return the request object index in the list by (1) ID, //--- (2) order ticket, (3) position ticket in the request int GetIndexPendingRequestByID(const uchar id); int GetIndexPendingRequestByOrder(const ulong ticket); int GetIndexPendingRequestByPosition(const ulong ticket); public:
これまでの記事のサンプルファイルをコンパイルする際に、突然、CTradingクラスのprivateメソッドへのアクセスを拒否するエラーが発生した場合は、上記の改善策を実行することで、ご自分で修正してください。
新しい注文実行ポリシーと新しいランタイムエラーコードが追加されたので、ライブラリのテキストメッセージの配列に、これらのイノベーションの説明を追加する必要があります。
\MQL5\Include\DoEasy\Data.mqhファイルを開き、新しい実行ポリシーに関する新しいライブラリメッセージインデックを追加します。
MSG_LIB_TEXT_REQUEST_ORDER_FILLING_FOK, // Order is executed in the specified volume only, otherwise it is canceled MSG_LIB_TEXT_REQUEST_ORDER_FILLING_IOK, // Order is filled within an available volume, while the unfilled one is canceled MSG_LIB_TEXT_REQUEST_ORDER_FILLING_BOK, // Order is placed in the market depth and cannot be executed immediately. If the order can be executed immediately when placed, it is canceled MSG_LIB_TEXT_REQUEST_ORDER_FILLING_RETURN, // Order is filled within an available volume, while the unfilled one remains
新しく追加されたインデックスに対応するテキストメッセージ(ロシア語と英語)も追加します。
{"Ордер исполняется исключительно в указанном объеме, иначе отменяется (FOK)","The order is executed exclusively in the specified volume, otherwise it is canceled (FOK)"}, {"Ордер исполняется на доступный объем, неисполненный отменяется (IOK)","The order is executed on the available volume, the unfulfilled is canceled (IOK)"}, { "Ордер выставляется в стакан цен и не может быть исполнен немедленно. Если ордер может быть исполнен немедленно при выставлении, то он снимается (BOK)", "The order is placed in the Depth of Market and cannot be executed immediately. If the order can be executed immediately when placed, then it is canceled" }, {"Ордер исполняется на доступный объем, неисполненный остаётся (Return)","The order is executed at an available volume, unfulfilled remains in the market (Return)"},
MQL5で実装された新しいエラーコード(4020~4025)をランタイムエラーメッセージの配列(エラーコード0、4001~4019)に追加します。
//+------------------------------------------------------------------+ //| Array of execution time error messages (0, 4001 - 4025) | //| (1) in user's country language | //| (2) in the international language | //+------------------------------------------------------------------+ string messages_runtime[][TOTAL_LANG]= { {"Операция выполнена успешно","Operation successful"}, // 0 {"Неожиданная внутренняя ошибка","Unexpected internal error"}, // 4001 {"Ошибочный параметр при внутреннем вызове функции клиентского терминала","Wrong parameter in inner call of client terminal function"}, // 4002 {"Ошибочный параметр при вызове системной функции","Wrong parameter when calling system function"}, // 4003 {"Недостаточно памяти для выполнения системной функции","Not enough memory to perform system function"}, // 4004 { "Структура содержит объекты строк и/или динамических массивов и/или структуры с такими объектами и/или классы", // 4005 "The structure contains objects of strings and/or dynamic arrays and/or structure of such objects and/or classes" }, { "Массив неподходящего типа, неподходящего размера или испорченный объект динамического массива", // 4006 "Array of a wrong type, wrong size, or a damaged object of a dynamic array" }, { "Недостаточно памяти для перераспределения массива либо попытка изменения размера статического массива", // 4007 "Not enough memory for the relocation of an array, or an attempt to change the size of a static array" }, {"Недостаточно памяти для перераспределения строки","Not enough memory for relocation of string"}, // 4008 {"Неинициализированная строка","Not initialized string"}, // 4009 {"Неправильное значение даты и/или времени","Invalid date and/or time"}, // 4010 {"Общее число элементов в массиве не может превышать 2147483647","Total amount of elements in array cannot exceed 2147483647"}, // 4011 {"Ошибочный указатель","Wrong pointer"}, // 4012 {"Ошибочный тип указателя","Wrong type of pointer"}, // 4013 {"Системная функция не разрешена для вызова","Function not allowed for call"}, // 4014 {"Совпадение имени динамического и статического ресурсов","Names of dynamic and static resource match"}, // 4015 {"Ресурс с таким именем в EX5 не найден","Resource with this name not found in EX5"}, // 4016 {"Неподдерживаемый тип ресурса или размер более 16 MB","Unsupported resource type or its size exceeds 16 Mb"}, // 4017 {"Имя ресурса превышает 63 символа","Resource name exceeds 63 characters"}, // 4018 {"При вычислении математической функции произошло переполнение ","Overflow occurred when calculating math function "}, // 4019 {"Выход за дату окончания тестирования после вызова Sleep()","Out of test end date after calling Sleep()"}, // 4020 {"Неизвестный код ошибки (4021)","Unknown error code (4021)"}, // 4021 { "Тестирование было прекращено принудительно извне. Например, прервана оптимизацию, или закрыто окно визуального тестирования, или остановлен агент тестирования", "Test forcibly stopped from the outside. For example, optimization interrupted, visual testing window closed or testing agent stopped"}, // 4022 {"Неподходящий тип","Invalid type"}, // 4023 {"Невалидный хендл","Invalid handle"}, // 4024 {"Пул объектов заполнен","Object pool filled out"}, // 4025 }; //+------------------------------------------------------------------+
いくつかのエラーコードは(まだ?)使用されていませんが、その値は使用されているエラーコードの間に直接配置されています。あるエラーコードがMQL5で使用されるようになったことをすぐに理解するには、不明なエラーコードのメッセージにそのコード自体を追加するだけです。
ライブラリに「不明なエラーコード」が表示された場合(現在のように、変更を加える前に)、以前は使われていなかったどのコードが関係しているのか理解するのは難しくなります。メッセージにエラーコードを追加することで、このエラーの番号を示す不明なエラーコードに関するメッセージを即座に受け取ることができます。更新されたヘルプを開き、今と同じようにこのエラーについての説明をライブラリに補足すればよいのです。このファイルに追加された内容は、不明なエラーコードに関するすべてのメッセージにすでに追加されています。
ONNXモデルと同様に行列メソッドとベクトルメソッドのエラーコードについては、2つの新しい配列を作成します。これは、コードの初期値が5700と5800から始まり、未知のコードに関するほぼ 1,000 件の同一のメッセージで空のスペースを埋めるのは現実的ではなく、最適ではないためです。さらに、同じ理由で、ライブラリはエラーコードのグループごとに異なる配列を使っており、コード5601~5626のランタイムエラーメッセージの配列の後に、さらに2つの配列を追加しています。
//+------------------------------------------------------------------+ //| Array of execution time error messages (5601 - 5626) | //| (Working with databases) | //| (1) in user's country language | //| (2) in the international language | //+------------------------------------------------------------------+ string messages_runtime_sqlite[][TOTAL_LANG]= { {"Общая ошибка","Generic error"}, // 5601 {"Внутренняя логическая ошибка в SQLite","SQLite internal logic error"}, // 5602 {"Отказано в доступе","Access denied"}, // 5603 {"Процедура обратного вызова запросила прерывание","Callback routine requested abort"}, // 5604 {"Файл базы данных заблокирован","Database file locked"}, // 5605 {"Таблица в базе данных заблокирована ","Database table locked"}, // 5606 {"Сбой malloc()","Insufficient memory for completing operation"}, // 5607 {"Попытка записи в базу данных, доступной только для чтения ","Attempt to write to readonly database"}, // 5608 {"Операция прекращена с помощью sqlite3_interrupt() ","Operation terminated by sqlite3_interrupt()"}, // 5609 {"Ошибка дискового ввода-вывода","Disk I/O error"}, // 5610 {"Образ диска базы данных испорчен","Database disk image corrupted"}, // 5611 {"Неизвестный код операции в sqlite3_file_control()","Unknown operation code in sqlite3_file_control()"}, // 5612 {"Ошибка вставки, так как база данных заполнена ","Insertion failed because database is full"}, // 5613 {"Невозможно открыть файл базы данных","Unable to open the database file"}, // 5614 {"Ошибка протокола блокировки базы данных ","Database lock protocol error"}, // 5615 {"Только для внутреннего использования","Internal use only"}, // 5616 {"Схема базы данных изменена","Database schema changed"}, // 5617 {"Строка или BLOB превышает ограничение по размеру","String or BLOB exceeds size limit"}, // 5618 {"Прервано из-за нарушения ограничения","Abort due to constraint violation"}, // 5619 {"Несоответствие типов данных","Data type mismatch"}, // 5620 {"Ошибка неправильного использования библиотеки","Library used incorrectly"}, // 5621 {"Использование функций операционной системы, не поддерживаемых на хосте","Uses OS features not supported on host"}, // 5622 {"Отказано в авторизации","Authorization denied"}, // 5623 {"Не используется ","Not used "}, // 5624 {"2-й параметр для sqlite3_bind находится вне диапазона","Bind parameter error, incorrect index"}, // 5625 {"Открытый файл не является файлом базы данных","File opened that is not database file"}, // 5626 }; //+------------------------------------------------------------------+ //| Array of execution time error messages (5700 - 5706) | //| (Matrix and vector methods) | //| (1) in user's country language | //| (2) in the international language | //+------------------------------------------------------------------+ string messages_runtime_matrix_vector[][TOTAL_LANG]= { {"Внутренняя ошибка исполняющей подсистемы матриц/векторов","Internal error of the matrix/vector executing subsystem"}, // 5700 {"Матрица/вектор не инициализирован","Matrix/vector not initialized"}, // 5701 {"Несогласованный размер матриц/векторов в операции","Inconsistent size of matrices/vectors in operation"}, // 5702 {"Некорректный размер матрицы/вектора","Invalid matrix/vector size"}, // 5703 {"Некорректный тип матрицы/вектора","Invalid matrix/vector type"}, // 5704 {"Функция недоступна для данной матрицы/вектора","Function not available for this matrix/vector"}, // 5705 {"Матрица/вектор содержит нечисла (Nan/Inf)","Matrix/vector contains non-numbers (Nan/Inf)"}, // 5706 }; //+------------------------------------------------------------------+ //| Array of execution time error messages (5800 - 5808) | //| (ONNX models) | //| (1) in user's country language | //| (2) in the international language | //+------------------------------------------------------------------+ string messages_runtime_onnx[][TOTAL_LANG]= { {"Внутренняя ошибка ONNX стандарта","ONNX internal error"}, // 5800 {"Ошибка инициализации ONNX Runtime API","ONNX Runtime API initialization error"}, // 5801 {"Свойство или значение неподдерживаются языком MQL5","Property or value not supported by MQL5"}, // 5802 {"Ошибка запуска ONNX runtime API","ONNX runtime API run error"}, // 5803 {"В OnnxRun передано неверное количество параметров ","Invalid number of parameters passed to OnnxRun"}, // 5804 {"Некорректное значение параметра","Invalid parameter value"}, // 5805 {"Некорректный тип параметра","Invalid parameter type"}, // 5806 {"Некорректный размер параметра","Invalid parameter size"}, // 5807 {"Размерность тензора не задана или указана неверно","Tensor dimension not set or invalid"}, // 5808 }; //+------------------------------------------------------------------+ #ifdef __MQL4__
CMessageクラスは、テキストがメッセージ配列に格納されているライブラリメッセージを表示することができます。今日作成された新しい配列からのものも含めて、新しいテキストメッセージの取り扱いを追加する必要があります。
\MQL5\Include\DoEasy\Services\GetTextByID.mqhファイルを開き、新しく追加された配列をGetTextByID()メソッドに追加します。このメソッドは、メソッドに渡されたメッセージインデックスに格納されているテキストメッセージをm_text変数に書き込みます。
実行時エラー(0,4001~4019)については、処理されるコードの数を4019から4025に拡張し、また、行列メソッドとベクトルメソッドおよびONNXモデルのエラーコードについて、新しい配列からテキストメッセージを書き込みます。
//+------------------------------------------------------------------+ //| Get messages from the text array by an ID | //+------------------------------------------------------------------+ void CMessage::GetTextByID(const int msg_id) { CMessage::m_text= ( //--- Runtime errors (0, 4001 - 4025) msg_id==0 ? messages_runtime[msg_id][m_lang_num] : #ifdef __MQL5__ msg_id>4000 && msg_id<4026 ? messages_runtime[msg_id-4000][m_lang_num] : //--- Runtime errors (Charts 4101 - 4116) msg_id>4100 && msg_id<4117 ? messages_runtime_charts[msg_id-4101][m_lang_num] : //--- Runtime errors (Graphical objects 4201 - 4205) msg_id>4200 && msg_id<4206 ? messages_runtime_graph_obj[msg_id-4201][m_lang_num] : //--- Runtime errors (MarketInfo 4301 - 4305) msg_id>4300 && msg_id<4306 ? messages_runtime_market[msg_id-4301][m_lang_num] : //--- Runtime errors (Access to history 4401 - 4407) msg_id>4400 && msg_id<4408 ? messages_runtime_history[msg_id-4401][m_lang_num] : //--- Runtime errors (Global Variables 4501 - 4524) msg_id>4500 && msg_id<4525 ? messages_runtime_global[msg_id-4501][m_lang_num] : //--- Runtime errors (Custom indicators 4601 - 4603) msg_id>4600 && msg_id<4604 ? messages_runtime_custom_indicator[msg_id-4601][m_lang_num] : //--- Runtime errors (Account 4701 - 4758) msg_id>4700 && msg_id<4759 ? messages_runtime_account[msg_id-4701][m_lang_num] : //--- Runtime errors (Indicators 4801 - 4812) msg_id>4800 && msg_id<4813 ? messages_runtime_indicator[msg_id-4801][m_lang_num] : //--- Runtime errors (Market depth 4901 - 4904) msg_id>4900 && msg_id<4905 ? messages_runtime_books[msg_id-4901][m_lang_num] : //--- Runtime errors (File operations 5001 - 5027) msg_id>5000 && msg_id<5028 ? messages_runtime_files[msg_id-5001][m_lang_num] : //--- Runtime errors (Converting strings 5030 - 5044) msg_id>5029 && msg_id<5045 ? messages_runtime_string[msg_id-5030][m_lang_num] : //--- Runtime errors (Working with arrays 5050 - 5063) msg_id>5049 && msg_id<5064 ? messages_runtime_array[msg_id-5050][m_lang_num] : //--- Runtime errors (Working with OpenCL 5100 - 5114) msg_id>5099 && msg_id<5115 ? messages_runtime_opencl[msg_id-5100][m_lang_num] : //--- Runtime errors (Working with databases 5120 - 5130) msg_id>5119 && msg_id<5131 ? messages_runtime_database[msg_id-5120][m_lang_num] : //--- Runtime errors (Working with WebRequest() 5200 - 5203) msg_id>5199 && msg_id<5204 ? messages_runtime_webrequest[msg_id-5200][m_lang_num] : //--- Runtime errors (Working with network (sockets) 5270 - 5275) msg_id>5269 && msg_id<5276 ? messages_runtime_netsocket[msg_id-5270][m_lang_num] : //--- Runtime errors (Custom symbols 5300 - 5310) msg_id>5299 && msg_id<5311 ? messages_runtime_custom_symbol[msg_id-5300][m_lang_num] : //--- Runtime errors (Economic calendar 5400 - 5402) msg_id>5399 && msg_id<5403 ? messages_runtime_calendar[msg_id-5400][m_lang_num] : //--- Runtime errors (Working with databases 5601 - 5626) msg_id>5600 && msg_id<5627 ? messages_runtime_sqlite[msg_id-5601][m_lang_num] : //--- Runtime errors (Matrix and vector methods 5700 - 5706) msg_id>5699 && msg_id<5707 ? messages_runtime_matrix_vector[msg_id-5700][m_lang_num] : //--- Runtime errors (ONNX models 5800 - 5808) msg_id>5799 && msg_id<5809 ? messages_runtime_onnx[msg_id-5800][m_lang_num] : //--- Trade server return codes (10004 - 10045) msg_id>10003 && msg_id<10047 ? messages_ts_ret_code[msg_id-10004][m_lang_num] : #else // MQL4 msg_id>0 && msg_id<10 ? messages_ts_ret_code_mql4[msg_id][m_lang_num] : msg_id>63 && msg_id<66 ? messages_ts_ret_code_mql4[msg_id-54][m_lang_num] : msg_id>127 && msg_id<151 ? messages_ts_ret_code_mql4[msg_id-116][m_lang_num] : msg_id<4000 ? messages_ts_ret_code_mql4[26][m_lang_num] : //--- MQL4 runtime errors (4000 - 4030) msg_id<4031 ? messages_runtime_4000_4030[msg_id-4000][m_lang_num] : //--- MQL4 runtime errors (4050 - 4075) msg_id>4049 && msg_id<4076 ? messages_runtime_4050_4075[msg_id-4050][m_lang_num] : //--- MQL4 runtime errors (4099 - 4112) msg_id>4098 && msg_id<4113 ? messages_runtime_4099_4112[msg_id-4099][m_lang_num] : //--- MQL4 runtime errors (4200 - 4220) msg_id>4199 && msg_id<4221 ? messages_runtime_4200_4220[msg_id-4200][m_lang_num] : //--- MQL4 runtime errors (4250 - 4266) msg_id>4249 && msg_id<4267 ? messages_runtime_4250_4266[msg_id-4250][m_lang_num] : //--- MQL4 runtime errors (5001 - 5029) msg_id>5000 && msg_id<5030 ? messages_runtime_5001_5029[msg_id-5001][m_lang_num] : //--- MQL4 runtime errors (5200 - 5203) msg_id>5199 && msg_id<5204 ? messages_runtime_5200_5203[msg_id-5200][m_lang_num] : #endif //--- Library messages (ERR_USER_ERROR_FIRST) msg_id>ERR_USER_ERROR_FIRST-1 ? messages_library[msg_id-ERR_USER_ERROR_FIRST][m_lang_num] : messages_library[MSG_LIB_SYS_ERROR_CODE_OUT_OF_RANGE-ERR_USER_ERROR_FIRST][m_lang_num] ); } //+------------------------------------------------------------------+
ライブラリでよく使われる関数は\MQL5\Include\DoEasy\Services\DELib.mqhにあります。
約定モードの説明を返す関数に、BoC約定タイプの処理を追加します。
//+------------------------------------------------------------------+ //| Return the order filling mode description | //+------------------------------------------------------------------+ string OrderTypeFillingDescription(const ENUM_ORDER_TYPE_FILLING type) { return ( type==ORDER_FILLING_FOK ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_FOK) : type==ORDER_FILLING_IOC ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_IOK) : type==ORDER_FILLING_BOC ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_BOK) : type==ORDER_FILLING_RETURN ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_RETURN): type==WRONG_VALUE ? "WRONG_VALUE" : EnumToString(type) ); } //+------------------------------------------------------------------+
実行ポリシーがBookまたはCancelの場合、この関数は、今日追加したメッセージ配列のMSG_LIB_TEXT_REQUEST_ORDER_FILLING_BOKインデックスに対応するテキストを返すようになります。
EAがチャートから削除されると、市場深度の終了に関するエラーメッセージがログに表示されました。しかし、エラーの説明もコードもありませんでした。この動作は間違っていると思います。何が起きているのか不明です。この状況を修正するために、\MT5\MQL5\Include\DoEasy\Objects\Symbols\Symbol.mqhのCSymbol銘柄オブジェクトクラスで相場深度を閉じるメソッドを完成させます。
市場の深さを閉じるエラーを処理するコードブロックのエラーコードを取得し、エラーを説明した後にログに表示します。
//+-------------------------------------+ //| Close the market depth | //+-------------------------------------+ bool CSymbol::BookClose(void) { //--- If the DOM subscription flag is off, subscription is disabled (or not enabled yet). Return 'true' if(!this.m_book_subscribed) return true; //--- Save the result of unsubscribing from the DOM bool res=( #ifdef __MQL5__ ::MarketBookRelease(this.m_name) #else true #endif ); //--- If unsubscribed successfully, reset the DOM subscription flag and write the status to the object property if(res) { this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=false; ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_DEL)+" "+this.m_name); } else { this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=true; int err=::GetLastError(); ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_DEL)+": "+CMessage::Text(err)+" (",(string)err,")"); } //--- Return the result of unsubscribing from DOM return res; } //+------------------------------------------------------------------+
これで、市場深度を閉じようとしたときにエラーが発生した理由が明らかになりました。
現時点では、スクロールバーオブジェクトの矢印ボタンをクリックすると、コンテナの中身が移動する機能があります。これを作成する際、コンテナの中身を移動させる量は2スクリーンピクセルに等しいと決めました。スクロールボタンでスクロール可能なコンテンツを快適に配置するにはこれで十分ですが、2つのスクロールオプションを作成します。スクロールバーのスライダーをマウスで動かす方法と、ホイールでスクロールする方法です。
スクロールスライダーをシフトさせるときに、コンテナの内容がシフトされる所定の値を持つ必要がない場合(オフセット値は、スライダーがシフトされた量によって設定される)、マウスホイールでスクロールするために、マウスホイールカウンタが1回トリガーされたときにコンテナの内容が何ピクセルシフトされるかを決定する必要があります。カウンターは離散的で、デルタ値が120または-120(スクロールホイールの方向による)に達するとイベントを送信します。ホイールでスクロールする場合は、値を4ピクセルに設定します。
\MT5\MQL5\Include\DoEasy\Defines.mqhファイルを開き、矢印のボタンをクリックして、オフセット値を担当するマクロ置換名(DEF_CONTROL_SCROLL_BAR_SCROLL_STEP)を修正します。また、マウスホイールでスクロールするときのシフト量を担当する新しいマクロ置換を追加します。
#define DEF_CONTROL_SCROLL_BAR_WIDTH (11) // Default ScrollBar control width #define DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN (8) // Minimum size of the capture area (slider) #define DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK (2) // Shift step in pixels of the container content when scrolling by clicking the button #define DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL (4) // Shift step in pixels of the container content when scrolling with the mouse wheel #define DEF_CONTROL_CORNER_AREA (4) // Number of pixels defining the corner area to resize #define DEF_CONTROL_LIST_MARGIN_X (1) // Gap between columns in ListBox controls #define DEF_CONTROL_LIST_MARGIN_Y (0) // Gap between rows in ListBox controls
これで予備的な修正はすべて終わったので、スクロールバーの機能を最終決定しましょう。
まず、マウスでスクロールバーを動かしてコンテナの中身を移動できるようにしてみましょう。ロジックは以下の通りです。スクロールバーはコンテナとその内容を縮小して表示します。スライダーサイズは、コンテナの可視部分のサイズを表示し、スクロールバー(矢印ボタンの端の間)は、それを超えるコンテナの内容を表示します。計算の基礎となる開始点は、スライダーが左矢印ボタンの右端から移動する量です。また、スライダーのサイズとコンテナの可視部分のサイズもあります。コンテナの見える部分がスライダーのサイズよりどれだけ大きいか、そしてスライダーがどれだけ動かされたかを知ることで、コンテナの中身をどれだけ動かせばいいかを計算できます。
- コンテナの見える部分の幅(W1)
- スライダー幅(W2)
- コンテナの見える部分がスライダーよりどれだけ大きいか(X = W1 / W2)
- スライダーの移動量(S1)
- コンテナの中身をどれだけ移動させるか(S1*X)
したがって、スライダーのサイズとコンテナの可視部分の比率、そしてスライダーがどれだけ移動したかを知ることで、コンテナの中身を移動させるべき量を計算します。この場合、スライダーが左に移動すれば、コンテナの中身は右に移動するはずであり、逆もまた然りです。
水平スクロールバークラスのファイル(MT5\MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarHorisontal.mqh)を開き、そのイベントハンドラに、これらの計算とコンテナの中身を移動するコードブロックを記述します。
//+-------------------------------------+ //| Event handler | //+-------------------------------------+ void CScrollBarHorisontal::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Adjust subwindow Y shift CGCnvElement::OnChartEvent(id,lparam,dparam,sparam); //--- Get the pointers to control objects of the scrollbar CArrowLeftButton *buttl=this.GetArrowButtonLeft(); CArrowRightButton *buttr=this.GetArrowButtonRight(); CScrollBarThumb *thumb=this.GetThumb(); if(buttl==NULL || buttr==NULL || thumb==NULL) return; //--- If the event ID is an object movement if(id==WF_CONTROL_EVENT_MOVING) { //--- Move the scrollbar to the foreground this.BringToTop(); //--- Declare the variables for the coordinates of the capture area int x=(int)lparam; int y=(int)dparam; //--- Set the Y coordinate equal to the Y coordinate of the control element y=this.CoordY()+this.BorderSizeTop(); //--- Adjust the X coordinate so that the capture area does not go beyond the control, taking into account the arrow buttons if(x<buttl.RightEdge()) x=buttl.RightEdge(); if(x>buttr.CoordX()-thumb.Width()) x=buttr.CoordX()-thumb.Width(); //--- If the capture area object is shifted by the calculated coordinates if(thumb.Move(x,y,true)) { //--- set the object relative coordinates thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX()); thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY()); } //--- Get the pointer to the base object CWinFormBase *base=this.GetBase(); if(base!=NULL) { //--- Check if the content goes beyond the container base.CheckForOversize(); //--- Calculate the distance the slider is from the left border of the scrollbar (from the right side of the left arrow button) int distance=thumb.CoordX()-buttl.RightEdge(); //--- Declare a variable that stores the distance value before the slider shift static int distance_last=distance; //--- Declare a variable that stores the value in screen pixels the slider was shifted by int shift_value=0; //--- If the values of the past and current distances are not equal (the slider is shifted), if(distance!=distance_last) { //--- calculate the value the slider is shifted by shift_value=distance_last-distance; //--- and enter the new distance into the value of the previous distance for the next calculation distance_last=distance; } //--- Get the largest and smallest coordinates of the right and left sides of the base object content int cntt_r=(int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT); int cntt_l=(int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X); //--- Get the coordinate offset of the left side of the base object content //--- relative to the initial coordinate of the base object working area int extl=base.CoordXWorkspace()-cntt_l; //--- Calculate the relative value of the desired coordinate, //--- where the contents of the base object, shifted by the slider, should be located double x=(double)this.WidthWorkspace()*(double)distance/double(thumb.Width()!=0 ? thumb.Width() : DBL_MIN); //--- Calculate the required shift value of the base object content along the above calculated coordinate int shift_need=extl-(int)::round(x); //--- If the slider is shifted to the left (positive shift value) if(shift_value>0) { if(cntt_l+shift_need<=base.CoordXWorkspace()) base.ShiftDependentObj(shift_need,0); } //--- If the slider is shifted to the right (negative shift value) if(shift_value<0) { if(cntt_r-shift_need>=base.RightEdgeWorkspace()) base.ShiftDependentObj(shift_need,0); } ::ChartRedraw(this.ChartID()); } } //--- If any scroll button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT || id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { //--- Move the scrollbar to the foreground this.BringToTop(); //--- Get the base object CWinFormBase *base=this.GetBase(); if(base==NULL) return; //--- Calculate how much each side of the content of the base object goes beyond its borders base.CheckForOversize(); //--- Get the largest and smallest coordinates of the right and left sides of the base object content int cntt_r=(int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT); int cntt_l=(int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X); //--- Set the number of pixels, by which the content of the base object should be shifted int shift=(sparam!="" ? DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK : DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL); //--- If the left button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT) { if(cntt_l+shift<=base.CoordXWorkspace()) base.ShiftDependentObj(shift,0); } //--- If the right button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { if(cntt_r-shift>=base.RightEdgeWorkspace()) base.ShiftDependentObj(-shift,0); } //--- Calculate the width and coordinates of the slider this.SetThumbParams(); } } //+------------------------------------------------------------------+
追加されたコードブロックのほとんどすべての行にコメントが添えられています。スライダーを左に動かすと、コンテナの中身が右に移動することに注意してください。同時に、コンテンツの左端がコンテナの作業領域(コンテナのコンテンツが表示される領域)の左端の右に移動しないように、コンテナのコンテンツの移動を制限する必要があります。スライダーを右に動かすときと同じように、コンテナの中身を左に移動させます。同時に、コンテンツの右端がコンテナの作業領域の右端より左に行ってはいけません。これらはすべて、コンテナの中身を移動させる前に、計算された座標を確認することでおこなわれます。
キャプチャ領域のパラメータを計算して設定するメソッドでは、%での計算をやめて、可視部分ウィンドウの相対的なサイズの計算を少し変更します。代わりに、コンテナワークスペースのサイズとスライダーのサイズの比率(一方のサイズが他方のサイズよりどれだけ大きいか)を計算します。
//+------------------------------------------------------------------+ //| Calculate and set the parameters of the capture area (slider) | //+------------------------------------------------------------------+ int CScrollBarHorisontal::SetThumbParams(void) { //--- Get the base object CWinFormBase *base=this.GetBase(); if(base==NULL) return 0; //--- Get the capture area object (slider) CScrollBarThumb *thumb=this.GetThumb(); if(thumb==NULL) return 0; //--- Get the width size of the visible part inside the container int base_w=base.WidthWorkspace(); //--- Calculate the total width of all attached objects int objs_w=base_w+base.OversizeLeft()+base.OversizeRight(); //--- Calculate the relative size of the visible part window double px=(double)base_w/double(objs_w!=0 ? objs_w : 1); //--- Calculate and adjust the size of the slider relative to the width of its workspace (not less than the minimum size) int thumb_size=(int)::floor(this.BarWorkAreaSize()*px); if(thumb_size<DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN) thumb_size=DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN; if(thumb_size>this.BarWorkAreaSize()) thumb_size=this.BarWorkAreaSize(); //--- Calculate the coordinate of the slider and change its size to match the previously calculated one int thumb_x=this.CalculateThumbAreaDistance(thumb_size); if(!thumb.Resize(thumb_size,thumb.Height(),true)) return 0; //--- Shift the slider by the calculated X coordinate if(thumb.Move(this.BarWorkAreaCoord()+thumb_x,thumb.CoordY())) { thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX()); thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY()); } //--- Return the calculated slider size return thumb_size; } //+------------------------------------------------------------------+
スライダーの最小サイズの調整とは別に、その最大サイズも修正します。スライダー幅の計算値は、スクロールバーの作業領域の実際のサイズ(左矢印ボタンと右矢印ボタンの間の距離)を超えてはなりません。
前の記事のEAを今回のライブラリの改良でコンパイルすると、スクロールバーのスライダーを動かすことで、コンテナの中身が逆方向に移動します。
ここで、コンテナの中身をマウスホイールでスクロールできるようにする必要があります。多くのプログラムでは、マウスホイールのスクロールはコンテナの内容を垂直方向に移動させるだけです(ブラウザページやテキストエディタなど)。水平スクロールバーにカーソルを合わせてマウスホイールをスクロールさせると、コンテナの中身が水平方向に移動します。私には論理的に思えます。垂直方向の変位が必要な場合は、マウスホイールを垂直スクロールバーまたはコンテナの中身の領域でスクロールさせる必要があります。しかし、カーソルが水平スクロールバー上にある場合は、マウスホイールの回転によってコンテナの内容が水平方向に変位することを期待するのが論理的です。少なくともAdobe PhotoShopではそうなっており、これは論理的で便利です。
この機能を実装する準備はほとんど整っています。スクロールバーの矢印ボタンがクリックされたときに、コンテナの中身を移動させるハンドラがあります。スクロールバー領域でマウスホイールがスクロールすると、シフトボタンにクリックイベントを送信します。ホイールのスクロール方向に応じて、左矢印ボタンか右矢印ボタンのクリックイベントを送信します。
また、イベントIDとともに、整数、実数、文字列といったパラメータもイベントハンドラに渡されます。矢印ボタンオブジェクトがクリックされると、オブジェクトの名前が文字列パラメータとして渡されます。マウスホイールがスクロールされると、文字列パラメータは使用されず、そこには空の文字列が存在します。これは、ボタンクリックのイベントハンドラで、それがどのようなイベントであったかを判断するために使うものです。検出されたイベントに基づいて、コンテナの中身をどれだけ移動させるかを決定します。矢印ボタンをクリックする場合は2ピクセル分、マウスホイールをスクロールする場合は4ピクセル分です。
クラスのprotectedセクションで、「仮想イベントハンドラカーソルがアクティブ領域内にあり、マウスホイールがスクロールされている」仮想イベントハンドラを宣言します。
protected: //--- Protected constructor with object type, chart ID and subwindow CScrollBarHorisontal(const ENUM_GRAPH_ELEMENT_TYPE type, CGCnvElement *main_obj,CGCnvElement *base_obj, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h); //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler virtual void MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); public:
グラフィック要素のすべてのクラスは、CFormフォームオブジェクトクラスの子孫です。継承したクラスで再定義する必要がある、さまざまなマウスイベント用の空のハンドラをすでに備えています。ここでは、このオブジェクトのアクティブ領域内でマウスホイールのスクロールイベントハンドラを再定義します。
クラス本体外にその実装を記述しましょう。
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| the mouse wheel is being scrolled | //+------------------------------------------------------------------+ void CScrollBarHorisontal::MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { ENUM_WF_CONTROL_EVENT evn=(dparam>0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT); this.OnChartEvent(evn,lparam,dparam,sparam); ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
ここではすべてが簡単です。マウスホイールをスクロールさせる場合、ホイールスクロールカウンタのデルタ値がdparam realパラメータに渡されます。
値はスクロールの方向によって120と-120になります。
Delta(dparamに渡される)が正の場合、右矢印ボタンのクリックはイベントとみなされます。
Deltaが負の場合、右矢印ボタンのクリックはイベントとみなされます。
このイベントをオブジェクトのイベントハンドラ(スクロールバー)に送ります。
スクロールバーオブジェクトのOnChartEvent()ハンドラで同じコードブロックを使って2つの異なるイベント(ボタンのクリックとマウスホイールのスクロール)を処理しているので、sparamパラメータの値によって元のイベントを判断することができます。ホイールがスクロールされると、このパラメータは空の文字列を含み、ボタンがクリックされると、その名前を含みます。イベントハンドラで、イベントを定義し、特定されたイベントに応じてコンテナコンテンツのシフトサイズをピクセル単位で設定します。ボタンクリックの場合は2ピクセルオフセット、マウスホイールのスクロールの場合は4ピクセルオフセットです。
//--- If any scroll button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT || id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { //--- Move the scrollbar to the foreground this.BringToTop(); //--- Get the base object CWinFormBase *base=this.GetBase(); if(base==NULL) return; //--- Calculate how much each side of the content of the base object goes beyond its borders base.CheckForOversize(); //--- Get the largest and smallest coordinates of the right and left sides of the base object content int cntt_r=(int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT); int cntt_l=(int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X); //--- Set the number of pixels, by which the content of the base object should be shifted int shift=(sparam!="" ? DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK : DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL); //--- If the left button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT) { if(cntt_l+shift<=base.CoordXWorkspace()) base.ShiftDependentObj(shift,0); } //--- If the right button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { if(cntt_r-shift>=base.RightEdgeWorkspace()) base.ShiftDependentObj(-shift,0); } //--- Calculate the width and coordinates of the slider this.SetThumbParams(); } } //+------------------------------------------------------------------+
ここで、前回のEAをもう一度コンパイルし、カーソルがスクロールバー上にある状態でマウスホイールをスクロールさせようとすると、カーソルがスライダー上にない場合にのみコンテンツが移動します。
カーソルがスライダーの上にある場合、スクロールはおこなわれません。なぜでしょうか。単に、スクロールバースライダーオブジェクトがアクティブになり、そのようなイベントのハンドラを持っていないからです。追加しましょう。
スクロールバースライダーオブジェクトのクラスファイル(\MT5\MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarThumb.mqh)を開き、protectedセクションで2つのマウスイベントハンドラを宣言します。
//+------------------------------------------------------------------+ //| ScrollBarThumb object class of the WForms controls | //+------------------------------------------------------------------+ class CScrollBarThumb : public CButton { private: protected: //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler virtual void MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, a mouse button is clicked (any)' event handler virtual void MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, the left mouse button is clicked' event handler virtual void MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler virtual void MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler virtual void MouseInsideWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Protected constructor with object type, chart ID and subwindow CScrollBarThumb(const ENUM_GRAPH_ELEMENT_TYPE type, CGCnvElement *main_obj,CGCnvElement *base_obj, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h); public:
なぜ2つのハンドラが必要なのでしょうか。ライブラリの各グラフィカルオブジェクトには、それぞれの状態を担当する領域があり、これらの領域に対応するイベントが送信されます。スクロールバースライダーオブジェクトの周囲に沿ったアクティブ領域は、スライダー自体のサイズより1ピクセル小さくなります。従って、カーソルがアクティブ領域内にあるときは、オブジェクトのアクティブ領域ハンドラがアクティブになり、カーソルがスライダーの端にあるときは、フォーム内のカーソル用ハンドラが働く別のゾーンに落ち入ります。
これら2つのハンドラの実装をクラス本体の外に書いてみましょう。
以下は、「カーソルがアクティブ領域内にあり、マウスホイールがスクロール中」イベントハンドラの実装です。
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| the mouse wheel is being scrolled | //+------------------------------------------------------------------+ void CScrollBarThumb::MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { CWinFormBase *base=this.GetBase(); if(base==NULL) return; base.BringToTop(); ENUM_WF_CONTROL_EVENT evn=(dparam>0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT); base.OnChartEvent(evn,lparam,dparam,sparam); ::ChartRedraw(base.ChartID()); } //+------------------------------------------------------------------+
ハンドラのロジックは、前述のスクロールバーオブジェクトのハンドラのロジックと同じです。しかし、ここではまず、スライダーのベースオブジェクト(スクロールバーオブジェクト)へのポインタを取得します。次に、スクロールバーを最前面に移動します(そのスライダーも最前面に移動します)。次に、マウスホイールをスクロールさせる方向に必要なイベントを決定し、対応するイベントを基本オブジェクト(スクロールバー)のイベントハンドラに送ります。その結果、変化を即座に表示するためにチャートを再描画します。
「カーソルがフォーム内にあり、マウスホイールがスクロールしている」イベントのハンドラを実装します。
このハンドラは、上で説明したハンドラと完全に同一であるべきなので、このハンドラに渡されたパラメータで最初のハンドラを呼び出すだけですみます。
//+------------------------------------------------------------------+ //| 'The cursor is inside the form, | //| the mouse wheel is being scrolled | //+------------------------------------------------------------------+ void CScrollBarThumb::MouseInsideWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { this.MouseActiveAreaWhellHandler(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+
水平スクロールバーの機能の準備ができました。結果をテストしてみましょう。
検証
テストをおこなうために、前回の記事のEAをそのまま使用します。これをコンパイルして、コンテナの自動サイズ変更を「No」に設定してチャート上で実行してみましょう。
作成した水平スクロールバー機能のすべてのコンポーネントの動作を確認してみましょう。
すべてが計画通りに動いています。
次の段階
次回は、作成した機能を「垂直スクロールバー」コントロールに移します。
連載のこれまでの記事
DoEasy - コントロール(第26部):ToolTipWinFormsオブジェクトの最終確認とProgressBar開発の開始
DoEasy - コントロール(第27部):ProgressBarWinFormsオブジェクトでの作業
DoEasy - コントロール(第28部):ProgressBarコントロールのバースタイル
DoEasy - コントロール(第29部):ScrollBar補助コントロール
DoEasy - コントロール(第30部):ScrollBarコントロールのアニメーション化
DoEasy - コントロール(第31部):ScrollBarバーコントロールのコンテンツののスクロール
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/12849
![MQL5のプログラム構造について学ぶ必要があるすべて](https://c.mql5.com/2/57/about_mql5_program_structure_avatar.png)
![MQL4およびMQL5開発のフレームワーク内のOpenAI ChatGPT機能](https://c.mql5.com/2/55/mql5-openai-avatar.png)
![リプレイシステムの開発—市場シミュレーション(第5回):プレビューの追加](https://c.mql5.com/2/53/replay-p5-avatar.png)
![MQL5 - MetaTrader 5クライアントターミナルにビルトインされたトレードストラテジーの言語](https://c.mql5.com/i/registerlandings/logo-2.png)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索