DearMQL5 is an institutional-grade, immediate-mode graphical user interface
(GUI) engine designed from the ground up for MetaTrader 5 (MQL5), inspired by
the ergonomics and performance of Dear ImGui.
### Core Architecture & Execution Pipeline
- Immediate-Mode Paradigm: Declarative frame-based rendering pipeline with zero
memory allocation churn during render loops and deterministic execution.
- Modular Per-Hub Canvas Surfaces (CUWindowSurface): Replaces full-screen canvas
overlays with isolated, window-bounded CCanvas bitmaps, reducing memory usage
by up to 93% (~570 KB vs 8.3 MB for 1080p) and preventing click-swallowing.
- 0-Cost Hardware Texture Drag Engine: Absolute-anchor drag mechanics update
native MT5 OBJPROP_XDISTANCE and OBJPROP_YDISTANCE properties directly on
the compositor, bypassing CPU rasterization completely (0.00% CPU usage).
- Chart Scroll Protection: Automatically suppresses chart history sliding
(CHART_MOUSE_SCROLL) during active HUD dragging and safely restores it on release.
- Two-Phase Hit-Testing: Interactive controls (buttons, sliders, toggles) receive
first-priority click focus, cleanly separating widget actions from window drags.
### Subsystems & Components
- Core State & Input (Core/):
* UInputState: Mouse/keyboard tracking, deltas, and chart scroll lock.
* UState: Retained state hash map in pure MQL5 with 64-bit FNV-1a hashing.
* UIdStack: Hierarchical identifier scoping preventing widget ID collisions.
* ULayout: Automatic cursor placement supporting SameLine, Indent, and Unindent.
* UEventQueue: Non-blocking circular UI event queue for decoupled handling.
* UTheme: Institutional OneDark/Nord dark color palette and typography metrics.
- Rendering Backends (Render/):
* URendererCanvas: Isolated per-hub surfaces with 32-bit ARGB alpha transparency.
* URendererNative: Native MT5 object pool (OBJ_RECTANGLE_LABEL) with diff caching.
* UFont: Anti-aliased font metrics and width cache.
- Complete Widget Suite (Widgets/):
* UButton: Click-on-release standard, accent, and emergency panic buttons.
* UToggle: Smooth draggable/clickable iOS-style toggle switches.
* USlider: Draggable horizontal float and integer sliders.
* UProgressBar: Adaptive progress bars with dynamic green->amber->red color ramps.
* UHeader: Collapsible section headers with immediate-mode state retention.
* ULabel & USeparator: Formatted text, colored metrics, and horizontal dividers.
- Window Management (Windows/):
* UWindow: Window container with titlebar, collapse toggle, and coordinate clipping.
* UWindowManager: Multi-window coordinator with GlobalVariable persistence across
chart timeframe changes and backward-compatible key resolution.
### Tooling, Documentation & Reference Implementation
- Examples/EA_DearMQL5_Demo.mq5: Complete reference Prop-Firm cockpit HUD.
- README.md: Comprehensive institutional English documentation for GitHub and Algo Forge.
- Docs/ARCHITECTURE.md: Technical architecture specification with Mermaid lifecycle.
477 lines
18 KiB
MQL5
477 lines
18 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| UI.mqh |
| |
//| Copyright 2026, DearMQL5 GUI Engine |
| |
//| https://www.mql5.com |
| |
//+------------------------------------------------------------------+
| |
#property copyright "Copyright 2026, DearMQL5 GUI Engine"
| |
#property link "https://www.mql5.com"
| |
#property strict
| |
| |
// Backend selection: Default is high-performance CCanvas (isolated Per-Hub OBJ_BITMAP_LABEL)
| |
// Define UI_BACKEND_NATIVE prior to including UI.mqh if native MT5 object pool is preferred
| |
#ifndef UI_BACKEND_NATIVE
| |
#define UI_BACKEND_CANVAS
| |
#endif
| |
| |
// Core Subsystems
| |
#include "Util\UMath.mqh"
| |
#include "Util\UDiagnostics.mqh"
| |
#include "Core\UTheme.mqh"
| |
#include "Core\UIdStack.mqh"
| |
#include "Core\UState.mqh"
| |
#include "Core\UInputState.mqh"
| |
#include "Core\UEventQueue.mqh"
| |
#include "Core\ULayout.mqh"
| |
#include "Render\UFont.mqh"
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
#include "Render\URendererCanvas.mqh"
| |
#else
| |
#include "Render\URendererNative.mqh"
| |
#endif
| |
| |
// Widgets & Windows
| |
#include "Widgets\UWidget.mqh"
| |
#include "Widgets\ULabel.mqh"
| |
#include "Widgets\USeparator.mqh"
| |
#include "Widgets\UButton.mqh"
| |
#include "Widgets\UToggle.mqh"
| |
#include "Widgets\USlider.mqh"
| |
#include "Widgets\UProgressBar.mqh"
| |
#include "Widgets\UHeader.mqh"
| |
#include "Windows\UWindow.mqh"
| |
#include "Windows\UWindowManager.mqh"
| |
| |
/// Primary public facade for DearMQL5 immediate-mode GUI operations in MQL5.
| |
class UI
| |
{
| |
public:
| |
//+------------------------------------------------------------------+
| |
//| Lifecycle & System Control |
| |
//+------------------------------------------------------------------+
| |
| |
/// Initializes the GUI engine on the target chart and subwindow.
| |
static bool Init(const long chart_id = 0, const int subwindow = 0)
| |
{
| |
UInputState::SetChartId((chart_id == 0) ? ChartID() : chart_id);
| |
#ifdef UI_BACKEND_CANVAS
| |
return(URendererCanvas::Init(chart_id, subwindow));
| |
#else
| |
URendererNative::Init(chart_id, subwindow);
| |
return(true);
| |
#endif
| |
}
| |
| |
/// Shuts down the GUI engine and cleans up all graphics resources.
| |
static void Shutdown(void)
| |
{
| |
UInputState::EnableChartScroll();
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::Shutdown();
| |
#else
| |
URendererNative::Shutdown();
| |
#endif
| |
}
| |
| |
/// Feeds OnChartEvent events directly into DearMQL5 input pipeline.
| |
static void OnChartEvent(const int id,
| |
const long &lparam,
| |
const double &dparam,
| |
const string &sparam)
| |
{
| |
if(id == CHARTEVENT_MOUSE_MOVE)
| |
{
| |
UInputState::OnMouseMove((int)lparam, (int)dparam, (uint)StringToInteger(sparam));
| |
}
| |
}
| |
| |
/// Begins a new GUI frame. Must be called before any windows or widgets.
| |
static void NewFrame(void)
| |
{
| |
UDiagnostics::BeginFrame();
| |
UIdStack::Reset();
| |
UWindow::ResetScope();
| |
UInputState::NewFrame();
| |
UState::NextFrame();
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::BeginFrame();
| |
#else
| |
URendererNative::BeginFrame();
| |
#endif
| |
}
| |
| |
/// Finalizes and renders the current GUI frame to chart.
| |
static void Render(void)
| |
{
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::EndFrame();
| |
#else
| |
URendererNative::EndFrame();
| |
#endif
| |
UInputState::EndFrame();
| |
UDiagnostics::EndFrame();
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Windows Containers |
| |
//+------------------------------------------------------------------+
| |
| |
/// Begins an immediate-mode window. Returns true if window body is visible.
| |
static bool Begin(const string title,
| |
bool &io_open,
| |
const int default_x = 20,
| |
const int default_y = 40,
| |
const int default_w = 260,
| |
const int default_h = 320,
| |
const uint flags = U_WINDOW_NONE)
| |
{
| |
int x = 0, y = 0, w = 0, h = 0;
| |
bool collapsed = false;
| |
bool visible = UWindow::Begin(title, io_open, default_x, default_y, default_w, default_h, flags, x, y, w, h, collapsed);
| |
| |
SUTheme th = UTheme::Get();
| |
ulong win_id = UIdStack::GetId(title);
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::BeginWindowSurface(win_id, title, x, y, w, h);
| |
| |
// Draw Window Shadow & Outer Border
| |
URendererCanvas::DrawRectFilled(x, y, w, h, th.window_bg);
| |
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
| |
| |
// Draw Titlebar
| |
if((flags & U_WINDOW_NO_TITLEBAR) == 0)
| |
{
| |
URendererCanvas::DrawRectFilled(x, y, w, th.window_title_height, th.window_titlebar);
| |
URendererCanvas::DrawLine(x, y + th.window_title_height, x + w, y + th.window_title_height, th.window_border);
| |
| |
// Title Text
| |
URendererCanvas::DrawText(x + th.padding_x, y + 4, title, th.text, th.font_name, th.font_size, 0);
| |
| |
// Collapse icon
| |
if((flags & U_WINDOW_NO_COLLAPSE) == 0)
| |
{
| |
string icon = collapsed ? "[+]" : "[-]";
| |
URendererCanvas::DrawText(x + w - 24, y + 4, icon, th.text_dim, th.font_name, th.font_size, 0);
| |
}
| |
}
| |
#else
| |
URendererNative::DrawRectFilled(x, y, w, h, th.window_bg);
| |
if((flags & U_WINDOW_NO_TITLEBAR) == 0)
| |
{
| |
URendererNative::DrawRectFilled(x, y, w, th.window_title_height, th.window_titlebar);
| |
URendererNative::DrawText(x + th.padding_x, y + 4, title, th.text, th.font_name, th.font_size);
| |
}
| |
#endif
| |
| |
return(visible);
| |
}
| |
| |
/// Ends the current window scope.
| |
static void End(void)
| |
{
| |
UWindow::End();
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::EndWindowSurface();
| |
#endif
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Layout Controls |
| |
//+------------------------------------------------------------------+
| |
| |
/// Directs the next widget to be placed on the same line horizontally.
| |
static void SameLine(const int spacing = -1)
| |
{
| |
ULayout::SameLine(spacing);
| |
}
| |
| |
/// Indents subsequent widgets to the right.
| |
static void Indent(const int amount = 16)
| |
{
| |
ULayout::Indent(amount);
| |
}
| |
| |
/// Restores indentation to the left.
| |
static void Unindent(const int amount = 16)
| |
{
| |
ULayout::Unindent(amount);
| |
}
| |
| |
/// Draws a horizontal divider line.
| |
static void Separator(void)
| |
{
| |
int x, y, w, h;
| |
USeparator::GetBoundingBox(w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
SUTheme th = UTheme::Get();
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::DrawLine(x, y + 2, x + w, y + 2, th.separator);
| |
#else
| |
URendererNative::DrawLine(x, y + 2, x + w, y + 2, th.separator);
| |
#endif
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Text & Metric Widgets |
| |
//+------------------------------------------------------------------+
| |
| |
/// Renders plain text.
| |
static void Text(const string text)
| |
{
| |
TextColored(text, UTheme::Get().text);
| |
}
| |
| |
/// Renders colored text (e.g. green for profit, red for drawdown).
| |
static void TextColored(const string text, const uint argb)
| |
{
| |
int x = 0, y = 0, w = 0, h = 0;
| |
ULabel::GetBoundingBox(text, w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
SUTheme th = UTheme::Get();
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::DrawText(x, y + 2, text, argb, th.font_name, th.font_size, 0);
| |
#else
| |
URendererNative::DrawText(x, y + 2, text, argb, th.font_name, th.font_size);
| |
#endif
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Interactive Button Widgets |
| |
//+------------------------------------------------------------------+
| |
| |
/// Renders a clickable button. Returns true on the frame it was clicked.
| |
static bool Button(const string label, const int custom_w = 0, const int custom_h = 0)
| |
{
| |
return(ButtonColor(label, 0, custom_w, custom_h));
| |
}
| |
| |
/// Renders a colored button (e.g. accent for Buy, danger for Panic Close).
| |
static bool ButtonColor(const string label, const uint bg_color, const int custom_w = 0, const int custom_h = 0)
| |
{
| |
ulong id = UIdStack::GetId(label);
| |
| |
int x = 0, y = 0, w = 0, h = 0;
| |
UButton::GetBoundingBox(label, custom_w, custom_h, w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
bool hovered = false, clicked = false, active = false;
| |
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
| |
| |
uint btn_color = UButton::GetStateColor(hovered, active, bg_color);
| |
SUTheme th = UTheme::Get();
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::DrawRectFilled(x, y, w, h, btn_color);
| |
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
| |
| |
// Center text inside button
| |
int tw = UFont::MeasureTextWidth(label, th.font_size, th.font_name);
| |
int tx = x + (w - tw) / 2;
| |
int ty = y + (h - UFont::MeasureTextHeight(th.font_size)) / 2;
| |
URendererCanvas::DrawText(tx, ty, label, th.text, th.font_name, th.font_size, 0);
| |
#else
| |
URendererNative::DrawRectFilled(x, y, w, h, btn_color);
| |
URendererNative::DrawText(x + th.padding_x, y + 4, label, th.text, th.font_name, th.font_size);
| |
#endif
| |
| |
if(clicked)
| |
UEventQueue::Push(U_EVENT_CLICK, id, 0.0, label);
| |
| |
return(clicked);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Toggle Switch Widgets |
| |
//+------------------------------------------------------------------+
| |
| |
/// Renders an ON/OFF toggle switch. Returns true if state changed.
| |
static bool Toggle(const string label, bool &io_value)
| |
{
| |
ulong id = UIdStack::GetId(label);
| |
| |
int x = 0, y = 0, w = 0, h = 0;
| |
UToggle::GetBoundingBox(label, w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
bool hovered = false, clicked = false, active = false;
| |
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
| |
| |
if(clicked)
| |
{
| |
io_value = !io_value;
| |
UEventQueue::Push(U_EVENT_VALUE_CHANGE, id, io_value ? 1.0 : 0.0, label);
| |
}
| |
| |
SUTheme th = UTheme::Get();
| |
int sw_w = 34;
| |
int sw_h = 16;
| |
int sw_y = y + (h - sw_h) / 2;
| |
| |
uint track_color = io_value ? th.accent : th.widget_bg;
| |
int knob_x = io_value ? (x + sw_w - sw_h) : x;
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::DrawRectFilled(x, sw_y, sw_w, sw_h, track_color);
| |
URendererCanvas::DrawRectFilled(knob_x, sw_y, sw_h, sw_h, th.text);
| |
if(StringLen(label) > 0)
| |
URendererCanvas::DrawText(x + sw_w + th.item_spacing_x, y + 2, label, th.text, th.font_name, th.font_size, 0);
| |
#else
| |
URendererNative::DrawRectFilled(x, sw_y, sw_w, sw_h, track_color);
| |
URendererNative::DrawRectFilled(knob_x, sw_y, sw_h, sw_h, th.text);
| |
if(StringLen(label) > 0)
| |
URendererNative::DrawText(x + sw_w + th.item_spacing_x, y + 2, label, th.text, th.font_name, th.font_size);
| |
#endif
| |
| |
return(clicked);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Slider Widgets |
| |
//+------------------------------------------------------------------+
| |
| |
/// Draggable horizontal slider for float/double parameters.
| |
static bool SliderFloat(const string label,
| |
double &io_value,
| |
const double min_val,
| |
const double max_val,
| |
const string format = "%.2f",
| |
const int custom_w = 0)
| |
{
| |
ulong id = UIdStack::GetId(label);
| |
| |
int x = 0, y = 0, w = 0, h = 0;
| |
USlider::GetBoundingBox(label, custom_w, w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
bool hovered = false, clicked = false, active = false;
| |
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
| |
| |
bool changed = false;
| |
if(active && UInputState::IsMouseDown())
| |
{
| |
changed = USlider::ProcessDrag(x, w, min_val, max_val, io_value);
| |
if(changed)
| |
UEventQueue::Push(U_EVENT_VALUE_CHANGE, id, io_value, label);
| |
}
| |
| |
SUTheme th = UTheme::Get();
| |
double fraction = (max_val > min_val) ? ((io_value - min_val) / (max_val - min_val)) : 0.0;
| |
fraction = UMath::Clamp(fraction, 0.0, 1.0);
| |
int fill_w = (int)(w * fraction);
| |
| |
string display_text = StringFormat("%s: " + format, label, io_value);
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
// Track background
| |
URendererCanvas::DrawRectFilled(x, y, w, h, th.widget_bg);
| |
// Active fill
| |
if(fill_w > 0)
| |
URendererCanvas::DrawRectFilled(x, y, fill_w, h, th.accent);
| |
// Border
| |
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
| |
// Value text centered
| |
URendererCanvas::DrawText(x + th.padding_x, y + 2, display_text, th.text, th.font_name, th.font_size, 0);
| |
#else
| |
URendererNative::DrawRectFilled(x, y, w, h, th.widget_bg);
| |
if(fill_w > 0)
| |
URendererNative::DrawRectFilled(x, y, fill_w, h, th.accent);
| |
URendererNative::DrawText(x + th.padding_x, y + 2, display_text, th.text, th.font_name, th.font_size);
| |
#endif
| |
| |
return(changed);
| |
}
| |
| |
/// Draggable horizontal slider for integer parameters.
| |
static bool SliderInt(const string label, int &io_value, const int min_val, const int max_val, const int custom_w = 0)
| |
{
| |
double dval = (double)io_value;
| |
bool changed = SliderFloat(label, dval, (double)min_val, (double)max_val, "%.0f", custom_w);
| |
if(changed)
| |
io_value = (int)MathRound(dval);
| |
return(changed);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Progress & Status Widgets |
| |
//+------------------------------------------------------------------+
| |
| |
/// Visual progress bar with optional high-value alert color ramp (for Prop Firm limits).
| |
static void ProgressBar(const double fraction,
| |
const int custom_w = 0,
| |
const int custom_h = 0,
| |
const string overlay_text = "",
| |
const bool alert_on_high = false)
| |
{
| |
int x = 0, y = 0, w = 0, h = 0;
| |
UProgressBar::GetBoundingBox(custom_w, custom_h, w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
SUTheme th = UTheme::Get();
| |
double clamped_frac = UMath::Clamp(fraction, 0.0, 1.0);
| |
int fill_w = (int)(w * clamped_frac);
| |
uint fill_color = UProgressBar::GetProgressColor(clamped_frac, alert_on_high);
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
// Track
| |
URendererCanvas::DrawRectFilled(x, y, w, h, th.widget_bg);
| |
// Fill
| |
if(fill_w > 0)
| |
URendererCanvas::DrawRectFilled(x, y, fill_w, h, fill_color);
| |
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
| |
| |
// Overlay text if present
| |
if(StringLen(overlay_text) > 0)
| |
{
| |
int tw = UFont::MeasureTextWidth(overlay_text, th.font_size - 1, th.font_name);
| |
int tx = x + (w - tw) / 2;
| |
int ty = y + (h - UFont::MeasureTextHeight(th.font_size - 1)) / 2;
| |
URendererCanvas::DrawText(tx, ty, overlay_text, th.text, th.font_name, th.font_size - 1, 0);
| |
}
| |
#else
| |
URendererNative::DrawRectFilled(x, y, w, h, th.widget_bg);
| |
if(fill_w > 0)
| |
URendererNative::DrawRectFilled(x, y, fill_w, h, fill_color);
| |
if(StringLen(overlay_text) > 0)
| |
URendererNative::DrawText(x + th.padding_x, y + 1, overlay_text, th.text, th.font_name, th.font_size - 1);
| |
#endif
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Collapsing Section Header |
| |
//+------------------------------------------------------------------+
| |
| |
/// Collapsible section header. Returns true if section contents should be drawn.
| |
static bool CollapsingHeader(const string label, const bool default_open = true)
| |
{
| |
ulong id = UIdStack::GetId(label);
| |
| |
int x = 0, y = 0, w = 0, h = 0;
| |
UHeader::GetBoundingBox(w, h);
| |
ULayout::AllocateSpace(w, h, x, y);
| |
| |
bool hovered = false, clicked = false, active = false;
| |
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
| |
| |
bool is_open = UHeader::ProcessHeader(id, clicked, default_open);
| |
| |
SUTheme th = UTheme::Get();
| |
uint bg = hovered ? th.widget_hover : th.widget_bg;
| |
string icon = is_open ? "[-] " : "[+] ";
| |
string full_title = icon + label;
| |
| |
#ifdef UI_BACKEND_CANVAS
| |
URendererCanvas::DrawRectFilled(x, y, w, h, bg);
| |
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
| |
URendererCanvas::DrawText(x + th.padding_x, y + 3, full_title, th.text, th.font_name, th.font_size, 0);
| |
#else
| |
URendererNative::DrawRectFilled(x, y, w, h, bg);
| |
URendererNative::DrawText(x + th.padding_x, y + 3, full_title, th.text, th.font_name, th.font_size);
| |
#endif
| |
| |
return(is_open);
| |
}
| |
};
|