Przejdź do treści
MQL5 · MetaTrader 5 · po polsku
kod odczarowany

MQL5 / Kurs / Lekcja 7 z 7

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.

Przedrostek, dzięki któremu sprzątanie to jedna linia
#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

MQL5\Experts\PanelInformacyjny.mq5
//+------------------------------------------------------------------+
//|                                           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();
  }

Pobierz plik

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 = falseużytkownik przesunie element myszą i rozjedzie panel
OBJPROP_HIDDEN = trueelement pojawi się na liście obiektów wykresu i zaśmieci ją
OBJPROP_CORNERdomyślny róg to lewy górny; przy zmianie warto ustawić jawnie
OBJPROP_ZORDERprzy nakładających się elementach decyduje, co jest na wierzchu
OBJPROP_FONTdomyś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.

Reakcja na przycisk
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:

idKiedy
CHARTEVENT_OBJECT_CLICKkliknięcie w obiekt, nazwa w sparam
CHARTEVENT_CLICKkliknięcie w wykres, współrzędne w lparam i dparam
CHARTEVENT_KEYDOWNklawisz, kod w lparam
CHARTEVENT_OBJECT_ENDEDITzatwierdzenie tekstu w polu OBJ_EDIT
CHARTEVENT_CHART_CHANGEzmiana 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ązkowo

Kiedy 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.

Powiązane