DoEasy. Controls (Part 32): Horizontal ScrollBar, mouse wheel scrolling
Contents
Concept
A lot of time has passed since the last article on the library was written. During this period, the MetaTrader 5 trading terminal received support for a new order filling policy — Passive / Book or Cancel (BOC), while MQL5 acquired new runtime error codes for the matrix and vector methods and ONNX models. We will add all these additions to the library today.
In the previous article, we ended up creating a WinForms scrollbar object functionality that allows us to scroll the contents of the container using the arrow buttons located at the edges of the scrollbar. This, of course, is not enough to fully work with this object, so here we will make it possible to scroll the contents of the container by dragging the scroll slider with the mouse. The methods created for this will enable us to connect the functionality, allowing to shift the contents of the container by scrolling the mouse wheel.
If you hover over an area with a horizontal scroll bar and scroll the mouse wheel, this will cause the contents of the container to shift to the right when scrolling the wheel up (away from you), and to the left when scrolling the wheel down (towards you). At least this is how horizontal scrolling works in Adobe PhotoShop, and here we will do exactly the same. In the future, I will add the ability to specify in the library the direction of horizontal (and also vertical) scrolling of the contents of the container with the mouse wheel.
Improving library classes
Some time ago, after updating the terminal, an annoying error surfaced - the library and example files for articles stopped compiling. The reason was my carelessness when specifying the private section for some methods of the CTrading class in the Trading.mqh file. Accordingly, the CTradingControl class, which is a derived class from CTrading, could not access such methods. Previously, the compiler left this error of mine unnoticed, but after the update it started detecting it. The fix is simple - we need to specify a protected section for private methods that are inaccessible from derived classes so that they become available in the inherited class.
Open the file \MQL5\Include\DoEasy\Trading.mqh and specify the protected section for the SetPrices() method located in the private section:
//--- 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);
After declaring the method, return the private section to its place, so that other methods do not fall into the protected one.
I already did the same with two other methods in the same class a bit earlier:
//--- 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:
If suddenly, when compiling the example files for the previous articles, you encounter an error of denying access to private methods of the CTrading class, you can fix it yourself by implementing the above improvements.
Since now there is a new order execution policy and new runtime error codes, we need to add a description of these innovations to the arrays of library text messages.
Open the file \MQL5\Include\DoEasy\Data.mqh and add the new library message index about the new execution policy:
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
and the text messages (Russian and English) corresponding to the newly added index:
{"Ордер исполняется исключительно в указанном объеме, иначе отменяется (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)"},
Add the new error codes implemented in MQL5 (from 4020 to 4025) to the array of runtime error messages (error codes 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 }; //+------------------------------------------------------------------+
Some error codes are not used (yet?), but their values are located directly between the used error codes. To quickly understand that a certain error code is now used in MQL5, simply add the code itself to the unknown error code message.
If the library displays "Unknown error code" (as it does now, before making changes), it is difficult to understand which code from the previously unused one has now become involved. By adding an error code to the message, we will immediately receive a message about an unknown error code indicating the number of this error. We will only have to open the updated help and supplement the library with a description of this error in the same way as we do now. Such additions in this file are already added to all messages about an unknown error code.
For the error codes of the matrix and vector methods, as well as ONNX models, we will create two new arrays, since the initial values of the codes start from 5700 and 5800 and it is not practical and not optimal to fill the "empty space" with unused codes with almost a thousand identical messages about an unknown code. Moreover, for the same reason, the library uses different arrays for different groups of error codes, and we add two more arrays after the array of runtime error messages with the codes 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__
The CMessage class allows us to display library messages whose texts are stored in message arrays. We need to add handling new text messages, including the ones from the new arrays created today.
Open the file \MQL5\Include\DoEasy\Services\Message.mqh and add handling the newly added arrays to the GetTextByID() method. The method writes the text message stored by the message index, passed to the method, to the m_text variable.
For the runtime errors (0, 4001 — 4019), expand the number of handled codes from 4019 to 4025, as well as write text messages from the new arrays for error codes of matrix and vector methods and ONNX models:
//+------------------------------------------------------------------+ //| 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] ); } //+------------------------------------------------------------------+
Some common functions used in the library are set in \MQL5\Include\DoEasy\Services\DELib.mqh.
Add handling the BoC filling type to the function that returns the description of the order filling mode:
//+------------------------------------------------------------------+ //| 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) ); } //+------------------------------------------------------------------+
Now, if the execution policy is Book or Cancel, then the function will return the text corresponding to the MSG_LIB_TEXT_REQUEST_ORDER_FILLING_BOK index in the message array that we added today.
When the EA was removed from the chart, an error message about the closing of the market depth was displayed in the log. However, there was neither an error description, nor its code. I think this behavior is wrong - it is unclear what is happening. To correct the situation, we will finalize the method that closes the market depth in the CSymbol symbol object class in \MT5\MQL5\Include\DoEasy\Objects\Symbols\Symbol.mqh.
Get the error code in the code block that handles the error of closing the market depth and and display it in the log after describing the error:
//+-------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Now it will be clear why the error occurred when trying to close the market depth.
At the moment, we have the functionality of shifting the contents of the container if we click on the arrow buttons in the scrollbar object. When creating it, we decided that the amount by which the contents of the container would be shifted would be equal to two screen pixels. This is quite sufficient for comfortable positioning of the scrollable content with the scroll buttons. But here I am going to create two more scrolling options - moving the scrollbar slider with the mouse and scrolling with the wheel.
If, when shifting the scroll slider, we do not need to have a predetermined value, by which the contents of the container will be shifted (the offset value will be set by the amount, by which the slider was shifted), then for scrolling with the mouse wheel, we need to determine by how many pixels the contents of the container will be shifted at a one-time triggering of the mouse wheel counter. The counter is discrete and sends an event when the Delta value reaches 120 or -120 (depending on the scroll wheel direction). For scrolling with the wheel, set the value to four pixels.
Open the file \MT5\MQL5\Include\DoEasy\Defines.mqh and fix the name of the macro substitution responsible for the offset value by clicking on the button with the arrow (DEF_CONTROL_SCROLL_BAR_SCROLL_STEP). Also, add a new macro substitution responsible for the amount of shift when scrolling with the mouse wheel :
#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
All preliminary fixes have now been made, let's finalize the scrollbar functionality.
First, let's make it possible to shift the contents of the container by moving the scrollbar with the mouse. The logic is as follows: the scrollbar displays the container and its contents in a reduced form. The slider size displays the size of the visible part of the container, and the scrollbar (between the edges of the arrow buttons) displays the content of the container that goes beyond it. We have a starting point to base our calculations on - the amount, by which the slider is moved from the right edge of the left arrow button. We also have the size of the slider and the size of the visible part of the container. Knowing how much the visible part of the container is larger than the size of the slider and how much the slider has been moved, we can calculate how much to move the contents of the container:
- Width of the visible part of the container (W1)
- Slider width (W2)
- How much the visible part of the container is larger than the slider (X = W1 / W2)
- How much the slider is shifted (S1)
- How much to shift the contents of the container (S1 * X)
Thus, knowing the ratio of the size of the slider and the visible part of the container, and how much the slider was shifted, we calculate the amount, by which the contents of the container should be shifted. In this case, if the slider is shifted to the left, then the contents of the container should be moved to the right, and vice versa.
Open the file of the horizontal scrollbar class \MT5\MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarHorisontal.mqh and write the code block, which performs these calculations and shifts the contents of the container, in its event handler:
//+-------------------------------------+ //| 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(); } } //+------------------------------------------------------------------+
Almost every line of the added code block is accompanied by a comment. Note that when we move the slider to the left, we shift the contents of the container to the right. At the same time, it is necessary to limit the shift of the container content so that the left edge of the content does not move to the right of the left edge of the container working area - the area, inside which the content of the container is visible. Just as when moving the slider to the right, we shift the contents of the container to the left, and at the same time, the right edge of the content should not go to the left of the right edge of the container working area. All this is done by checking the calculated coordinates before shifting the contents of the container.
In the method that calculates and sets the parameters of the capture area, we will slightly change the calculation of the relative size of the visible part window by getting away from calculations in %. Instead, we will calculate the ratio of the size of the container workspace to the size of the slider (how much one size is larger than the other):
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Apart from adjusting the slider minimum size, let's also amend its maximum size — the calculated value of the slider width should not exceed the actual size of the scrollbar working area (the distance between the left and right arrow buttons).
If we now compile the EA from the previous article with the current library improvements, then we will accordingly shift the contents of the container in the opposite direction by moving the scrollbar slider:
Now we need to make it possible to scroll the contents of the container with the mouse wheel - this is convenient and familiar. In many programs, scrolling the mouse wheel causes only a vertical shift in the contents of the container (like in browser pages or text editors). But it will be a little different in our case - when we hover over the horizontal scroll bar and scroll the mouse wheel, we will move the contents of the container horizontally. It seems logical to me. If vertical displacement is required, then the mouse wheel should be scrolled in the area of the vertical scrollbar or the contents of the container. But if the cursor is on a horizontal scrollbar, then it is logical to expect a horizontal displacement of the container contents from the rotation of the mouse wheel. At least that is how it is done in Adobe PhotoShop, and it is logical and convenient.
Almost everything is ready to implement this functionality. We have a handler that shifts the contents of the container when the arrow buttons on the scrollbar are clicked. When the mouse wheel scrolls in the scrollbar area, we will send a click event to the shift buttons. Depending on the direction of the wheel scrolling, we will send either a click event on the left or right arrow button.
Some parameters are also passed to the event handler along with the event ID - integer, real and string. When an arrow button object is clicked, the name of the object is passed as a string parameter. When the mouse wheel is scrolled, the string parameter is not used - there is an empty string there. This is what we will use in the button click event handler to determine what kind of event it was - either a button click or mouse wheel scrolling. Based on the detected event, we will determine how much to shift the contents of the container. When clicking on the arrow button - by two pixels and when scrolling the mouse wheel - by four.
In the protected section of the class, declare the virtual event handler "The cursor is inside the active area, the mouse wheel is being scrolled":
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:
All classes of graphic elements are descendants of the CForm form object class. It already features empty handlers for various mouse events that need to be redefined in inherited classes. Here we redefine the mouse wheel scrolling event handler inside the active area of this object.
Let's write its implementation outside the class body:
//+------------------------------------------------------------------+ //| '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()); } //+------------------------------------------------------------------+
Here all is simple. When scrolling the mouse wheel, the Delta value of the wheel scrolling counter is passed in the dparam real parameter.
Values can be 120 and -120 depending on the direction of scrolling.
If Delta (passed to dparam) is positive, then clicking the right arrow button is considered an event.
If the Delta is negative, clicking on the right arrow button is considered an event.
Send this event to the object event handler (scrollbar).
Since we handle two different events (clicking the button and scrolling the mouse wheel) using the same block of code in the OnChartEvent() handler of the scrollbar object, we can determine the original event by the value of the sparam parameter — when the wheel is scrolled, this parameter will contain an empty string, and when the button is clicked - it will contain its name. In the event handler, define the event and set the shift size of the container content in pixels depending on the identified event. For a button click — two offset pixels , for scrolling the mouse wheel — four:
//--- 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(); } } //+------------------------------------------------------------------+
Now, if we compile the EA from the previous article again and try to scroll the mouse wheel while the cursor is on the scrollbar, then the content will shift only if the cursor is not on the slider:
If the cursor is over the slider, then there will be no scrolling. Why? Simply because the scrollbar slider object becomes active, and it does not yet have a handler for such an event. Let's add it.
Open the class file of the scrollbar slider object \MT5\MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarThumb.mqh and declare two mouse event handlers in the protected section:
//+------------------------------------------------------------------+ //| 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:
Why do we need two handlers? Each graphical object of the library contains areas that are responsible for its different states and send events corresponding to these areas. The active area of the scrollbar slider object along the perimeter is one pixel smaller than the size of the slider itself. Accordingly, when the cursor is inside the active area, the object active area handler is active, and if the cursor is at the very edge of the slider, then it falls into another zone where the handler for the cursor inside the form works.
Let's write the implementation of these two handlers outside the class body.
Implementing the "The cursor is inside the active area, the mouse wheel is being scrolled" event handler:
//+------------------------------------------------------------------+ //| '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 logic of the handler is identical to the logic of the handler of the scrollbar object discussed above. But here we first get the pointer to the base object for the slider, which is the scrollbar object. Then we bring the scrollbar to the foreground (its slider will also be brought to the forefront). Next, we determine the desired event in the direction of scrolling the mouse wheel and send the corresponding event to the event handler of the base object — the scrollbar. As a result, we redraw the chart to display changes immediately.
Implementing the "The cursor is inside the form, the mouse wheel is being scrolled" event handler:
Since this handler should be completely identical to the one discussed above, we simply call the first handler with the parameters passed to this one:
//+------------------------------------------------------------------+ //| '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); } //+------------------------------------------------------------------+
The horizontal scrollbar functionality is ready. Let's test the results.
Test
To perform the test, I will use the EA from the previous article without any changes. Let's compile it and run it on the chart setting "No" for auto resizing of the container to fit its contents:
Let's check the operation of all components of the created horizontal scrollbar functionality:
Everything works as planned.
What's next?
In the next article, we will transfer the created functionality to the "Vertical scrollbar" control.
*Previous articles within the series:
DoEasy. Controls (Part 26): Finalizing the ToolTip WinForms object and moving on to ProgressBar development
DoEasy. Controls (Part 27): Working on ProgressBar WinForms object
DoEasy. Controls (Part 28): Bar styles in the ProgressBar control
DoEasy. Controls (Part 29): ScrollBar auxiliary control
DoEasy. Controls (Part 30): Animating the ScrollBar control
DoEasy. Controls (Part 31): Scrolling the contents of the ScrollBar control
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/12849
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use