Простые решения для удобной работы с индикаторами
Индикаторы давно стали неотъемлемой частью любой торговой платформы, их используют почти все трейдеры. Зачастую на графике используется не один индикатор, а целая система, поэтому удобство настройки индикатора является важным аспектом в торговле.
В этой статье расскажу, как сделать простенькую панельку для изменения настроек индикатора прямо с графика, и какие изменения нужно внести в индикатор, чтобы подключить эту панель. Статья рассчитана исключительно на тех, кто только начал знакомиться с языком MQL5, поэтому я постарался объяснить каждую строчку кода. Профессионалам, если они всё-таки решатся прочитать статью, я могу со всей ответственностью заявить, что они не найдут для себя здесь ничего нового.
Делаем панель
На этом сайте описано много различных панелей, есть статьи и просто код в кодобазе, почему бы не взять уже готовый код? Существует множество хорошего кода, в том числе замечательные библиотеки, которые позволяют создать панель любой сложности.
Однако, меня не устраивает то, что в погоне за универсальностью страдает простота использования этих библиотек. Поэтому писать будем панель специально для индикаторов, по сути, обычную таблицу, в которой ширина и высота ячеек будут автоматически подстраиваться под ширину и высоту текста в зависимости от размера шрифта.
В верхней части панели будет строка, за которую можно перетаскивать всю панель. В этой строке будет указано имя индикатора, а также значки для закрепления и сворачивания панели. Каждая ячейка таблицы будет описываться одной строкой кода. Таким образом, при необходимости мы сможем сделать столбцы на разных строках разной ширины.
Имя панели (индикатора) "Закрепить" "Свернуть" |
---|
Название настройки 1 | Поле ввода |
Название настройки 2 | Поле ввода |
Код, описывающий ячейку, будет содержать: имя объекта, тип объекта, № строки, текст ячейки, ширину ячейки в % от ширины панели. Ширина панели и ширина ячейки будут взаимозависимы. Сумма процентов всех ячеек одной строки должна равняться 100%.
Допустим, нужно в строке указать три объекта, тогда номер строки у всех трёх объектов будет одинаковый, а ширина, например, 30% + 30% + 40% = 100%. В большинстве случаев строку достаточно разделить на две части: 50% для названия параметра настроек и 50% под поле ввода.
Как я уже говорил, код панели был задуман максимально простым. Поэтому я планировал обойтись без объектно-ориентированного программирования (ООП). Однако полностью отказаться от ООП не получилось. Копировать много кода из индикатора в индикатор было бы неудобно. Поэтому я оформил код панели в виде класса во включаемом файле.
Я использовал класс вместо обычных отдельных функций в основном потому, что в деструкторе удобно удалять объекты панели, иначе пришлось бы удалять в OnDeinit() индикатора, а там это сделать было бы сложнее.
Также будет включаемый файл Object.mqh с методами рисования объектов, и в него же я записал геттеры и сеттеры для большего удобства обращения к функциям. Не буду описывать, что такое геттеры и сеттеры. Те, кто ещё не знает об этом, могут обратиться к Яндексу — он расскажет лучше меня.
Частично идею панели я позаимствовал из этих статей: статья 1, статья 2.
Все файлы кода описанного здесь, прикреплены внизу статьи. Я рекомендую скачать их, разложить по папкам и только потом приступать к изучению кода. У меня для файла Object.mqh создана отдельная папка Object в папке include. Для файла Panel.mqh создана отдельная папка Panel в папке include. Соответственно и путь к этим файлам в моём коде указан с учётом вложенных папок.
Начнём с подключения включаемого файла Object.mqh и объявления input переменных. Нам надо объявить переменные, в которых укажем: цвета панели, текста, кнопок, рамок, а также дополнительные цвета, в которые будет окрашиваться панель при скрытии индикатора, размер шрифта, стиль шрифта, отступы панели от краёв графика.
Input настройки:
//+------------------------------------------------------------------+ #include <Object\\Object.mqh> //+------------------------------------------------------------------+ input group "--- Input Panel ---" input int shiftX = 3; // Отступ панели по оси X input int shiftY = 80; // Отступ панели по оси Y input bool NoPanel = false; // Без панели input int fontSize = 9; // Размер шрифта input string fontType = "Arial"; /* Стиль шрифта*/ //"Arial", "Consolas" input string PanelHiddenShown = "❐"; // Панель скрыта/показана input string PanelPin = "∇"; /* Панель закрепить*/ // ⮂ ↕ ↔ ➽ 🖈 ∇ input string PanelUnpin = "_"; // Панель открепить input color clrTitleBar = C'109,117,171'; // Цвет фона названия панели (1) input color clrTitleBar2 = clrGray; // Цвет фона названия панели (2) input color clrDashboard = clrDarkGray; // Цвет фона панели input color clrTextDashboard = clrWhite; // Цвет текста на панели input color clrBorder = clrDarkGray; // Цвет окантовки input color clrButton1 = C'143,143,171'; // Цвет фона кнопок (1) input color clrButton2 = C'213,155,156'; // Цвет фона кнопок (2) input color clrButton3 = clrGray; // Цвет фона кнопок (3) input color clrTextButton1 = clrBlack; // Цвет текста на кнопках (1) input color clrTextButton2 = clrWhite; // Цвет текста на кнопках (2) input color clrEdit1 = C'240,240,245'; // Цвет фона в поле ввода (1) input color clrEdit2 = clrGray; // Цвет фона в поле ввода (2) input color clrTextEdit1 = C'50,50,50'; // Цвет текста в поле ввода (1) input color clrTextEdit2 = clrWhite; // Цвет текста в поле ввода (2) //+------------------------------------------------------------------+
Далее сам класс CPanel:
//+------------------------------------------------------------------+ class CPanel { private: enum ENUM_FLAG //флаги { FLAG_PANEL_HIDDEN = 1, // панель скрыта FLAG_PANEL_SHOWN = 2, // панель показана FLAG_IND_HIDDEN = 4, // индикатор скрыт FLAG_IND_SHOWN = 8, // индикатор показан FLAG_PANEL_FIX = 16, // панель закреплена FLAG_PANEL_UNPIN = 32 // панель откреплена }; int sizeObject; int widthPanel, heightPanel; int widthLetter, row_height; int _shiftX, _shiftY; long mouseX, mouseY; long chartWidth, chartHeight; string previousMouseState; long mlbDownX, mlbDownY, XDistance, YDistance; string _PanelHiddenShown, _PanelPin, _PanelUnpin; struct Object { string name; string text; ENUM_OBJECT object; int line; int percent; int column; int border; color txtColr; color backClr; color borderClr; }; Object mObject[]; int prefixInd; string Chart_ID; string addedNames[]; long addedXDisDiffrence[], addedYDisDiffrence[]; int WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL); void Add(string name); // запомним имя и точку привязки объекта void HideShow(bool hide = false); // скрыть//показать void DestroyPanel(); // удаляем все объекты public: CPanel(void); ~CPanel(void); string namePanel; // имя панели string indName; // имя индикатора должно соответствовать короткому имени индикатора string prefix; // префикс для имён объектов панели bool hideObject; // Будет использоваться как флаг в индикаторах в которых нужно скрывать графические объекты int sizeArr; double saveBuffer[]; // массив для сохранения координат точки привязки панели, свойств панели (состояния флагов) а также последних настроек индикатора enum ENUM_BUTON // флаги разрешения создания кнопок { BUTON_1 = 1, BUTON_2 = 2 }; void Init(string name, string indName); void Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);}; void Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0); bool OnEvent(int id, long lparam, double dparam, string sparam); int Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();} bool Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);} void Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1); void ApplySaved(); void HideShowInd(bool hide); }; //+------------------------------------------------------------------+ CPanel::CPanel(void) {} //+------------------------------------------------------------------+ CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();} //+------------------------------------------------------------------+
Методы класса обсудим чуть ниже на примерах.
Для примера напишем пустой индикатор:
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param; //+------------------------------------------------------------------+ int OnInit() { string short_name = "Ind Pnl(" + (string)param + ")"; mPanel.Init("Ind Pnl", short_name); mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60); mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40); mPanel.Create(0); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
При запуске этого индикатора, на графике получится вот такая панель:
А теперь, на примере этого индикатора, подробно разберём код панели.
Сразу после input-параметров индикатора, подключаем файл с классом панели и объявляем класс панели.
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param;
Обычно класс объявляют в самом начале кода. Но так как класс панели находится во включаемом файле, в котором также имеются input-параметры, то если мы его напишем в самом начале кода input, параметры индикатора окажутся ниже input-параметров панели. Это создаст некоторое неудобство при запуске и настройке индикатора.
Так как input-переменные являются константными, то изменять их нельзя. Но можно создать копию input-переменных и в дальнейшем работать с ними и изменять их из поля ввода панели.
Далее в функции индикатора OnInit() добавим код панели.
Но сначала хочу обратить ваше внимание, что для правильной работы панели нужно, чтоб у индикатора обязательно было задано в коде короткое имя индикатора, в котором обязательно должны быть указаны основные input-параметры.
string short_name = "Ind Pnl(" + (string)_param + ")";
Это нужно для того, чтоб запускать индикатор с разными настройками.
Напомню, в именах индикаторов нельзя использовать некоторые символы. Если вы хотите разделить параметры двоеточием, то лучше заменить его на точку с запятой.
Имя панели может быть таким же, как и имя индикатора, но удобнее сделать имя панели без учёта параметров индикатора.
Первый метод класса CPanel, который мы добавим в индикатор, это метод Init() в который передаём два имени: имя панели и имя индикатора.
mPanel.Init("Ind Pnl", short_name);
И первое, что делает метод Init(), проверяет, не отключена ли панель в настройках.
void CPanel::Init(string name, string short_name) { if(NoPanel) return;
Далее инициализируем переменные:
namePanel = name; indName = short_name; MovePanel = true; sizeObject = 0; Chart_ID = (string)ChartID(); int lastX = 0, lastY = 0;
Зададим разрешение на отправку всем mql5-программам на графике сообщений о событиях перемещения и нажатия кнопок мышки (CHARTEVENT_MOUSE_MOVE), а так же разрешим отправку сообщений о событии создания графического объекта (CHARTEVENT_OBJECT_CREATE):
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
Для расчёта ширины панели нужно сначала задать тип шрифта и его размер, а также получить размер одного символа, чтобы в дальнейшем от этого размера высчитывать отступы текста от границ панели.
Так мы получим масштабируемые отступы, которые будут привязаны к размеру шрифта. Высота ячейки будет равна высоте полутора символов.
// зададим шрифт и размер шрифта TextSetFont(fontType, fontSize * -10); //получим ширину и высоту одного символа TextGetSize("0", widthLetter, row_height); // рассчитаем высоту ячейки row_height += (int)(row_height / 2);
В настройках панели есть значки, которые используются для скрытия/отображения панели ❐, а также для её закрепления/открепления ∇ и _.
Значки я нашёл на просторах интернета и их можно поменять в настройках.
Добавим пробелов к символам, для правильного позиционирования от края панели. Если не добавить пробелов, то значки отображаются слишком близко друг от друга и попасть по ним мышкой становится проблематично.
string space = " "; _PanelHiddenShown = space + PanelHiddenShown + space; _PanelPin = space + PanelPin + space; _PanelUnpin = space + PanelUnpin + space;
Панель состоит из графических объектов, чтобы имена этих объектов были уникальны, создадим для них префикс:
MathSrand((int)GetMicrosecondCount()); prefixInd = MathRand(); prefix = (string)prefixInd;
Хочу обратить ваше внимание на то, что если индикаторов с панелью на графике несколько, то нельзя использовать при создании префикса функцию GetTickCount(), потому что при переключении таймфреймов времени проходит настолько мало, что если использовать миллисекунды вместо микросекунд, префиксы некоторых панелей могут совпадать.
При перетаскивании панели используется функция OnChartEvent(), в ней определяется положение мыши на графике и на объекте. А ведь панель может наехать на другую панель, и тогда у панелей получается конфликт, они обе считают, что мышь перетаскивает именно эту панель. Как итог, перетаскиваются все панели, которые находятся под мышкой. Для того, чтобы избежать конфликтов, создадим глобальную переменную и, при нажатии мыши на панели, какая первая панель запишет в эту глобальную переменную свой префикс, ту и будет двигать мышь. Принцип — кто первый встал того и тапки.
При инициализации запишем в эту переменную ноль, пока в переменной записан ноль, она считается свободной.
GlobalVariableTemp("CPanel"); GlobalVariableSet("CPanel", 0);
Когда мы перемещаем, сворачиваем, закрепляем панель или изменяем параметры индикатора, нам нужно где-то сохранять эти изменения. Это позволит нам загружать панель и индикатор с последними настройками при переключении таймфрейма или перезапуске терминала. В представленном индикаторе я не стал включать код для сохранения последних настроек панели и индикатора, но даже без этого кода индикатор будет записывать изменения настроек панели. Для этого нужно выделить память под массив с настройками панели.
sizeArr = ArraySize(saveBuffer); if(sizeArr == 0) Resize(0);
И хоть мы передаём в функцию количество настроек индикатора = 0, Resize(0); в самой функции добавляется три ячейки для запоминания настроек панели. То есть для того, чтоб запомнить положение панели на графике, её состояние (закреплена/откреплена, свёрнута/развёрнута) и ещё состояние индикатора (показан/скрыт), используется три ячейки массива saveBuffer.
Дальше код, который определяет начальные координаты точки привязки панели. Дело в том, что точка привязки панели может быть взята из input-настроек или же из сохранённых, если панель уже рисовалась на этом графике. Есть ещё вариант — это если использовать шаблон, в котором был записан индикатор с панелью.
С шаблоном оказалось сложнее. Если сохранить шаблон, в котором есть индикатор с панелью, то мы в этом шаблоне никак не можем сохранить координаты, где была панель в момент создания шаблона.
Но если добавить индикатор на график, сохранить шаблон, а потом применить этот шаблон, то мы увидим, что в шаблон записались OBJ_LABEL объект текстовая метка.
Сохранили шаблон:
Применили шаблон:
Именно эти текстовые метки мы используем для определения положения панели на момент создания шаблона.
string delPrefix = ""; int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL); for(int i = 0; i < total; i++) { string nameObject = ObjectName(0, i, 0, OBJ_LABEL); if(StringFind(nameObject, "TitleText " + indName) >= 0) // если в шаблоне есть объекты с именем этого индикатора { lastX = (int)GetXDistance(nameObject);// определим координаты Х панели в шаблоне lastY = (int)GetYDistance(nameObject);// определим координаты Y панели в шаблоне StringReplace(nameObject, "TitleText " + indName, ""); // запомним префикс объекта, для последующего его удаления delPrefix = nameObject; } }
В переменные lastX и lastY записываются координаты точки привязки объекта — текстовая метка с именем, в котором после префикса есть имя индикатора, то есть координаты текста с названием панели.
Напомню, название панели может отличаться от названия индикатора. После того, как нашли нужный текст, выделяем из него префикс и запоминаем.
Следующий код, по префиксу, который запомнили на предыдущем шаге, удаляет с графика артефактные текстовые метки, которые были сохранены в шаблоне.
if(delPrefix != "")// удаляем артефактные объекты сохранённые в шаблоне ObjectsDeleteAll(0, delPrefix);
Далее идёт проверка и выбор нужного варианта точки привязки панели.
if(lastX != 0 || lastY != 0)//если используем шаблон { lastX = lastX - widthLetter / 2; lastY = lastY - (int)(row_height / 8); saveBuffer[sizeArr - 1] = _shiftX = lastX; saveBuffer[sizeArr - 2] = _shiftY = lastY; } else//если используются данные из файла if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0) { _shiftX = (int)saveBuffer[sizeArr - 1]; _shiftY = (int)saveBuffer[sizeArr - 2]; } else//если это первый запуск индикатора { saveBuffer[sizeArr - 1] = _shiftX = shiftX; saveBuffer[sizeArr - 2] = _shiftY = shiftY; }
В самом конце метода Init() отправим записаться в массив структур те объекты панели, которые не нужно редактировать, они для всех панелей будут одинаковые.
Два прямоугольника, текст с названием панели и значки для скрытия/отображения и для закрепления/открепления панели.
Record("TitleBar"); Record("MainDashboardBody"); Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100); Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0); Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);
Метод Init() рассмотрели, переходим к следующему методу Record().
В методе Record() заполняется структура будущего объекта. Обычно большая часть структуры заполняется значениями по умолчанию. Однако набор параметров, передаваемых в эту функцию, позволяет немного изменить значения по умолчанию и, например, задать другой цвет для объекта.
//+------------------------------------------------------------------+ void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0) { if(NoPanel) return; int column = WidthHidthCalc(line + 1, text, percent, object); ArrayResize(mObject, sizeObject + 1); mObject[sizeObject].column = column; // столбец mObject[sizeObject].name = prefix + name; // имя объекта mObject[sizeObject].object = object; // тип объекта mObject[sizeObject].line = line + 1; // номер строки mObject[sizeObject].text = text; // текст (если есть)) mObject[sizeObject].percent = percent; // процент от ширины панели mObject[sizeObject].txtColr = txtColr; // цвет текста mObject[sizeObject].backClr = backClr; // цвет основания mObject[sizeObject].borderClr = borderClr; // цвет окантовки mObject[sizeObject].border = 0; // сдвиг от края панели sizeObject++; } //+------------------------------------------------------------------+
В начале метода Record() идёт обращение к методу WidthHidthCalc(), в котором рассчитывается ширина панели и её высота.
Метод WidthHidthCalc() разберём подробнее.
В этом методе рассчитывается ширина панели с учётом самого широкого элемента. Например, если в индикаторе "Ind Pnl", представленном выше, изменить название на более длинное.
Было:
mPanel.Init("Ind Pnl", short_name);
Сделали:
mPanel.Init("Ind Pnl 0000000000000000000", short_name);
Получаем вот такую панель:
Или если, например, изменим название настройки индикатора, получится следующее.
Было:
mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
Сделаем:
mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);
Результат:
Панель сама подстраивается под размеры текста. Все расчёты ширины и высоты панели производятся в функции WidthHidthCalc().
Сначала получаемширину текста ячейки.
Ширину для текста с названием панели и со значками скрыть/показать находим немного отлично от других ячеек.
int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL) { static int lastLine = -1, column = 0; int width, height; if(line == 1) TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); //получим ширину и высоту текста для строки с именем панели else TextGetSize(text, width, height); //получим ширину и высоту текста
Текст должен иметь отступ от границы ячейки, и такой отступ мы сделаем равным половине символа. Мы уже нашли ширину одного символа в функции Init() и записали в переменную widthLetter.
Чтобы обеспечить отступ текста с двух сторон, нужно добавить к полученной ширине текста ширину ещё одного символа, а для текста на объекте "Кнопка" - OBJ_BUTTON, нужно будет добавить ещё один символ для отступа от краёв кнопки.
И вот после того, как мы знаем размер всей строки в ячейке с учётом отступов, можно посчитать какого размера получится панель с учётом указанных под ячейку процентов.
Запоминаем самое большое значение панели. В дальнейшем все ячейки будут рассчитываться, отталкиваясь от этого самого большого значения ширины панели.
double indent = 0; if(object == OBJ_BUTTON) indent += widthLetter; if(text != "" && percent != 0) { //расчитаем ширину панели исходя из размера текста и процента отведённого под этот текст int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent); if(widthPanel < tempWidth) widthPanel = tempWidth; }
Расчёт ширины панели в тестовом индикаторе будет выглядеть следующим образом.
Сначала считается ширина названия с учётом значков. Название "Ind Pnl" + " ∇ " + " ❐ " получилась ширина 71 рх плюс ширина одного символа 7 рх, итого 78 рх — это 100% ширины панели.
Текст ячейки — "param", ширина 36 px с учётом добавленных отступов 7 рх, получилось 43 px, на эту ячейку выделено 60% от ширины панели, значит, ширина панели будет равна 43 * 100 / 60 = 72 px. Это меньше, чем нужно для названия панели, значит, ширина панели будет равна ячейке с названием.
Далее определяется номер столбца и/или добавляется высота панели, если это новая строка.
if(lastLine != line)// если это новая строка в панели, то увеличим высоту всей панели { heightPanel = row_height * line; lastLine = line; column = 0; // обнулим количество столбцов в новой строке } else column++; // добавим новый столбец return column; }
Вот мы и разобрали подробно работу двух из десяти методов класса CPanel.
После того, как программа определила будущие размеры панели и записала параметры объектов в массив структур mObject[], мы переходим к следующему методу — Create(). Этот метод будет строить панель на основе полученных ранее размеров.
В начале метода, как обычно, выполняется проверка, нужна ли эта панель. Затем следует код для записи двух предопределённых кнопок. Одна для скрытия индикатора, вторая для удаления. В зависимости от выбранных флагов, можно выбрать следующее 0-без кнопок, 1-одна кнопка скрыть/показать индикатор, 2-одна кнопка удалить индикатор, 3-будут созданы обе кнопки.
Почему эти кнопки здесь, а не в коде индикатора? Просто чтобы меньше кода вставлять в код индикаторов.
Дальше идёт инициализация переменных. А вот этот код нужен, если понадобится использовать панель не совсем так, как задумывалось, а например, как всплывающую панельку для изменения параметров объектов, и будет нужно, чтоб панель появлялась в месте клика мыши на графике.
void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1) { if(NoPanel) return; if((Button & BUTON_1) == BUTON_1)// если нужно создавать кнопки Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50); if((Button & BUTON_2) == BUTON_2)// если нужно создавать кнопки Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2); ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER; int X = 0, Y = 0, xSize = 0, ySize = 0; if(shiftx != -1 && shifty != -1) { _shiftX = shiftx; _shiftY = shifty; }
Пояснение к "а вот этот код ..." — панельку можно приспособить и под информационную и под торговую панель, и под вот такую панель настроек объектов графика. Всё простенько, без изысков, никакой красоты, чистый функционал:
Немного отвлёкся, продолжим разбирать метод Create(). Далее код, который создаёт два прямоугольника, прямоугольник заголовка и прямоугольник тела панели:
// прямоугольник заголовка RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName); Add(mObject[0].name);//запомним точку привязки объекта // прямоугольник панели RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName); Add(mObject[1].name);
После создания каждого объекта вызывается функция Add(), которая записывает в массивы имя и координаты этого объекта относительно левого верхнего угла графика.
//+------------------------------------------------------------------+ void CPanel::Add(string name)//запомним имя и точку привязки объекта { int size = ArraySize(addedNames); ArrayResize(addedNames, size + 1); ArrayResize(addedXDisDiffrence, size + 1); ArrayResize(addedYDisDiffrence, size + 1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name); } //+------------------------------------------------------------------+
В дальнейшем эти массивы с координатами будут использованы при перемещении панели.
Вернёмся опять к коду метода Create(). Дальше все объекты создаются в цикле в той последовательности, в какой их записали в массив структур mObject[]. Сначала расчет координат и размеров, затем создание объекта.
Так как панелька узкоспециализированная, в ней используются только три типа объектов, и этого вполне достаточно для её функционала.
При заполнении текстом прямоугольника заголовка, пришлось обратиться к исключениям и сделать точку привязки значков закрепления и сворачивания панели отличными от точки привязки всех остальных объектов панели. Это упростило позиционирование этих значков на панели, так как они имеют точку привязки в правом верхнем углу.
for(int i = 2; i < sizeObject; i++) { // рассчитаем координаты точки привязки объекта if(mObject[i].column != 0) { X = mObject[i - 1].border + widthLetter / 2; mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100); } else { X = _shiftX + widthLetter / 2; mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100); } Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8); //--- switch(mObject[i].object) { case OBJ_LABEL: ap = ANCHOR_LEFT_UPPER; // точки привязки объектов "закрепить" и "свернуть", в отличии от всех остальных, сделаем в правом верхнем углу. if(i == 3) { int w, h; TextGetSize(_PanelHiddenShown, w, h); X = _shiftX + widthPanel - w; ap = ANCHOR_RIGHT_UPPER; } if(i == 4) { X = _shiftX + widthPanel; ap = ANCHOR_RIGHT_UPPER; } LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1); break; case OBJ_EDIT: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER, (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1); break; case OBJ_BUTTON: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1); break; } Add(mObject[i].name); }
После того, как все объекты панели будут построены, мы удалим массив структур mObject[], так как он больше не будет нужен.
ArrayFree(mObject); ApplySaved(); ChartRedraw();
Обычно я создаю отдельную функцию, если код используется несколько раз. Однако, если суть операции сгруппирована по смыслу, я выделяю её в отдельный метод. Например, так я поступил с функцией ApplySaved(). Она проверяет, есть ли уже сохранённые данные панели, и применяет их, если они есть, или сохраняет новые данные, если их нет.
ApplySaved()
Если это первый запуск индикатора на этом графике, то массив saveBuffer[] заполняется стартовыми настройками.
Если же в массиве saveBuffer[] уже есть сохранённые данные, то мы применяем их вместо стартовых настроек.
//+------------------------------------------------------------------+ void CPanel::ApplySaved() { //свернём панель сразу после запуска индикатора, если это сохранено в файле if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN) CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN; //скроем индикатор сразу после запуска индикатора, если это сохранено в файле if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN) { HideShowInd(true); SetButtonState(prefix + "hideButton", true); hideObject = true; } else { saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN; hideObject = false; } //закрепим панель сразу после запуска индикатора, если это сохранено в файле if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX) SetText(addedNames[3], _PanelUnpin); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN; int Err = Save(); if(Err != 0) Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName); } //+------------------------------------------------------------------+
Как вы наверное заметили, в функции ApplySaved() используются ещё такие функции Save(), HideShowInd() и OnEvent(), если вы это читаете, напишите, пожалуйста в комменте слово — "заметил", очень интересно узнать, читает кто-то эти описания или нет.
Перейдём к описанию этих функций по порядку. В Save() сохраняем полученные настройки. Чтоб не засорять папку Files, под сохранённые настройки панелей выделим отдельную папку pnl
Вот так выглядит функция Save():
int Save() { ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError(); }
HideShowInd()
Задача этой функции просто поменять цвет заголовка панели и цвет и текст кнопки. Из массива saveBuffer удаляем предыдущий флаг и записываем новый.
Переменная hideObject нужна будет только в тех индикаторах, где для отрисовки используются объекты (стрелки, значки, текст и т.д.). При создании нового объекта в индикаторе мы будем проверять состояние этой переменной и, в зависимости от состояния, либо сразу скрывать вновь созданные объекты, либо ничего не делать, и объекты будут отображаться.
//+------------------------------------------------------------------+ void CPanel::HideShowInd(bool hide) { // сменим цвет и текст кнопок в зависимости от состояния панели, скрыта/показана, а также цвет заголовка if(hide) { SetColorBack(prefix + "TitleBar", clrTitleBar2); SetColorBack(prefix + "hideButton", clrButton3); SetText(prefix + "hideButton", "Ind Show"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN; hideObject = true; } else { SetColorBack(prefix + "TitleBar", clrTitleBar); SetColorBack(prefix + "hideButton", clrButton1); SetText(prefix + "hideButton", "Ind Hide"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN; hideObject = false; } Save(); ChartRedraw(); } //+------------------------------------------------------------------+
Эта функция используется только при клике на кнопку скрытия/отображения индикатора Ind Show/Ind Hide если она используется.
Пример скрытия одного из двух индикаторов RSI
В коде также присутствует функция HideShow(), которая отвечает за скрытие и отображение объектов. Эта функция используется для сворачивания панели и вывода объектов панели на передний план.
Функция принимает аргумент, который указывает, свёрнута панель или нет (true/false). Если панель свёрнута, то необходимо вывести на передний план только четыре её объекта: прямоугольник с названием, само название и два значка — закрепить панель и свернуть её.
Если флаг равен true, то есть панель свёрнута, то мы последовательно скрываем, а затем отображаем пять объектов. Почему пять, а не четыре? Дело в том, что среди нужных объектов есть один лишний — прямоугольник самой панели. Мы создаём его раньше названия и значков, поэтому этот прямоугольник нужно скрыть отдельно.
Если же флаг равен false, то последовательно скрываются, а затем отображаются все объекты панели, и таким образом они выводятся на передний план.
//+------------------------------------------------------------------+ void CPanel::HideShow(bool hide = false) //скрыть и сразу отобразить объекты, для вывода на передний план { int size = hide ? 5 : ArraySize(addedNames); for(int i = 0; i < size; i++) { SetHide(addedNames[i]); SetShow(addedNames[i]); } if(hide) SetHide(addedNames[1]); } //+------------------------------------------------------------------+
Вот так выглядит свёрнутая панель рядом с обычной:
Следующая функция, которую рассмотрим это OnEvent().
В начале функции проверяем, разрешена ли панель в настройках:
bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam) { if(NoPanel) return false;
Далее рассмотрим код, который отвечает за перемещение панели. Постараюсь объяснить работу кода максимально подробно.
При наступлении события "перемещение мыши" и в памяти записан флаг, разрешающий перемещение панели, то запоминаем координаты мыши.
Если это нажатие левой кнопки, то есть значение аргумента sparam равно 1, и если мышь до этого была не нажата, то считываем значение глобальной переменной.
Эта глобальная переменная общая для всех панелей, запущенных в терминале. При перемещении панели мы проверяем, не записано ли в эту переменную значение префикса другой панели, и если не записано, то в эту глобальную переменную записывается префикс панели, и другие панели, если находятся под/над той панелью, что мы перемещаем, уже не могут перемещаться.
Если в глобальной переменной записан ноль или префикс этой панели, то считываем текущие координаты точки привязки панели.
Затем проверяем, было ли это нажатие мыши на прямоугольнике с заголовком панели, и если это так, то получим размеры графика в пикселях, запретим прокрутку(перемещение) графика и в глобальную переменную записываем префикс данной панели. Запись префикса является одновременно флагом, разрешающим передвигать панель, и гарантирует, что будет двигаться только эта панель.
И последнее здесь, скроем/отобразим панель для выведения её на передний план, при этом передаём аргумент (true/false) в функцию, является панель на данный момент скрытой, или она отображается полностью.
if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { mouseX = (long)lparam; mouseY = (long)dparam; if(previousMouseState != "1" && sparam == "1") { int gvg = (int)GlobalVariableGet("Panel"); if(gvg == prefixInd || gvg == 0) { XDistance = GetXDistance(addedNames[0]); YDistance = GetYDistance(addedNames[0]); mlbDownX = mouseX; mlbDownY = mouseY; if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height) { chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); ChartSetInteger(0, CHART_MOUSE_SCROLL, false); GlobalVariableSet("Panel", prefixInd); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // скроем/отобразим панель, чтоб она была на переднем плане } } }
После того, как пользователь нажимает на прямоугольник с именем панели и начинает перемещать мышь, запускается следующий код.
Сначала проверяется, разрешено ли перемещать панель. Если в глобальной переменной есть префикс этой панели, то её можно двигать. Затем мы находим координаты точки привязки панели. Если переместить панель согласно координатам мыши, то мы проверяем новое положение панели. Если она может выйти за пределы графика, то немного изменяем значения координат точки привязки, чтоб панель не выходила за пределы графика.
В цикле перемещаем все объекты панели.
Записываем новые координаты точки привязки панели в массив для последующей записи в файл. После этого перерисовываем график.
if((int)GlobalVariableGet("Panel") == prefixInd) { // не дадим панельке убежать за пределы графика long posX = XDistance + mouseX - mlbDownX; if(posX < 0) posX = 0; else if(posX + widthPanel > chartWidth) posX = chartWidth - widthPanel; long posY = YDistance + mouseY - mlbDownY; if(posY < 0) posY = 0; else if(posY + row_height > chartHeight) posY = chartHeight - row_height; // перемещаем панель int size = ArraySize(addedNames); for(int i = 0; i < size; i++) { SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]); SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]); } saveBuffer[sizeArr - 1] = (double)(posX); saveBuffer[sizeArr - 2] = (double)(posY); ChartRedraw(0); }
Последнее действие при перемещении панели происходит, когда отпускают кнопку мыши, значение sparam при этом перестаёт быть равным единице.
Возвращаем возможность скролить график, обнуляем глобальную переменную и записываем в файл новые координаты точки привязки панели.
if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd) { ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); } previousMouseState = sparam; }
Мы подробно разобрали механизм перетаскивания панели, далее разберём действия при клике на значки закрепления/открепления панели или сворачивания/разворачивания панели.
Всё это в этой же функции OnEvent().
При наступлении события нажатие мышки на графическом объекте в переменной sparam находится имя объекта, на котором был клик мыши, если оно совпадает с именем объекта "❐", то проверяем следующий объект, и если он видимый, то скрываем объекты панели, а если он невидим, то отображаем все объекты панели. Меняем флаг видимости панели и записываем его в массив для последующего сохранения в файл.
При клике на объект, бывает срабатывает предыдущий код, который отвечает за перемещение панели, а так как значок "❐" находится в зоне, за которую панель перетаскивается, частенько отключается скролл графика, поэтому зададим разрешение на скролл. По этой же причине также нужно обнулить глобальную переменную.
Сохраняем изменения в файл, для отображения изменений на графике и перерисовываем его.
else if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == addedNames[4]) // prefix+"CollapseExpand" { if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// если панель видимая, скроем её { SetHide(addedNames[1]); for(int i = 5; i < sizeObject; i++) SetHide(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN; } else// если панель скрыта, покажем её { for(int i = 0; i < sizeObject; i++) SetShow(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN; } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
Следующий код аналогичен вышеописанному, с разницей только в имени объекта "∇"
else if(sparam == addedNames[3]) // prefix+"PinUnpin" { if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX; SetText(addedNames[3], _PanelUnpin); } else { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN; SetText(addedNames[3], _PanelPin); } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
Завершает код кнопка удаления индикатора:
else if(sparam == prefix + "delButton") // обработка кнопки удаления индикатора ChartIndicatorDelete(0, ChartWindowFind(), indName); }
Последнее событие, которое нам нужно обработать, это создание графического объекта.
Вновь созданные объекты обычно размещаются на переднем плане и могут перекрывать панель, поэтому мы сначала запоминаем состояние выделенности объекта, затем скрываем/отображаем панель для выведения её на передний план, а затем восстанавливаем состояние выделенности объекта. Зачем такие заморочки? А всё дело в том, что программное скрытие отображения объектов панели вызывает снятие выделенности с вновь созданных объектов.
Более подробно это явление описано в этой статье.
else if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179 "Делаем информационную панель для отображения данных в индикаторах и советниках" { bool select = GetSelect(sparam); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// скроем/отобразим панель, чтоб она была на переднем плане SetSelect(sparam, select);//воостанавливаем состояние крайнего объекта } return true; }
На этом описание кода панели можно считать законченным.
Какие правки нужно внести в код индикатора
Для разных индикаторов требуются разные изменения. Не удалось создать универсальный код, который можно было бы вставить в индикатор и получить готовую панель управления. С каждым индикатором нужно работать отдельно.
В этой статье я приведу несколько примеров, которые помогут вам изменить другие индикаторы по аналогии.
Для единообразия добавил слово "Pnl" к именам индикаторов, которые подверглись модификации.
Индикатор Custom Moving Average
Чтобы управлять индикатором из панели, необходимо иметь возможность изменять входные переменные. Однако входные переменные являются константными и не могут быть изменены.
Для решения этой проблемы можно скопировать входные переменные в обычные переменные, которые можно изменять. Чтобы изменения в коде индикатора были минимальными, мы объявим новые переменные с теми же именами, что и у текущих входных переменных, но добавим к ним спереди нижнее подчёркивание.
Было:
Стало:
Сразу после input параметров подключим включаемый файл Panel.mqh и объявим экземпляр класса CPanel mPanel;
Если директиву #include написать до input-параметров, то все input-параметры, написанные во включаемом файле, окажутся выше input-параметров индикатора, а это будет неудобно при запуске индикатора.
Если всё сделать правильно и запустить индикатор, то при запуске у нас должна появиться вот такая картинка:
Если настройки панели не нужны, то можно просто во включаемом файле Panel.mqh удалить все слова "input" и использовать настройки по умолчанию.
В функцию OnInit() добавляем следующий код.
Далее проверяем, включена ли панель в этом индикаторе, загружаем ранее сохранённые настройки панели, если это не первый запуск индикатора на этом графике. Если же запуск первый, то изменим размер массива на количество input-параметров, в данном случае на три и запишем в массив значения этих input параметров.
if(!NoPanel) { if(mPanel.Load(short_name)) { InpMAPeriod = (int)mPanel.saveBuffer[0]; InpMAShift = (int)mPanel.saveBuffer[1]; InpMAMethod = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = InpMAPeriod; mPanel.saveBuffer[1] = InpMAShift; mPanel.saveBuffer[2] = InpMAMethod; }
Имя панели, имя индикатора
Дальше всё заполняется однотипно: имя объекта, тип объекта, номер строки в панели, сам объект, процентов от ширины панели.
mPanel.Init("Moving Average", short_name); mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50); mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50); mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50); mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50); mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50); mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50); mPanel.Create(); }
Если на этом этапе запустить индикатор, то получим вот такую панельку:
Итак, панель у нас есть, осталось написать код для коммуникации с пользователем.
Добавляем в индикатор ещё одну функцию — OnChartEvent().
Метод, отвечающий за перемещение панели, описан выше. При наступлении события "окончание редактирования текста в графическом объекте Edit", проверяем префикс объекта Edit, в котором закончилось редактирование, и если префикс соответствует префиксу нашей панели, то проверим, какой из параметров индикатора был изменён, записываем изменения в переменную и в массив для последующего сохранения.
Далее сохраняем все изменения в файл и перезапускаем индикатор.
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "MAPeriod") { mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam)); } else if(sparam == mPanel.prefix + "MAShift") { mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam)); PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift); } else if(sparam == mPanel.prefix + "MAMethod") { mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam)); } mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); }
Обработка кнопки скрытия/отображения индикатора, в индикаторе МА предельно проста.
Чтобы скрыть линию мувинга, достаточно задать стиль графического построения DRAW_NONE, для отображения соответственно DRAW_LINE.
if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); mPanel.HideShowInd(false); } }
На этом изменения в индикаторе Custom Moving Average закончены. Изменённый индикатор имеет название Custom Moving Average Pnl.
Индикатор ParabolicSAR
Модификация индикатора ParabolicSAR абсолютно одинакова с индикатором Custom Moving Average, за исключением совсем небольших нюансов.
В индикаторе ParabolicSAR не нужно создавать новые переменные с таким же названием как и input-переменные, так как они там уже есть.
Поэтому сразу подключаем включаемый файл:
В OnInit() добавляем код:
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtSarStep = mPanel.saveBuffer[0]; ExtSarMaximum = mPanel.saveBuffer[1]; } else { mPanel.Resize(2); mPanel.saveBuffer[0] = ExtSarStep; mPanel.saveBuffer[1] = ExtSarMaximum; } mPanel.Init("ParabolicSAR", short_name); mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50); mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50); mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50); mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50); mPanel.Create(); }
В код индикатора добавляем функцию OnChartEvent().
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "SARStep") mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam)); else if(sparam == mPanel.prefix + "SARMaximum") mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 159); mPanel.HideShowInd(false); } } //+------------------------------------------------------------------+
Всё, на этом изменения в индикаторе ParabolicSAR закончены.
Индикатор RSI
В индикаторе RSI всё делается точно так же, как в предыдущих двух индикаторах.
В глобале после input-настройки вставляем:
#include <Panel\\Panel.mqh>
CPanel mPanel;
Далее в OnInit():
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtPeriodRSI = (int)mPanel.saveBuffer[0]; } else { mPanel.Resize(1); mPanel.saveBuffer[0] = ExtPeriodRSI; } mPanel.Init("RSI", short_name); mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60); mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40); mPanel.Create(); }
OnChartEvent() будет немного отличаться от предыдущих индикаторов.
Обработка объекта-поле ввода, делается точно так же, как в предыдущих индикаторах. А вот скрытие/отображение индикатора обрабатывается по-другому, так как до этого мы рассматривали индикаторы с основного графика, а RSI является подвальным индикатором.
При нажатии на кнопку "Ind Hide" задаём высоту окна индикатора равной нулю. Меняем цвет панельки, цвет и текст кнопки.
При повторном нажатии на кнопку, у неё теперь другое название — "Ind Show", устанавливаем значение CHART_HEIGHT_IN_PIXELS равным -1. Меняем цвет панельки, цвет и текст кнопки.
Цитата из учебника:
"Программная установка свойства CHART_HEIGHT_IN_PIXELS делает невозможным редактирование размера окна/подокна пользователем. Для того чтобы убрать фиксацию размера, следует установить значение свойства в -1".
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(mPanel.OnEvent(id, lparam, dparam, sparam)) { if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) if(sparam == mPanel.prefix + "PeriodRSI") { mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") //скрыть подвальный индикатор { if(GetButtonState(sparam)) { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0); mPanel.HideShowInd(true); } else { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1); mPanel.HideShowInd(false); } } } } //+------------------------------------------------------------------+
Ещё один индикатор
Есть индикаторы, которые вообще не используют графические стили, вместо этого они рисуют графические объекты, обычно стрелки. Это ещё один вариант обработки кнопки «Скрыть/показать индикатор». Давайте его разберём подробнее.
Я не стал искать индикатор со стрелками, просто написал индикатор фракталов, в котором верхние значки отображаются с помощью графического построения PLOT_ARROW, а нижние с помощью отрисовки объектов OBJ_ARROW.
Код индикатора приведу здесь полностью.
В настройки вынесены размер плечей фрактала, а также количество дней, на котором будем рисовать OBJ_ARROW. К ограничению количества дней пришлось прибегнуть, так как большое количество объектов может сильно затормозить график.
Как и в предыдущих индикаторах, сразу после input-переменных подключаем файл Panel.mqh и объявляем экземпляр класса CPanel.
Дублируем input переменные обычными.
#property indicator_chart_window #property indicator_plots 1 #property indicator_buffers 1 #property indicator_type1 DRAW_ARROW #property indicator_color1 clrRed #property indicator_label1 "Fractals" input int _day = 10; // day input int _barLeft = 1; // barLeft input int _barRight = 1; // barRight #include <Panel\\Panel.mqh> CPanel mPanel; double buff[]; int day = _day, barLeft = _barLeft, barRight = _barRight; datetime limitTime = 0;
В OnInit() всё как в предыдущих индикаторах.
//+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, buff, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); PlotIndexSetInteger(0, PLOT_ARROW, 217); PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5); string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight); IndicatorSetString(INDICATOR_SHORTNAME, short_name); if(!NoPanel) { if(mPanel.Load(short_name)) { day = (int)mPanel.saveBuffer[0]; barLeft = (int)mPanel.saveBuffer[1]; barRight = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = day; mPanel.saveBuffer[1] = barLeft; mPanel.saveBuffer[2] = barRight; } mPanel.Init("Fractals", short_name); mPanel.Record("dayText", OBJ_LABEL, 1, "Дней:", 50); mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50); mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50); mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50); mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50); mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50); mPanel.Create(); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Основные отличия от работы OnChartEvent() в предыдущих индикаторах, это то, что при каждом изменении параметров индикатора нужно удалять с графика, объекты нарисованные индикатором.
В случае нажатия кнопки скрывающей индикатор, мы скрываем в цикле все объекты нарисованные индикатором. А также устанавливаем тип графического построения DRAW_NONE.
При обратном процессе, нужно не только указать тип графического построения DRAW_ARROW, но и указать номер стрелки из набора Wingdings. Затем в цикле делаем все скрытые объекты, видимыми.
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "day") mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barLeft") mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barRight") mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") { if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetHide(name); } mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 217); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetShow(name); } mPanel.HideShowInd(false); } } } //+------------------------------------------------------------------+
Ещё нужно добавить в код индикатора проверку после каждого нарисованного нового объекта на флаг, указывающий на необходимость скрыть индикатор и если этот флаг true, то скрыть вновь нарисованный объект.
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
int limit = prev_calculated - 1;
if(prev_calculated <= 0)
{
ArrayInitialize(buff, 0);
datetime itime = iTime(_Symbol, PERIOD_D1, day);
limitTime = itime <= 0 ? limitTime : itime;
if(limitTime <= 0)
return 0;
int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
limit = MathMax(rates_total - shift, barRight + barLeft);
}
for(int i = limit; i < rates_total && !IsStopped(); i++)
{
bool condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(high[j - 1] >= high[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(high[j - 1] <= high[j])
{
condition = false;
break;
}
if(condition)
buff[i - barRight] = high[i - barRight];
condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(low[j - 1] <= low[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(low[j - 1] >= low[j])
{
condition = false;
break;
}
if(condition)
{
string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
if(mPanel.hideObject)
SetHide(name);
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
}
//+------------------------------------------------------------------+
В процессе работы над статьёй я привёл пример использования панели, как быстрого доступа к настройкам объектов. Полагаю, стоит пояснить код этого индикатора.
Индикатор Setting Objects Pnl
В этом индикаторе создавать объект класса панели в OnInit() нам не нужно, так как панель должна вызываться для разных объектов, а значит, создавать её будем динамически с помощью оператора new.
Объявляем описатель объекта класса. Если был клик на каком-либо графическом объекте чарта и при этом, была зажата клавиша Shift, инициализируем ранее созданный описатель объекта класса.
Создаём панель также, как делали это в индикаторах, с единственным нюансом — в метод Create() передаём в качестве аргументов текущие координаты мыши на графике.
Изменения в полях ввода обрабатываем так же как в индикаторах с одной разницей, здесь нам не нужно сохранять в файл внесённые изменения.
После того, как редактирования завершены, панель можно убрать нажав на кнопку "Del Pnl", при этом будет удалён описатель.
Из-за того, что у разных объектов могут быть разные свойства, то и панель придётся рисовать с учётом этого. Если мы редактируем трендовую линию, то нам в панели совершенно не нужно поле, отвечающее за заливку объекта.
Значит, для трендовой такое поле не создаём, а создаём его только для объектов, в которых предусмотрена заливка. В такой ситуации мы не можем заранее точно знать количество строк в панели, поэтому введём переменную line, запишем в неё номер текущей строки и будем её увеличивать по мере необходимости.
#property indicator_chart_window #property indicator_plots 0 #define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P) #include <Panel\\Panel.mqh> CPanel * mPl; //+------------------------------------------------------------------+ int OnCalculate(const int, const int, const int, const double &price[]) {return(0);} //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { static bool panel = false; if(panel) mPl.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_CLICK) if(!panel) { if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0) { int line = 1; mPl = new CPanel(); ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam); mPl.Init(EnumToString(ObjectType), sparam); mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50); mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50); line++; mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50); mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50); line++; mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50); mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50); line++; if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE) { mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50); mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50); line++; } mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100); mPl.Create(0, (int)lparam, (int)dparam); panel = true; } } else if(sparam == mPl.prefix + "delButton") { FREE(mPl); panel = false; } if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPl.prefix) >= 0) { if(sparam == mPl.prefix + "Color") SetColor(mPl.indName, StringToColor(GetText(sparam))); else if(sparam == mPl.prefix + "Style") SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Width") SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Fill") SetFill(mPl.indName, (int)StringToInteger(GetText(sparam))); ChartRedraw(); } } //+------------------------------------------------------------------+ void OnDeinit(const int reason) {FREE(mPl);} //+------------------------------------------------------------------+
Панель настроек объектов вызывается, зажатием клавиши Shift + левый клик мыши на объекте.
Заключение
Плюсы:
- В целом, получилось удобное в использовании решение.
Минусы:
- Хотелось "скрытие" индикатора спрятать в класс CPanel, но не получилось.
- Если не добавить input-переменные в короткое имя индикатора, то вызов нескольких индикаторов с панелью будет невозможен, из-за совпадения имён.
- Если запустить индикатор на графике, поменять его настройки с помощью панели, а затем сохранить шаблон, то при загрузке шаблона загрузятся не последние параметры индикатора, а те, что были заданы в настройках при запуске индикатора.
- Не ко всем индикаторам можно приаттачить панель.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования