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);
| |||
}
| |||
};
|