497 lines
18 KiB
MQL5
497 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);
| |
| |
// Modern Vector Collapse Button with Hover/Active Pill Container
| |
if((flags & U_WINDOW_NO_COLLAPSE) == 0)
| |
{
| |
int btn_size = th.window_title_height - 6;
| |
int btn_x = x + w - btn_size - 6;
| |
int btn_y = y + 3;
| |
bool is_hov = UWindow::IsCollapseHovered(win_id);
| |
bool is_act = UWindow::IsCollapseActive(win_id);
| |
URendererCanvas::DrawCollapseButton(btn_x, btn_y, btn_size, collapsed, is_hov, is_act, th.text_dim);
| |
}
| |
}
| |
#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);
| |
if((flags & U_WINDOW_NO_COLLAPSE) == 0)
| |
{
| |
int btn_size = th.window_title_height - 6;
| |
int btn_x = x + w - btn_size - 6;
| |
int btn_y = y + 3;
| |
bool is_hov = UWindow::IsCollapseHovered(win_id);
| |
bool is_act = UWindow::IsCollapseActive(win_id);
| |
URendererNative::DrawCollapseButton(btn_x, btn_y, btn_size, collapsed, is_hov, is_act, th.text_dim);
| |
}
| |
}
| |
#endif
| |
| |
return(visible);
| |
}
| |
| |
/// Checks if a window is currently collapsed by its title.
| |
static bool IsWindowCollapsed(const string title)
| |
{
| |
ulong win_id = UIdStack::GetId(title);
| |
return(UWindow::IsWindowCollapsed(win_id));
| |
}
| |
| |
/// 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);
| |
}
| |
};
|