//+------------------------------------------------------------------+ //| VisualSystem.mqh | //| Copyright 2025, MetaQuotes Ltd. | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "VisualInterfaces.mqh" #include "ZoneLevel.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class VisualSystem : public ViewEvents { private: long m_chart_id; int m_width, m_height; long m_zoom; datetime m_from_time, m_to_time; double m_min_price, m_max_price; bool m_auto_update; long m_min_zoom_to_keep_texts; bool m_min_zoom_reached_already; // Manual dynamic array ZoneLevel* m_levels[]; int m_count; int m_capacity; void ResizeArray(int newCapacity); public: VisualSystem(); ~VisualSystem(); void SetMinZoomForTexts(long zoomLevel) { m_min_zoom_to_keep_texts = zoomLevel; } void SetAutoUpdate(bool enable) { m_auto_update = enable; } long Zoom() const { return m_zoom; } int GetScaledFontSize(int baseSizePoints) const; // Level management void AddLevel(ZoneLevel *level); bool RemoveLevel(int index); bool RemoveLevel(ZoneLevel *level); ZoneLevel* GetLevel(int index) const; ZoneLevel* GetLevelByName(string levelName); int GetSize() const { return m_count; } int GetCapacity() const { return m_capacity; } void SetCapacity(int newCapacity); void ClearLevels(); void DeleteLevel(int index); // Rendering – now uses dirty tracking void Render(); // Event handlers void UpdateViewBounds(); void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); // Virtual overrides virtual void OnViewChanged() override; virtual void OnZoomChanged(long newZoom, long oldZoom) override; virtual void OnMinZoomReached() override { } virtual void OnBeforeRender() override { UpdateViewBounds(); } virtual void OnAfterRender() override { } private: void DeleteAllTextObjects(); // DeleteAllVisualObjects no longer used in Render, kept for emergency void DeleteAllVisualObjects(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ VisualSystem::VisualSystem() : m_chart_id(ChartID()), m_width(0), m_height(0), m_zoom(0), m_auto_update(true), m_min_zoom_to_keep_texts(2), m_min_zoom_reached_already(false), m_count(0), m_capacity(0) { UpdateViewBounds(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ VisualSystem::~VisualSystem() { ClearLevels(); } //+------------------------------------------------------------------+ //| Array management | //+------------------------------------------------------------------+ void VisualSystem::ResizeArray(int newCapacity) { if(newCapacity <= 0) { ArrayResize(m_levels, 0); m_capacity = 0; return; } ArrayResize(m_levels, newCapacity); m_capacity = newCapacity; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::SetCapacity(int newCapacity) { if(newCapacity <= m_count) return; ResizeArray(newCapacity); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::AddLevel(ZoneLevel *level) { if(CheckPointer(level) == POINTER_INVALID) return; if(m_count >= m_capacity) { int newCap = (m_capacity == 0) ? 4 : m_capacity * 2; ResizeArray(newCap); } m_levels[m_count] = level; m_count++; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool VisualSystem::RemoveLevel(int index) { if(index < 0 || index >= m_count) return false; for(int i = index; i < m_count - 1; i++) m_levels[i] = m_levels[i + 1]; m_count--; if(m_count > 0) m_levels[m_count] = NULL; return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool VisualSystem::RemoveLevel(ZoneLevel *level) { if(level == NULL) return false; for(int i = 0; i < m_count; i++) if(m_levels[i] == level) return RemoveLevel(i); return false; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ ZoneLevel* VisualSystem::GetLevelByName(string levelName) { for(int i = 0; i < m_count; i++) if(m_levels[i].GetName() == levelName) return m_levels[i]; return NULL; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::DeleteLevel(int index) { if(index < 0 || index >= m_count) return; ZoneLevel* p = m_levels[index]; if(CheckPointer(p) != POINTER_INVALID) delete p; RemoveLevel(index); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ ZoneLevel* VisualSystem::GetLevel(int index) const { if(index < 0 || index >= m_count) return NULL; return m_levels[index]; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::ClearLevels() { for(int i = 0; i < m_count; i++) { if(CheckPointer(m_levels[i]) != POINTER_INVALID) delete m_levels[i]; m_levels[i] = NULL; } m_count = 0; } //+------------------------------------------------------------------+ //| Optimized Render – uses dirty flags | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Optimized Render – reverse iteration + early culling | //+------------------------------------------------------------------+ void VisualSystem::Render() { OnBeforeRender(); // <-- updates m_from_time, m_to_time, m_min_price, m_max_price // If bounds are invalid (chart not ready), just update all levels without culling bool boundsValid = (m_from_time < m_to_time && m_min_price < m_max_price); for(int i = m_count - 1; i >= 0; i--) // reverse iteration: draw newest first { ZoneLevel* p = m_levels[i]; if(p == NULL) continue; // Fast culling using level's stored time/price range /* datetime levelFrom = p.GetFromTime(); datetime levelTo = p.GetToTime(); double levelMin = p.GetMinPrice(); double levelMax = p.GetMaxPrice(); // Skip if completely outside visible time OR price range if(levelTo < m_from_time || levelFrom > m_to_time || levelMax < m_min_price || levelMin > m_max_price) { // Level not visible at all – skip UpdateDraw entirely // (Objects will be deleted when they become invisible inside UpdateDraw later) continue; } //} if(boundsValid) //{*/ // Level intersects view (or bounds invalid) – update its drawing p.UpdateDraw(GetPointer(this), m_from_time, m_to_time, m_min_price, m_max_price); } OnAfterRender(); } //+------------------------------------------------------------------+ //| View bounds update | //+------------------------------------------------------------------+ void VisualSystem::UpdateViewBounds() { long old_zoom = m_zoom; datetime old_from = m_from_time, old_to = m_to_time; double old_minp = m_min_price, old_maxp = m_max_price; m_width = (int)ChartGetInteger(m_chart_id, CHART_WIDTH_IN_PIXELS); m_height = (int)ChartGetInteger(m_chart_id, CHART_HEIGHT_IN_PIXELS); m_zoom = ChartGetInteger(m_chart_id, CHART_SCALE); int sub_window = 0; ChartXYToTimePrice(m_chart_id, 0, 0, sub_window, m_from_time, m_max_price); ChartXYToTimePrice(m_chart_id, m_width, m_height, sub_window, m_to_time, m_min_price); if(m_zoom != old_zoom) { OnZoomChanged(m_zoom, old_zoom); bool below_threshold = (m_zoom < m_min_zoom_to_keep_texts); if(below_threshold && !m_min_zoom_reached_already) { DeleteAllTextObjects(); OnMinZoomReached(); m_min_zoom_reached_already = true; } else if(!below_threshold) { m_min_zoom_reached_already = false; } } if(m_from_time != old_from || m_to_time != old_to || m_min_price != old_minp || m_max_price != old_maxp) { OnViewChanged(); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int VisualSystem::GetScaledFontSize(int baseSizePoints) const { return (int)MathMax(6.0, baseSizePoints - (m_zoom - 2)); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::DeleteAllTextObjects() { for(int i = 0; i < m_count; i++) if(CheckPointer(m_levels[i]) != POINTER_INVALID) m_levels[i].DeleteText(); } // Emergency cleanup – not used in normal render but kept for completeness void VisualSystem::DeleteAllVisualObjects() { for(int i = 0; i < m_count; i++) if(CheckPointer(m_levels[i]) != POINTER_INVALID) m_levels[i].Delete(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) UpdateViewBounds(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::OnViewChanged(void) { Render(); // Print("OnViewChanged"); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualSystem::OnZoomChanged(long newZoom, long oldZoom) { if(!m_min_zoom_reached_already) for(int i = 0; i < m_count; i++) if(CheckPointer(m_levels[i]) != POINTER_INVALID) m_levels[i].SetFontSize(GetScaledFontSize(12)); } //+------------------------------------------------------------------+