DoEasy. 控件 (第 32 部分): 水平滚动条,鼠标轮滚动
内容
概述
自上一篇撰写关于函数库的文章以来,时间已经过去了很久。在此期间,MetaTrader 5 交易终端得以支持新订单填单策略 — 被动/预订或取消(BOC),而 MQL5 则为矩阵和向量方法、以及 ONNX 模型获取了新的运行时错误代码。我们今天将把所有这些新增内容添加到函数库之中。
在上一篇文章中,我们最终创建了一个 WinForms 滚动条对象功能,允许我们使用位于滚动条边缘的箭头按钮拉滚动容器的内容。当然,这还不足以操控该对象,故在此我们将令其可用鼠标拖动滚动滑块来滚动容器里的内容。为此创建的方法将可令我们衔接所需功能,允许滚动鼠标滚轮来移动容器里的内容。
如果您将鼠标悬停在水平滚动条区域的上方,并滚动鼠标滚轮,这将导致向上滚动滚轮时(远离您)容器里的内容向右移动,向下滚动滚轮(朝向您)时向左移动。至少这是 Adobe PhotoShop 中水平滚动的工作方式,于此我们将执行完全相同的操作。将来,我将在库中添加指定鼠标滚轮水平(以及垂直)滚动容器内容方向的功能。
改进库类
一段时间以前,终端更新之后,一个烦人的错误浮出水面 — 所有文章里的函数库和示例文件停止编译。原因是,我在 Trading.mqh 文件中为 CTrading 类指定某些私密方法时粗心大意。相应地,从 CTrading 派生的 CTradingControl 类就无法访问这些方法。以前,我没有注意到编译器留下的这个错误,但在更新后它开始检测到它。修复方法很简单 — 我们需要把无法从派生类访问的私密方法指定为受保护,因而在继承类中它们可供调用。
打开文件 \MQL5\Include\DoEasy\Trading.mqh,并把位于私密部分的 SetPrices() 方法指定到受保护部分:
//--- 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);
声明方法之后,将私密部分返回到其位置,如此这般其它方法就不会落入受保护区域。
我之前已经对同一个类中的另外两个方法做了同样的事情:
//--- 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 类的私密方法的错误,您就可以按照上述改进实现,自行修复它。
由于现在有了新的订单执行策略,和新的运行时错误代码,我们需要将这些新创的描述添加到函数库文本消息数组当中。
打开文件 \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 模型的错误代码,我们将创建两个新数组,因为代码的初始值从 5700 和 5800 开始,以未用代码填充“空白区域”是不切实际的,也不是最优的,其中包含近一千条关于未知代码的雷同消息。甚至,出于同样的原因,该函数库针对不同错误代码组使用不同的数组,我们在代码为 5601 — 5626 的运行时错误消息数组之后再添加两个数组:
//+------------------------------------------------------------------+ //| 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\Message.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; } //+------------------------------------------------------------------+
现在就清楚了为什么在尝试市场深度收市时会发生错误。
此刻,如果我们单击滚动条对象中的箭头按钮,我们已拥有可移动容器内容功能。在创建它时,我们决定容器内容的移动量等于两个屏幕像素。这对于经由滚动按钮来舒适地定位可滚动内容已经足够了。但在此,我将创建另外两个滚动选项 — 用鼠标移动滚动条滑块,以及用滚轮滚动。
如果在移动滚动滑块时,我们不需要有一个预判值,容器内容物将按该值移动(偏移值将由滑块移动的量值设置),那么对于使用鼠标滚轮滚动,我们需要判定触发鼠标滚轮计数器时,容器内容一次性将移动多少个像素。计数器是离散值,当 Delta 值达到 120 或 -120(取决于滚轮方向)时发送事件。为了使用滚轮滚动,将该值设置为四个像素。
打开文件 \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 一起传递给事件处理程序 — 整数型、实数型、和字符串型。单击箭头按钮对象时,该对象的名称将作为字符串型参数传递。滚动鼠标滚轮时,不使用字符串型参数 — 那只是一个空字符串。这就是我们在按钮单击事件处理程序中会用到的东西,以便检测它是哪种事件 — 按钮单击或鼠标滚轮滚动。基于检测到的事件,我们将判定容器内容的移动量。单击箭头按钮时 — 两个像素;滚动鼠标滚轮时 — 四个像素。
在类的受保护部分中,声明虚拟的事件处理程序“光标位于活动区域内,鼠标滚轮正在滚动”:
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()); } //+------------------------------------------------------------------+
此处的一切都很简单。滚动鼠标滚轮时,滚轮滚动计数器的 Delta 值经由 dparam 实数型参数传递。
数值可以是 120 和 -120,具体取决于滚动方向。
如果 Delta(传递给 dparam)为正数,则事件视为单击右箭头按钮。
如果 Delta 为负数,则事件视为单击右箭头按钮将。
将此事件发送到对象事件处理程序(滚动条)。
由于我们在滚动条对象的 OnChartEvent() 处理程序中使用相同的代码模块来处理两个不同的事件(单击按钮和滚动鼠标滚轮),我们可以通过 sparam 参数的值来检测原始事件 — 当滚轮滚动时,此参数将包含一个空字符串,而当单击按钮时,它将包含其名称。在事件处理程序中,定义事件并根据事件标识设置容器内容的移动量(以像素为单位)。 对于按钮单机 — 两个偏移像素;对于滚动鼠标滚轮 — 四个:
//--- 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 的类文件,并在受保护部分声明两个鼠标事件处理程序:
//+------------------------------------------------------------------+ //| 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:
为什么我们需要两个处理程序呢?函数库的每个图形对象都包含负责其不同状态的区域,并发送与这些区域对应的事件。滚动条滑块对象沿周长的活动区域比滑块本身的大小少一个像素。相应地,当光标位于活动区域内时,对象活动区域处理程序处于激活状态,如果光标位于滑块的最边缘,则它将落入窗体内光标处理程序工作的另一个区域。
我们在类的主体之外编写这两个处理程序的实现。
实现“光标位于活动区域内,鼠标滚轮正在滚动”事件处理程序:
//+------------------------------------------------------------------+ //| '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,且不做任何修改。我们编译它,并在图表运行它,“自动调整容器大小以便适应其内容”标志设置为“否”:
我们检查一下所创建水平滚动条功能所有组件的操作:
一切都按计划工作。
下一步是什么?
在下一篇文章中,我们将转移到针对“垂直滚动条”控件创建功能。
*该系列的前几篇文章:
DoEasy. 控件 (第 26 部分): 完成 ToolTip(工具提示)WinForms 对象,并转移至 ProgressBar(进度条)开发
DoEasy. 控件 (第 27 部分): 继续致力 ProgressBar(进度条)WinForms 对象
DoEasy. 控件 (第 28 部分): 进度条控件中的柱线样式
DoEasy. 控件 (第 29 部分): 滚动条(ScrollBar)辅助控件
DoEasy. 控件 (第 30 部分): 动态滚动条控件
DoEasy. 控件(第三十一部分):滚动条控件内内容的滚动
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/12849