Panel na wykresie
Kompletny panel z przyciskiem, obsługą kliknięcia i sprzątaniem po sobie. Temat bardzo poszukiwany i wyjątkowo słabo opisany.
4 fragmenty kodu z tej strony przeszły przez kompilator MetaEditor build 6090, 2026-08-05.
Interfejs na wykresie buduje się z obiektów graficznych. Nie ma tu żadnego systemu okien ani układu — każdy element umieszczasz na własnych współrzędnych w pikselach i sam pilnujesz, żeby na siebie nie nachodziły.
Brzmi prymitywnie i takie jest. Za to działa wszędzie, nie wymaga bibliotek i mieści się w jednym pliku.
Trzy rzeczy, które trzeba zrozumieć na starcie
Obiekt istnieje na wykresie, nie w twoim programie. Zdejmiesz eksperta, a obiekty zostaną. Stąd konieczność sprzątania w OnDeinit i konieczność wspólnego przedrostka w nazwach.
#define PRZEDROSTEK "panel_"
void OnDeinit(const int powod)
{
ObjectsDeleteAll(0, PRZEDROSTEK);
ChartRedraw(0);
}Współrzędne liczone są od rogu, nie od ceny. Obiekty typu OBJ_LABEL i OBJ_BUTTON są przypięte do okna, nie do wykresu — nie przesuwają się przy przewijaniu. Róg wybierasz przez OBJPROP_CORNER, a odległość podajesz w pikselach.
Zmiany widać dopiero po ChartRedraw. Terminal odświeża wykres własnym rytmem. Bez jawnego wywołania panel będzie się aktualizował z widocznym opóźnieniem albo wcale, jeśli rynek stoi.
Kompletny panel
//+------------------------------------------------------------------+
//| PanelInformacyjny.mq5 |
//| Panel na wykresie: etykiety, przycisk i obsługa kliknięcia. |
//| Niczym nie handluje — pokazuje wyłącznie dane instrumentu. |
//| https://finanseodczarowane.pl/mql5/kurs/07-panel-na-wykresie/ |
//| Licencja MIT |
//+------------------------------------------------------------------+
#property copyright "finanseodczarowane.pl/kod"
#property version "1.00"
//--- wspólny przedrostek pozwala usunąć wszystkie obiekty jednym wywołaniem
#define PRZEDROSTEK "panel_"
input int OdstepX = 12; // odległość od krawędzi w pikselach
input int OdstepY = 24;
input color KolorTekstu = clrWhite;
bool szczegoly = false; // czy pokazujemy dodatkowe wiersze
//+------------------------------------------------------------------+
//| Tworzy etykietę tekstową w rogu wykresu. |
//+------------------------------------------------------------------+
bool UtworzEtykiete(const string nazwa, const int x, const int y,
const int rozmiar = 9)
{
string pelna = PRZEDROSTEK + nazwa;
if(ObjectFind(0, pelna) < 0)
if(!ObjectCreate(0, pelna, OBJ_LABEL, 0, 0, 0))
{
PrintFormat("Nie udało się utworzyć %s, błąd %d", pelna, GetLastError());
return false;
}
ObjectSetInteger(0, pelna, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, pelna, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, pelna, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, pelna, OBJPROP_COLOR, KolorTekstu);
ObjectSetInteger(0, pelna, OBJPROP_FONTSIZE, rozmiar);
ObjectSetInteger(0, pelna, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, pelna, OBJPROP_HIDDEN, true);
ObjectSetString(0, pelna, OBJPROP_FONT, "Consolas");
return true;
}
//+------------------------------------------------------------------+
bool UtworzPrzycisk(const string nazwa, const int x, const int y,
const int szerokosc, const int wysokosc, const string napis)
{
string pelna = PRZEDROSTEK + nazwa;
if(ObjectFind(0, pelna) < 0)
if(!ObjectCreate(0, pelna, OBJ_BUTTON, 0, 0, 0))
return false;
ObjectSetInteger(0, pelna, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, pelna, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, pelna, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, pelna, OBJPROP_XSIZE, szerokosc);
ObjectSetInteger(0, pelna, OBJPROP_YSIZE, wysokosc);
ObjectSetInteger(0, pelna, OBJPROP_BGCOLOR, clrDimGray);
ObjectSetInteger(0, pelna, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, pelna, OBJPROP_BORDER_COLOR, clrSilver);
ObjectSetInteger(0, pelna, OBJPROP_STATE, false);
ObjectSetString(0, pelna, OBJPROP_TEXT, napis);
return true;
}
//+------------------------------------------------------------------+
void UstawTekst(const string nazwa, const string tekst)
{
ObjectSetString(0, PRZEDROSTEK + nazwa, OBJPROP_TEXT, tekst);
}
//+------------------------------------------------------------------+
void Odswiez(void)
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
return;
double punkt = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int spread = (punkt > 0.0) ? (int)MathRound((tick.ask - tick.bid) / punkt) : 0;
UstawTekst("naglowek", StringFormat("%s %s", _Symbol,
EnumToString((ENUM_TIMEFRAMES)_Period)));
UstawTekst("ceny", StringFormat("bid %.*f ask %.*f",
_Digits, tick.bid, _Digits, tick.ask));
UstawTekst("spread", StringFormat("spread %d pkt", spread));
if(szczegoly)
{
double dzienMax = iHigh(_Symbol, PERIOD_D1, 0);
double dzienMin = iLow(_Symbol, PERIOD_D1, 0);
int zakres = (punkt > 0.0) ? (int)MathRound((dzienMax - dzienMin) / punkt) : 0;
UstawTekst("zakres", StringFormat("zakres dnia %d pkt", zakres));
UstawTekst("wolumen", StringFormat("min lot %.2f krok %.2f",
SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN),
SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP)));
}
else
{
UstawTekst("zakres", "");
UstawTekst("wolumen", "");
}
ChartRedraw(0); // bez tego zmiany będą widoczne z opóźnieniem
}
//+------------------------------------------------------------------+
int OnInit()
{
int y = OdstepY;
UtworzEtykiete("naglowek", OdstepX, y, 11); y += 20;
UtworzEtykiete("ceny", OdstepX, y); y += 16;
UtworzEtykiete("spread", OdstepX, y); y += 16;
UtworzEtykiete("zakres", OdstepX, y); y += 16;
UtworzEtykiete("wolumen", OdstepX, y); y += 22;
UtworzPrzycisk("przelacz", OdstepX, y, 120, 22, "Szczegóły");
Odswiez();
EventSetTimer(1); // odświeżanie także gdy rynek stoi
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int powod)
{
EventKillTimer();
//--- sprzątamy po sobie: bez tego obiekty zostają na wykresie
ObjectsDeleteAll(0, PRZEDROSTEK);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
void OnTick()
{
Odswiez();
}
//+------------------------------------------------------------------+
void OnTimer()
{
Odswiez();
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam,
const double &dparam, const string &sparam)
{
if(id != CHARTEVENT_OBJECT_CLICK)
return;
if(sparam != PRZEDROSTEK + "przelacz")
return;
szczegoly = !szczegoly;
//--- przycisk zostaje wciśnięty, dopóki sami go nie zwolnimy
ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
ObjectSetString(0, sparam, OBJPROP_TEXT, szczegoly ? "Mniej" : "Szczegóły");
Odswiez();
}Pokazuje symbol, ceny, spread i — po kliknięciu przycisku — zakres dnia oraz parametry wolumenu. Nie dotyka rachunku.
Właściwości, bez których panel wygląda źle
| Właściwość | Co się stanie bez niej |
|---|---|
OBJPROP_SELECTABLE = false | użytkownik przesunie element myszą i rozjedzie panel |
OBJPROP_HIDDEN = true | element pojawi się na liście obiektów wykresu i zaśmieci ją |
OBJPROP_CORNER | domyślny róg to lewy górny; przy zmianie warto ustawić jawnie |
OBJPROP_ZORDER | przy nakładających się elementach decyduje, co jest na wierzchu |
OBJPROP_FONT | domyślna czcionka proporcjonalna rozjeżdża kolumny liczb |
Ta ostatnia rzecz jest niedoceniana: w panelu z liczbami ustaw czcionkę o stałej szerokości, na przykład Consolas. Inaczej kolumny drgają przy każdej zmianie ceny, bo cyfra 1 jest węższa od 8.
Obsługa kliknięcia
Kliknięcia trafiają do OnChartEvent jako zdarzenie CHARTEVENT_OBJECT_CLICK. Nazwa klikniętego obiektu jest w argumencie sparam.
void OnChartEvent(const int id, const long &lparam,
const double &dparam, const string &sparam)
{
if(id != CHARTEVENT_OBJECT_CLICK)
return;
if(sparam != PRZEDROSTEK + "przelacz")
return;
//--- przycisk zostaje wciśnięty, dopóki sami go nie zwolnimy
ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
}Inne zdarzenia, które przychodzą do tej samej funkcji:
id | Kiedy |
|---|---|
CHARTEVENT_OBJECT_CLICK | kliknięcie w obiekt, nazwa w sparam |
CHARTEVENT_CLICK | kliknięcie w wykres, współrzędne w lparam i dparam |
CHARTEVENT_KEYDOWN | klawisz, kod w lparam |
CHARTEVENT_OBJECT_ENDEDIT | zatwierdzenie tekstu w polu OBJ_EDIT |
CHARTEVENT_CHART_CHANGE | zmiana rozmiaru okna albo skali |
To ostatnie przydaje się, gdy panel ma trzymać się prawej krawędzi: po zmianie rozmiaru okna trzeba przeliczyć współrzędne.
Odświeżanie, gdy rynek stoi
OnTick nie wywoła się w weekend ani w nocy na spokojnym instrumencie, więc panel oparty wyłącznie na nim zamarza. Czasomierz rozwiązuje to jedną linią:
EventSetTimer(1); // w OnInit
EventKillTimer(); // w OnDeinit, obowiązkowoKiedy sięgnąć po Canvas
Obiekty graficzne wystarczają do paneli z tekstem i przyciskami. Gdy potrzebne jest rysowanie — wykresy, wskaźniki własnego kroju, wypełnienia — sensowniejsza jest klasa CCanvas z biblioteki standardowej. Rysuje po bitmapie, więc jedno płótno zastępuje setki obiektów i nie obciąża wykresu. Cena: musisz sam narysować wszystko, łącznie z tekstem i ramkami.
Granica przebiega mniej więcej tak: do trzydziestu elementów obiekty graficzne, powyżej — płótno.
Zadanie
Dodaj do panelu pole OBJ_EDIT, w którym użytkownik wpisze liczbę świec, i pokaż średni zakres z tylu świec. Obsłuż CHARTEVENT_OBJECT_ENDEDIT, odczytując tekst przez ObjectGetString i zamieniając go na liczbę przez StringToInteger. Zabezpiecz się przed wpisaniem tekstu, który liczbą nie jest.