preview
MQL5 Trading Tools (Part 40): Adding SQLite Persistence and Per-Timeframe Visibility to the Canvas Drawing Layer

MQL5 Trading Tools (Part 40): Adding SQLite Persistence and Per-Timeframe Visibility to the Canvas Drawing Layer

MetaTrader 5Trading systems |
162 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In the previous article (Part 39), we added a pinned-tools ribbon, rounding out a toolkit that now draws, edits, styles, and organizes a deep set of chart objects. But everything that the toolkit produces lives only in memory. The moment we switch a timeframe, reload the chart, or restart the terminal, every drawing we placed, every style we tuned, every tool we pinned, and even the theme and panel positions are gone, and we start the next session from a blank chart. For a workflow built on marking up structure that we return to day after day, that is the one missing piece that makes all the rest feel temporary.

We address this in two ways and change the tool's program type. We add an SQLite persistence layer that saves every drawn object and the full interface session to a database. On startup, it restores them so the workspace resumes exactly where we left it. We add a per-timeframe visibility tab so each object appears only on the periods where it matters. Although MetaTrader 5 already supports this in the object properties dialog, we expose the same control in our tab alongside the other properties. We also convert the program from an Expert Advisor to an indicator, so it appears on the chart as a pure utility and can run alongside other indicators, utilities, or an EA, rather than taking the single Expert Advisor slot.

This article is written for the intermediate-to-advanced MetaQuotes Language 5 (MQL5) developer who is comfortable with the inheritance chain and the drawing engine we built in the previous parts. The subtopics we will cover are:

  1. From a Session Toolkit to a Persistent Workspace
  2. Building the SQLite Persistence Layer
  3. Wiring Persistence into the Session Lifecycle
  4. Adding the Per-Timeframe Visibility Tab
  5. Visualization
  6. Conclusion

By the end, you will be able to close and reopen the terminal and restore the workspace. Drawings and styles will persist, the interface theme and layout will reload, and each object will display only on selected timeframes. The tool runs as an indicator alongside other chart components.


From a Session Toolkit to a Persistent Workspace

Persistence comes down to two questions: what state matters, and where do we keep it. Three kinds of state matter here. There are the drawn objects themselves; their geometry, their styles, and now the timeframe mask that says where each one shows. There is the per-tool style memory that remembers how we last configured each tool. And there is the interface session; the theme, the sidebar's position and height, both ribbons' last placements, the pinned set, the active tool, and the current selection. Lose any of these, and the workspace feels like it forgot us between sessions.

For where to keep it, we reach for MQL5 SQLite, which MetaTrader 5 ships with built-in, so we get a real database with no external dependency. We lay it out as two simple stores: one that holds the drawn objects keyed per symbol, so each chart restores only its own marks, and one key-value store for the interface session. Rather than freeze each object as a rigid binary blob, we serialize it to a versioned text payload, so the format can grow in later parts without breaking the rows already written. To keep the writes cheap, we mark the state dirty only when something actually changes, flush when the activity settles, and again on shutdown, and read everything back once on startup.

The visibility side is a smaller idea sitting on the same foundation. Basically, we extended what we built; we felt we could control this from the main terminal window, but for better control, we brought it to our side. We give every object a mask of the periods it should appear on, the render pass skips any object whose mask excludes the current chart period, and a dedicated tab lets us edit that mask per object. Converting the program to an indicator lets it run alongside other tools instead of occupying the single Expert Advisor slot. This matters more once the chart becomes a durable, shared workspace. So, we can now attach it alongside other utility tools, indicators, and Expert Advisors. In a nutshell, here is what we intend to achieve.

PERSISTENCE ARCHITECTURE


Building the SQLite Persistence Layer

The Segment Codec

The groundwork for the persistence layer is a small codec that flattens a typed array into a single text segment. We need it because a drawn object carries several parallel arrays: the Fibonacci levels, the path points, the per-level colors and widths, and the whole object has to collapse into one string we can store in a table cell. We build this layer in a new database file.

//+------------------------------------------------------------------+
//|                                        ToolsPalette_Database.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Guard against multiple inclusion of this header
#ifndef TOOLS_PALETTE_DATABASE_MQH
#define TOOLS_PALETTE_DATABASE_MQH

//--- Pull in CDrawingEngine class declaration (Tools.mqh include guard handles double-load)
#include "ToolsPalette_Tools.mqh"

//--- SQLite file (under MQL5\Files\Database) + reserved symbol key for per-tool style memory rows
#define TOOLS_PALETTE_DB_FILENAME   "Database\\ToolsPaletteSQL.db"
#define TOOLS_PALETTE_DB_MEMORY_KEY "@toolmem"

//+------------------------------------------------------------------+
//| Append an int array as one payload segment (count,v1,v2,...)     |
//+------------------------------------------------------------------+
void SegWriteIntArr(string &s, const int &arr[])
  {
   //--- Leading count + comma-joined values form a single ';'-delimited segment
   const int n = ArraySize(arr);
   string seg = IntegerToString(n);
   for(int i = 0; i < n; i++) seg += "," + IntegerToString(arr[i]);
   s += ";" + seg;
  }

//+------------------------------------------------------------------+
//| Read one int-array payload segment back; returns advanced cursor |
//+------------------------------------------------------------------+
int SegReadIntArr(const string &parts[], int c, int &arr[])
  {
   //--- Out-of-range cursor leaves the array empty
   if(c >= ArraySize(parts)) { ArrayResize(arr, 0); return c; }
   string vals[];
   const int m = StringSplit(parts[c], ',', vals);
   const int n = (m > 0) ? (int)StringToInteger(vals[0]) : 0;
   ArrayResize(arr, n);
   for(int i = 0; i < n && i + 1 < m; i++) arr[i] = (int)StringToInteger(vals[i + 1]);
   return c + 1;
  }

Like we have been doing, the header guards against double inclusion and pulls in the Tools layer so the engine class and the object structure are visible. Two constants anchor it: the database file path, which sits under the terminal's files folder in a "Database" subfolder, and a reserved symbol key that keeps the per-tool style memory rows separate from the real drawn objects. You can place it directly in the default folder, but we chose an extra layer for organization.

Writing an array falls to "SegWriteIntArr". It leads with the element count, comma-joins the values after it, and prefixes the whole thing with a semicolon so the result becomes one self-delimited segment appended to the growing payload. Encoding the count up front is what lets us know exactly how many values to pull back without having to guess where the segment ends.

Reading it back, "SegReadIntArr" takes the already-split payload parts and a cursor index, splits the segment on its commas, reads the leading count, sizes the output array to it, and pulls that many values; then returns the cursor advanced by one so the caller moves on to the next segment. This cursor-passing style is what lets the deserializer walk the payload segment by segment in the order they were written.

The same write-and-read pair exists for every other type the object holds — doubles, colors, booleans, and datetimes — differing only in the per-value conversion each one applies, so there is no need to walk through them individually. With the codec in place, we move on to serializing and deserializing a whole object.

Serializing and Rebuilding an Object

We flatten a whole object into one versioned payload with "SerializeDrawnObject", and we reverse the process with "DeserializeDrawnObject" to rebuild it field for field.

//+------------------------------------------------------------------+
//| Serialize a DrawnObject into the versioned text payload          |
//+------------------------------------------------------------------+
string SerializeDrawnObject(const DrawnObject &o)
  {
   //--- Version prefix first; every struct field follows in declaration order (labelText is a separate column)
   string s = "2";
   s += ";" + IntegerToString((int)o.toolType);
   s += ";" + IntegerToString(o.id);
   s += ";" + IntegerToString((long)o.time1);
   s += ";" + StringFormat("%.17g", o.price1);
   s += ";" + IntegerToString((long)o.time2);
   s += ";" + StringFormat("%.17g", o.price2);
   s += ";" + IntegerToString((long)o.time3);
   s += ";" + StringFormat("%.17g", o.price3);
   SegWriteTimeArr(s, o.pathTimes);
   SegWriteDblArr(s, o.pathPrices);
   s += ";" + IntegerToString((int)o.objColor);
   s += ";" + (o.selected ? "1" : "0");
   s += ";" + (o.visible ? "1" : "0");
   s += ";" + IntegerToString(o.timeframes);
   s += ";" + IntegerToString(o.lineWidth);
   s += ";" + IntegerToString(o.lineStyle);
   s += ";" + IntegerToString((int)o.textColor);
   s += ";" + IntegerToString(o.lineOpacity);
   s += ";" + IntegerToString(o.textOpacity);
   s += ";" + IntegerToString(o.fontSize);
   s += ";" + (o.bold ? "1" : "0");
   s += ";" + IntegerToString(o.vAlign);
   s += ";" + IntegerToString(o.hAlign);
   s += ";" + IntegerToString((int)o.fillColor);
   s += ";" + IntegerToString(o.fillOpacity);
   s += ";" + IntegerToString((int)o.borderColor);
   s += ";" + IntegerToString(o.borderOpacity);
   s += ";" + IntegerToString((int)o.midColor);
   s += ";" + IntegerToString(o.midOpacity);
   s += ";" + IntegerToString((int)o.fillColor2);
   s += ";" + IntegerToString(o.fillOpacity2);
   s += ";" + (o.midVisible ? "1" : "0");
   s += ";" + IntegerToString(o.midWidth);
   s += ";" + IntegerToString(o.midStyle);
   s += ";" + StringFormat("%.17g", o.midOffset);
   s += ";" + (o.centerVisible ? "1" : "0");
   s += ";" + IntegerToString((int)o.centerColor);
   s += ";" + IntegerToString(o.centerOpacity);
   s += ";" + IntegerToString(o.centerWidth);
   s += ";" + IntegerToString(o.centerStyle);
   s += ";" + (o.upperBandVisible ? "1" : "0");
   s += ";" + StringFormat("%.17g", o.upperBandSigma);
   s += ";" + (o.lowerBandVisible ? "1" : "0");
   s += ";" + StringFormat("%.17g", o.lowerBandSigma);
   s += ";" + (o.pearsonVisible ? "1" : "0");
   SegWriteDblArr(s, o.fiboLevelRatio);
   SegWriteColArr(s, o.fiboLevelColor);
   SegWriteIntArr(s, o.fiboLevelOpacity);
   SegWriteIntArr(s, o.fiboLevelWidth);
   SegWriteIntArr(s, o.fiboLevelStyle);
   SegWriteBoolArr(s, o.fiboLevelVisible);
   SegWriteDblArr(s, o.fibexLevelRatio);
   SegWriteColArr(s, o.fibexLevelColor);
   SegWriteIntArr(s, o.fibexLevelOpacity);
   SegWriteIntArr(s, o.fibexLevelWidth);
   SegWriteIntArr(s, o.fibexLevelStyle);
   SegWriteBoolArr(s, o.fibexLevelVisible);
   SegWriteDblArr(s, o.fibchLevelRatio);
   SegWriteColArr(s, o.fibchLevelColor);
   SegWriteIntArr(s, o.fibchLevelOpacity);
   SegWriteIntArr(s, o.fibchLevelWidth);
   SegWriteIntArr(s, o.fibchLevelStyle);
   SegWriteBoolArr(s, o.fibchLevelVisible);
   SegWriteDblArr(s, o.fibtzLevelRatio);
   SegWriteColArr(s, o.fibtzLevelColor);
   SegWriteIntArr(s, o.fibtzLevelOpacity);
   SegWriteIntArr(s, o.fibtzLevelWidth);
   SegWriteIntArr(s, o.fibtzLevelStyle);
   SegWriteBoolArr(s, o.fibtzLevelVisible);
   SegWriteDblArr(s, o.fibfanLevelRatio);
   SegWriteColArr(s, o.fibfanLevelColor);
   SegWriteIntArr(s, o.fibfanLevelOpacity);
   SegWriteIntArr(s, o.fibfanLevelWidth);
   SegWriteIntArr(s, o.fibfanLevelStyle);
   SegWriteBoolArr(s, o.fibfanLevelVisible);
   SegWriteDblArr(s, o.fibarcLevelRatio);
   SegWriteColArr(s, o.fibarcLevelColor);
   SegWriteIntArr(s, o.fibarcLevelOpacity);
   SegWriteIntArr(s, o.fibarcLevelWidth);
   SegWriteIntArr(s, o.fibarcLevelStyle);
   SegWriteBoolArr(s, o.fibarcLevelVisible);
   SegWriteDblArr(s, o.gannfanLevelRatio);
   SegWriteColArr(s, o.gannfanLevelColor);
   SegWriteIntArr(s, o.gannfanLevelOpacity);
   SegWriteIntArr(s, o.gannfanLevelWidth);
   SegWriteIntArr(s, o.gannfanLevelStyle);
   SegWriteBoolArr(s, o.gannfanLevelVisible);
   SegWriteDblArr(s, o.gannboxLevelRatio);
   SegWriteColArr(s, o.gannboxLevelColor);
   SegWriteIntArr(s, o.gannboxLevelOpacity);
   SegWriteIntArr(s, o.gannboxLevelWidth);
   SegWriteIntArr(s, o.gannboxLevelStyle);
   SegWriteBoolArr(s, o.gannboxLevelVisible);
   s += ";" + (o.medianVisible ? "1" : "0");
   s += ";" + IntegerToString((int)o.medianColor);
   s += ";" + IntegerToString(o.medianWidth);
   s += ";" + IntegerToString(o.medianStyle);
   s += ";" + IntegerToString(o.medianOpacity);
   s += ";" + (o.outerVisible ? "1" : "0");
   s += ";" + IntegerToString((int)o.outerColor);
   s += ";" + IntegerToString(o.outerWidth);
   s += ";" + IntegerToString(o.outerStyle);
   s += ";" + IntegerToString(o.outerOpacity);
   s += ";" + (o.innerVisible ? "1" : "0");
   s += ";" + IntegerToString((int)o.innerColor);
   s += ";" + IntegerToString(o.innerWidth);
   s += ";" + IntegerToString(o.innerStyle);
   s += ";" + IntegerToString(o.innerOpacity);
   return s;
  }

//+------------------------------------------------------------------+
//| Deserialize the text payload back into a DrawnObject             |
//+------------------------------------------------------------------+
bool DeserializeDrawnObject(const string payload, const string label, DrawnObject &o)
  {
   //--- Split into segments + verify the version prefix and minimum field count
   string parts[];
   const int total = StringSplit(payload, ';', parts);
   if(total < 109 || parts[0] != "2") return false;
   int c = 1;
   o.toolType = (TOOL_TYPE)(int)StringToInteger(parts[c]); c++;
   o.id = (int)StringToInteger(parts[c]); c++;
   o.time1 = (datetime)StringToInteger(parts[c]); c++;
   o.price1 = StringToDouble(parts[c]); c++;
   o.time2 = (datetime)StringToInteger(parts[c]); c++;
   o.price2 = StringToDouble(parts[c]); c++;
   o.time3 = (datetime)StringToInteger(parts[c]); c++;
   o.price3 = StringToDouble(parts[c]); c++;
   c = SegReadTimeArr(parts, c, o.pathTimes);
   c = SegReadDblArr(parts, c, o.pathPrices);
   o.objColor = (color)(int)StringToInteger(parts[c]); c++;
   o.selected = (StringToInteger(parts[c]) != 0); c++;
   o.visible = (StringToInteger(parts[c]) != 0); c++;
   o.timeframes = (int)StringToInteger(parts[c]); c++;
   o.lineWidth = (int)StringToInteger(parts[c]); c++;
   o.lineStyle = (int)StringToInteger(parts[c]); c++;
   o.textColor = (color)(int)StringToInteger(parts[c]); c++;
   o.lineOpacity = (int)StringToInteger(parts[c]); c++;
   o.textOpacity = (int)StringToInteger(parts[c]); c++;
   o.fontSize = (int)StringToInteger(parts[c]); c++;
   o.bold = (StringToInteger(parts[c]) != 0); c++;
   o.vAlign = (int)StringToInteger(parts[c]); c++;
   o.hAlign = (int)StringToInteger(parts[c]); c++;
   o.fillColor = (color)(int)StringToInteger(parts[c]); c++;
   o.fillOpacity = (int)StringToInteger(parts[c]); c++;
   o.borderColor = (color)(int)StringToInteger(parts[c]); c++;
   o.borderOpacity = (int)StringToInteger(parts[c]); c++;
   o.midColor = (color)(int)StringToInteger(parts[c]); c++;
   o.midOpacity = (int)StringToInteger(parts[c]); c++;
   o.fillColor2 = (color)(int)StringToInteger(parts[c]); c++;
   o.fillOpacity2 = (int)StringToInteger(parts[c]); c++;
   o.midVisible = (StringToInteger(parts[c]) != 0); c++;
   o.midWidth = (int)StringToInteger(parts[c]); c++;
   o.midStyle = (int)StringToInteger(parts[c]); c++;
   o.midOffset = StringToDouble(parts[c]); c++;
   o.centerVisible = (StringToInteger(parts[c]) != 0); c++;
   o.centerColor = (color)(int)StringToInteger(parts[c]); c++;
   o.centerOpacity = (int)StringToInteger(parts[c]); c++;
   o.centerWidth = (int)StringToInteger(parts[c]); c++;
   o.centerStyle = (int)StringToInteger(parts[c]); c++;
   o.upperBandVisible = (StringToInteger(parts[c]) != 0); c++;
   o.upperBandSigma = StringToDouble(parts[c]); c++;
   o.lowerBandVisible = (StringToInteger(parts[c]) != 0); c++;
   o.lowerBandSigma = StringToDouble(parts[c]); c++;
   o.pearsonVisible = (StringToInteger(parts[c]) != 0); c++;
   c = SegReadDblArr(parts, c, o.fiboLevelRatio);
   c = SegReadColArr(parts, c, o.fiboLevelColor);
   c = SegReadIntArr(parts, c, o.fiboLevelOpacity);
   c = SegReadIntArr(parts, c, o.fiboLevelWidth);
   c = SegReadIntArr(parts, c, o.fiboLevelStyle);
   c = SegReadBoolArr(parts, c, o.fiboLevelVisible);
   c = SegReadDblArr(parts, c, o.fibexLevelRatio);
   c = SegReadColArr(parts, c, o.fibexLevelColor);
   c = SegReadIntArr(parts, c, o.fibexLevelOpacity);
   c = SegReadIntArr(parts, c, o.fibexLevelWidth);
   c = SegReadIntArr(parts, c, o.fibexLevelStyle);
   c = SegReadBoolArr(parts, c, o.fibexLevelVisible);
   c = SegReadDblArr(parts, c, o.fibchLevelRatio);
   c = SegReadColArr(parts, c, o.fibchLevelColor);
   c = SegReadIntArr(parts, c, o.fibchLevelOpacity);
   c = SegReadIntArr(parts, c, o.fibchLevelWidth);
   c = SegReadIntArr(parts, c, o.fibchLevelStyle);
   c = SegReadBoolArr(parts, c, o.fibchLevelVisible);
   c = SegReadDblArr(parts, c, o.fibtzLevelRatio);
   c = SegReadColArr(parts, c, o.fibtzLevelColor);
   c = SegReadIntArr(parts, c, o.fibtzLevelOpacity);
   c = SegReadIntArr(parts, c, o.fibtzLevelWidth);
   c = SegReadIntArr(parts, c, o.fibtzLevelStyle);
   c = SegReadBoolArr(parts, c, o.fibtzLevelVisible);
   c = SegReadDblArr(parts, c, o.fibfanLevelRatio);
   c = SegReadColArr(parts, c, o.fibfanLevelColor);
   c = SegReadIntArr(parts, c, o.fibfanLevelOpacity);
   c = SegReadIntArr(parts, c, o.fibfanLevelWidth);
   c = SegReadIntArr(parts, c, o.fibfanLevelStyle);
   c = SegReadBoolArr(parts, c, o.fibfanLevelVisible);
   c = SegReadDblArr(parts, c, o.fibarcLevelRatio);
   c = SegReadColArr(parts, c, o.fibarcLevelColor);
   c = SegReadIntArr(parts, c, o.fibarcLevelOpacity);
   c = SegReadIntArr(parts, c, o.fibarcLevelWidth);
   c = SegReadIntArr(parts, c, o.fibarcLevelStyle);
   c = SegReadBoolArr(parts, c, o.fibarcLevelVisible);
   c = SegReadDblArr(parts, c, o.gannfanLevelRatio);
   c = SegReadColArr(parts, c, o.gannfanLevelColor);
   c = SegReadIntArr(parts, c, o.gannfanLevelOpacity);
   c = SegReadIntArr(parts, c, o.gannfanLevelWidth);
   c = SegReadIntArr(parts, c, o.gannfanLevelStyle);
   c = SegReadBoolArr(parts, c, o.gannfanLevelVisible);
   c = SegReadDblArr(parts, c, o.gannboxLevelRatio);
   c = SegReadColArr(parts, c, o.gannboxLevelColor);
   c = SegReadIntArr(parts, c, o.gannboxLevelOpacity);
   c = SegReadIntArr(parts, c, o.gannboxLevelWidth);
   c = SegReadIntArr(parts, c, o.gannboxLevelStyle);
   c = SegReadBoolArr(parts, c, o.gannboxLevelVisible);
   o.medianVisible = (StringToInteger(parts[c]) != 0); c++;
   o.medianColor = (color)(int)StringToInteger(parts[c]); c++;
   o.medianWidth = (int)StringToInteger(parts[c]); c++;
   o.medianStyle = (int)StringToInteger(parts[c]); c++;
   o.medianOpacity = (int)StringToInteger(parts[c]); c++;
   o.outerVisible = (StringToInteger(parts[c]) != 0); c++;
   o.outerColor = (color)(int)StringToInteger(parts[c]); c++;
   o.outerWidth = (int)StringToInteger(parts[c]); c++;
   o.outerStyle = (int)StringToInteger(parts[c]); c++;
   o.outerOpacity = (int)StringToInteger(parts[c]); c++;
   o.innerVisible = (StringToInteger(parts[c]) != 0); c++;
   o.innerColor = (color)(int)StringToInteger(parts[c]); c++;
   o.innerWidth = (int)StringToInteger(parts[c]); c++;
   o.innerStyle = (int)StringToInteger(parts[c]); c++;
   o.innerOpacity = (int)StringToInteger(parts[c]); c++;
   //--- Label arrives from its own column; loaded objects always start deselected
   o.labelText = label;
   o.selected  = false;
   return true;
  }

We start "SerializeDrawnObject" by writing a version prefix, then we append every structure field after it in declaration order. For the scalars, we convert each one to text — the tool type, the ID, the three anchor times and prices, the colors, opacities, widths, styles, and all the channel, regression, and pitchfork flags. For the parallel arrays, we hand each one to the codec from the previous step, so the path points and every tool's per-level ratio, color, opacity, width, style, and visibility arrays each become a self-delimited segment. We deliberately leave the label out of this payload, since we store it in its own database column rather than inside the blob.

We lead with that version tag for a reason. By stamping the payload with a format number, we let later parts grow the structure and still recognize rows written by an earlier build, so we never silently misread an old object as a new one.

On the way back, we open "DeserializeDrawnObject" by splitting the payload on its delimiter, and we reject it immediately if the version does not match or the field count falls short of the minimum we expect; a cheap guard against a truncated or foreign row. We then walk the very same order we wrote in, advancing a running cursor as we read each scalar and letting the codec readers move the cursor through the array segments. Keeping the read order locked to the write order is what keeps the two halves in step.

We finish by taking the label from the separate column the caller passes in, and we force every loaded object deselected, since a freshly restored chart should come up with nothing highlighted. With a single object able to round-trip through text, we move on to the database itself.

Opening the Database and Ensuring the Schema

We create two functions to manage the database lifecycle; "DbOpen" to open or create the toolkit database and guarantee its tables exist, and "DbClose" to release the handle.

//+------------------------------------------------------------------+
//| Open or create the toolkit database + ensure the schema          |
//+------------------------------------------------------------------+
bool CDrawingEngine::DbOpen()
  {
   //--- Reuse an already-open handle
   if(m_dbHandle != INVALID_HANDLE) return true;
   m_dbHandle = DatabaseOpen(TOOLS_PALETTE_DB_FILENAME, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(m_dbHandle == INVALID_HANDLE)
     {
      Print("Tools Palette DbOpen: failed to open '", TOOLS_PALETTE_DB_FILENAME, "' error=", GetLastError());
      return false;
     }
   //--- One row per drawn object; payload carries every field, label is queryable text
   if(!DatabaseExecute(m_dbHandle,
      "CREATE TABLE IF NOT EXISTS drawings ("
      "  symbol  TEXT    NOT NULL,"
      "  obj_id  INTEGER NOT NULL,"
      "  tool    INTEGER NOT NULL,"
      "  label   TEXT    NOT NULL DEFAULT '',"
      "  payload TEXT    NOT NULL,"
      "  PRIMARY KEY(symbol, obj_id)"
      ");"))
     {
      Print("Tools Palette DbOpen: create drawings table failed: ", GetLastError());
      DatabaseClose(m_dbHandle);
      m_dbHandle = INVALID_HANDLE;
      return false;
     }
   //--- Generic key/value table reserved for future persisted settings
   if(!DatabaseExecute(m_dbHandle,
      "CREATE TABLE IF NOT EXISTS settings ("
      "  k TEXT PRIMARY KEY,"
      "  v TEXT NOT NULL"
      ");"))
     {
      Print("Tools Palette DbOpen: create settings table failed: ", GetLastError());
      DatabaseClose(m_dbHandle);
      m_dbHandle = INVALID_HANDLE;
      return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Close the toolkit database handle if it is open                  |
//+------------------------------------------------------------------+
void CDrawingEngine::DbClose()
  {
   //--- Idempotent close
   if(m_dbHandle == INVALID_HANDLE) return;
   DatabaseClose(m_dbHandle);
   m_dbHandle = INVALID_HANDLE;
  }

We design "DbOpen" to reuse an already-open handle when one exists, so repeated calls never reopen the file. When none exists, we open the database with the read-write flag together with the create flag, which makes the terminal create the file on the first run and reuse it on every run after, and we log the error and return false if the handle comes back invalid.

We define the function to create the drawings table whenever it is absent, giving it one row per drawn object, keyed by the pair of symbol and object ID, so that each chart owns its own rows. We keep the tool type and the label as their own columns and store the full serialized payload in a text column, and we hold the label separately rather than burying it in the payload, so we can read or search it without unpacking the whole structure.

We follow with a settings table, a simple key-and-value store we reserve for the interface session and any other persisted preferences. We guard each step. On failure, we print the error, close the handle, and reset it to the invalid sentinel. This prevents a half-initialized database state.

We make "DbClose" idempotent; when a handle is open, we close it and reset it to the sentinel, and when nothing is open, we return at once, so calling it twice or calling it on a session that never opened a database does no harm. After execution, we have tables and a schema created as follows.

DATABASE AND SCHEMA

With the database ready and its schema assured, we move on to writing the objects into it.

Writing the Snapshot to the Database

We create "PersistAllObjects" to write a full snapshot of this symbol's objects and the per-tool style memory into the database, and "PersistIfDirty" to gate that work behind the dirty flag.

//+------------------------------------------------------------------+
//| Persist a full snapshot of this symbol's objects + memory        |
//+------------------------------------------------------------------+
void CDrawingEngine::PersistAllObjects()
  {
   //--- Bail quietly when persistence is unavailable (toolkit keeps working)
   if(!DbOpen()) return;
   DatabaseTransactionBegin(m_dbHandle);
   //--- Replace this symbol's rows with the live object set
   int del = DatabasePrepare(m_dbHandle, "DELETE FROM drawings WHERE symbol=?;");
   if(del != INVALID_HANDLE)
     {
      DatabaseBind(del, 0, _Symbol);
      DatabaseRead(del);
      DatabaseFinalize(del);
     }
   const int n = ArraySize(m_drawnObjects);
   for(int i = 0; i < n; i++)
     {
      int ins = DatabasePrepare(m_dbHandle,
         "INSERT INTO drawings (symbol,obj_id,tool,label,payload) VALUES (?,?,?,?,?);");
      if(ins == INVALID_HANDLE) continue;
      DatabaseBind(ins, 0, _Symbol);
      DatabaseBind(ins, 1, m_drawnObjects[i].id);
      DatabaseBind(ins, 2, (int)m_drawnObjects[i].toolType);
      DatabaseBind(ins, 3, m_drawnObjects[i].labelText);
      DatabaseBind(ins, 4, SerializeDrawnObject(m_drawnObjects[i]));
      DatabaseRead(ins);
      DatabaseFinalize(ins);
     }
   //--- Replace the global per-tool style-memory rows under the reserved key
   int dm = DatabasePrepare(m_dbHandle, "DELETE FROM drawings WHERE symbol=?;");
   if(dm != INVALID_HANDLE)
     {
      DatabaseBind(dm, 0, TOOLS_PALETTE_DB_MEMORY_KEY);
      DatabaseRead(dm);
      DatabaseFinalize(dm);
     }
   for(int t = 0; t < 64; t++)
     {
      if(!m_toolMemoryValid[t]) continue;
      int mi = DatabasePrepare(m_dbHandle,
         "INSERT INTO drawings (symbol,obj_id,tool,label,payload) VALUES (?,?,?,?,?);");
      if(mi == INVALID_HANDLE) continue;
      DatabaseBind(mi, 0, TOOLS_PALETTE_DB_MEMORY_KEY);
      DatabaseBind(mi, 1, t);
      DatabaseBind(mi, 2, t);
      DatabaseBind(mi, 3, m_toolMemory[t].labelText);
      DatabaseBind(mi, 4, SerializeDrawnObject(m_toolMemory[t]));
      DatabaseRead(mi);
      DatabaseFinalize(mi);
     }
   DatabaseTransactionCommit(m_dbHandle);
   m_dbDirty = false;
  }

//+------------------------------------------------------------------+
//| Persist only when a mutation marked the snapshot stale           |
//+------------------------------------------------------------------+
void CDrawingEngine::PersistIfDirty()
  {
   //--- Cheap no-op on the hot path; real work only after a mutation
   if(m_dbDirty) PersistAllObjects();
  }

We have "PersistAllObjects" bail quietly when the database cannot be opened, so a persistence failure never stops the toolkit from working — the drawings simply stay in memory for that session. When the database is available, we wrap the whole write in a transaction so the snapshot commits as one atomic unit and so the many small inserts run fast rather than each paying its own disk cost.

We take a replace approach rather than trying to compare the database against memory row by row. We delete every row for this symbol, then insert each live object in turn, binding its symbol, ID, tool type, label, and serialized payload into the prepared statement. Because the object set on a chart is small and we already hold the full truth in memory, clearing the rows and rewriting them is simpler than reconciling individual differences, and it leaves no stale entries behind.

We then repeat that same delete-and-insert for the per-tool style memory, storing it under the reserved memory key so it never collides with a real symbol's rows, and we write only the slots marked valid. Once both sets are in, we commit the transaction and clear the dirty flag, since the database now matches memory exactly.

We design "PersistIfDirty" as the cheap guard for the hot path — it checks the dirty flag and calls the full persist only when a mutation has actually marked the snapshot stale, so the common case where nothing changed costs us nothing more than a single boolean test. With the writing path in place, we move on to loading the objects back.

Loading the Objects Back

We create "LoadPersistedObjects" to read this symbol's stored objects and the per-tool style memory back from the database when the chart starts up.

//+------------------------------------------------------------------+
//| Load this symbol's persisted objects + per-tool memory           |
//+------------------------------------------------------------------+
void CDrawingEngine::LoadPersistedObjects()
  {
   //--- Bail quietly when persistence is unavailable
   if(!DbOpen()) return;
   //--- Load this symbol's drawn objects in creation order
   int sel = DatabasePrepare(m_dbHandle,
      "SELECT obj_id, label, payload FROM drawings WHERE symbol=? ORDER BY obj_id;");
   if(sel != INVALID_HANDLE)
     {
      DatabaseBind(sel, 0, _Symbol);
      while(DatabaseRead(sel))
        {
         string lbl = "", payload = "";
         DatabaseColumnText(sel, 1, lbl);
         DatabaseColumnText(sel, 2, payload);
         const int k = ArraySize(m_drawnObjects);
         ArrayResize(m_drawnObjects, k + 1);
         //--- Deserialize straight into the live slot (avoids the struct array-copy pitfall)
         if(!DeserializeDrawnObject(payload, lbl, m_drawnObjects[k]))
           {
            //--- Corrupt or future-version row - drop it and continue with the rest
            ArrayResize(m_drawnObjects, k);
            continue;
           }
         if(m_drawnObjects[k].id > m_drawnObjectCounter)
            m_drawnObjectCounter = m_drawnObjects[k].id;
        }
      DatabaseFinalize(sel);
     }
   m_drawnObjectCount = ArraySize(m_drawnObjects);
   //--- Load the global per-tool style memory from the reserved key
   int mem = DatabasePrepare(m_dbHandle,
      "SELECT obj_id, label, payload FROM drawings WHERE symbol=?;");
   if(mem != INVALID_HANDLE)
     {
      DatabaseBind(mem, 0, TOOLS_PALETTE_DB_MEMORY_KEY);
      while(DatabaseRead(mem))
        {
         long slot = 0;
         DatabaseColumnLong(mem, 0, slot);
         if(slot < 0 || slot >= 64) continue;
         string lbl = "", payload = "";
         DatabaseColumnText(mem, 1, lbl);
         DatabaseColumnText(mem, 2, payload);
         if(DeserializeDrawnObject(payload, lbl, m_toolMemory[(int)slot]))
            m_toolMemoryValid[(int)slot] = true;
        }
      DatabaseFinalize(mem);
     }
   //--- Paint whatever was restored
   RedrawAllObjects();
  }

Here, we have the function bail quietly when the database cannot be opened, mirroring the write path, so a missing or locked database leaves us with an empty chart rather than an error. When it is available, we select this symbol's rows ordered by object ID, which brings them back in creation order and preserves the original sequence in which the objects were drawn.

We grow the object array by one for each row and deserialize directly into that newly added live slot, rather than filling a temporary and copying it in. We do this deliberately to avoid the pitfall of copying a structure that owns dynamic arrays. As we read, we also track the highest ID we see into the object counter, so any new object placed after the load still receives a unique ID that cannot clash with a restored one.

We treat a row that fails to deserialize as disposable. When the payload is corrupt or carries a future format version our build does not understand, we shrink the array back by the slot we just added and move on, so a single unreadable row never aborts the whole restore.

We then read the per-tool style memory from the reserved key, bounds-checking each slot against the memory array before we use it, deserializing the payload into that slot, and marking the slot valid only when the read succeeds. Once both the objects and the memory are restored, we repaint the chart so everything we loaded appears at once. With loading and saving both in place, we move on to wiring them into the session lifecycle, but first, we declare the settings helpers.

The Settings Key-and-Value Helpers

We create four helpers over the settings table — a string pair that writes and reads one row, and an integer pair that wraps the string pair for numeric values.

//+------------------------------------------------------------------+
//| Set or replace one key/value row in the settings table           |
//+------------------------------------------------------------------+
void CDrawingEngine::DbSetSetting(const string k, const string v)
  {
   //--- Quietly skip when the database is unavailable
   if(!DbOpen()) return;
   int st = DatabasePrepare(m_dbHandle, "INSERT OR REPLACE INTO settings (k,v) VALUES (?,?);");
   if(st == INVALID_HANDLE) return;
   DatabaseBind(st, 0, k);
   DatabaseBind(st, 1, v);
   DatabaseRead(st);
   DatabaseFinalize(st);
  }

//+------------------------------------------------------------------+
//| Read one settings value by key; false when the key is absent     |
//+------------------------------------------------------------------+
bool CDrawingEngine::DbGetSetting(const string k, string &v)
  {
   //--- Quietly fail when the database is unavailable
   if(!DbOpen()) return false;
   int st = DatabasePrepare(m_dbHandle, "SELECT v FROM settings WHERE k=?;");
   if(st == INVALID_HANDLE) return false;
   DatabaseBind(st, 0, k);
   bool ok = DatabaseRead(st);
   if(ok) DatabaseColumnText(st, 0, v);
   DatabaseFinalize(st);
   return ok;
  }

//+------------------------------------------------------------------+
//| Integer convenience wrapper over DbSetSetting                    |
//+------------------------------------------------------------------+
void CDrawingEngine::DbSetSettingInt(const string k, const long v)
  {
   //--- Integers ride the settings text column
   DbSetSetting(k, IntegerToString(v));
  }

//+------------------------------------------------------------------+
//| Integer convenience wrapper over DbGetSetting                    |
//+------------------------------------------------------------------+
bool CDrawingEngine::DbGetSettingInt(const string k, long &v)
  {
   //--- A missing key leaves v untouched
   string s = "";
   if(!DbGetSetting(k, s)) return false;
   v = StringToInteger(s);
   return true;
  }

We design "DbSetSetting" to write or replace a single key-and-value row, using an insert-or-replace statement so that storing the same key twice overwrites the existing row rather than creating a duplicate. We have it skip quietly when the database is unavailable, the same way the object writers do, so a caller never has to guard the call itself.

We pair it with "DbGetSetting", which selects the value for a given key and returns whether the read found anything. When the key is absent, we report false and leave the output string untouched, which lets a caller apply its own default value cleanly instead of mistaking an empty string for a stored one.

We add "DbSetSettingInt" and "DbGetSettingInt" as thin convenience wrappers for the many session values that are simply numbers. The setter converts the integer to text and writes the same text column, while the getter reads that text back, converts it to an integer, and leaves the output untouched when the key is missing. By funneling integers through the same two string functions, we keep one storage format in the table and let only the wrapper know the value was ever a number.

With the settings store in place alongside the object readers and writers, the persistence layer is complete, and we move on to wiring it into the session lifecycle.


Wiring Persistence into the Session Lifecycle

Saving and Restoring the Interface Session

We move to the shell file and create three functions for the interface session; "SaveUiState" to write the whole session into the settings table, and "LoadUiChrome" together with "LoadUiWindows" to read it back in two phases.

//--- We write the persistence logic in the shell file

//+------------------------------------------------------------------+
//| Persist the full UI session into the settings table              |
//+------------------------------------------------------------------+
void CToolsSidebar::SaveUiState()
  {
   //--- Quietly skip when the database is unavailable
   if(!DbOpen()) return;
   //--- Theme + sidebar snap state and position
   DbSetSettingInt("ui.theme", m_isDarkTheme ? 1 : 0);
   DbSetSettingInt("ui.snap", (int)m_snapState);
   DbSetSettingInt("ui.panelX", m_panelX);
   DbSetSettingInt("ui.panelY", m_panelY);
   //--- Sidebar user-resized height (reveals more category icons) so it survives a timeframe switch
   DbSetSettingInt("ui.snappedSidebarHeight", m_snappedSidebarHeight);
   //--- Properties ribbon: exact last-dragged position so it reopens where the user left it
   DbSetSettingInt("ui.lastRibbonX", m_lastRibbonX);
   DbSetSettingInt("ui.lastRibbonY", m_lastRibbonY);
   DbSetSettingInt("ui.lastRibbonValid", m_lastRibbonValid ? 1 : 0);
   //--- Pinned ribbon: open-state, exact last-dragged position, and the user-resized width
   DbSetSettingInt("ui.pinnedVisible", m_isPinnedVisible ? 1 : 0);
   DbSetSettingInt("ui.lastPinnedX", m_lastPinnedX);
   DbSetSettingInt("ui.lastPinnedY", m_lastPinnedY);
   DbSetSettingInt("ui.lastPinnedValid", m_lastPinnedValid ? 1 : 0);
   DbSetSettingInt("ui.pinnedWidthPx", m_pinnedUserWidthPx);
   //--- Settings window: open-state, owner object, and exact dragged position (-1 sentinel = never moved)
   DbSetSettingInt("ui.settingsVisible", m_isSettingsVisible ? 1 : 0);
   DbSetSettingInt("ui.settingsOwner", m_settingsOwnerObjectId);
   DbSetSettingInt("ui.savedSettingsX", m_savedSettingsX);
   DbSetSettingInt("ui.savedSettingsY", m_savedSettingsY);
   //--- Pinned tool set as a CSV of tool ids
   string csv = "";
   const int np = ArraySize(m_pinnedTools);
   for(int i = 0; i < np; i++) csv += (i > 0 ? "," : "") + IntegerToString((int)m_pinnedTools[i]);
   DbSetSetting("ui.pinnedTools", csv);
   //--- Per-category last-used tool memory as a CSV in category order
   csv = "";
   for(int c = 0; c < CAT_COUNT; c++) csv += (c > 0 ? "," : "") + IntegerToString((int)m_lastUsedToolPerCategory[c]);
   DbSetSetting("ui.lastUsedTools", csv);
   //--- Active tool + this symbol's selection
   DbSetSettingInt("ui.activeTool", (int)m_currentActiveTool);
   DbSetSettingInt("sel." + _Symbol, m_selectedObjectId);
  }

//+------------------------------------------------------------------+
//| Restore persisted UI state members before the first paint        |
//+------------------------------------------------------------------+
void CToolsSidebar::LoadUiChrome()
  {
   //--- Quietly skip when the database is unavailable (defaults stay in effect)
   if(!DbOpen()) return;
   long v = 0;
   //--- Theme + sidebar snap state and position (clamped into the current chart)
   if(DbGetSettingInt("ui.theme", v)) m_isDarkTheme = (v != 0);
   if(DbGetSettingInt("ui.snap", v)) m_snapState = (ENUM_SNAP_STATE)(int)v;
   const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   const int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(DbGetSettingInt("ui.panelX", v)) m_panelX = MathMax(0, MathMin(chartW - m_sidebarWidth, (int)v));
   //--- Float panel position loaded above; snapped re-anchoring happens in Init
   if(DbGetSettingInt("ui.panelY", v)) m_panelY = MathMax(0, MathMin(chartH - 40, (int)v));
   //--- Restore the user-resized sidebar height (0 = use the natural calculated height); CalcSidebarHeight clamps it to the chart
   if(DbGetSettingInt("ui.snappedSidebarHeight", v)) m_snappedSidebarHeight = (int)v;
   //--- Pinned tool set (CSV of tool ids) - loaded HERE (before CalcSidebarHeight) so the pinned category tile's space is reserved on every init, including timeframe switches
   string pcsv = "";
   if(DbGetSetting("ui.pinnedTools", pcsv) && pcsv != "")
     {
      string ptoks[];
      const int pn = StringSplit(pcsv, ',', ptoks);
      ArrayResize(m_pinnedTools, pn);
      for(int i = 0; i < pn; i++) m_pinnedTools[i] = (TOOL_TYPE)(int)StringToInteger(ptoks[i]);
     }
  }

//+------------------------------------------------------------------+
//| Restore window state AFTER canvas creation (which resets it)     |
//+------------------------------------------------------------------+
void CToolsSidebar::LoadUiWindows()
  {
   //--- Quietly skip when the database is unavailable (defaults stay in effect)
   if(!DbOpen()) return;
   long v = 0;
   //--- Properties ribbon: exact last-dragged position (ShowRibbonFor reopens here)
   if(DbGetSettingInt("ui.lastRibbonX", v)) m_lastRibbonX = (int)v;
   if(DbGetSettingInt("ui.lastRibbonY", v)) m_lastRibbonY = (int)v;
   if(DbGetSettingInt("ui.lastRibbonValid", v)) m_lastRibbonValid = (v != 0);
   //--- Pinned ribbon: exact last-dragged position + user-resized width (ShowPinnedRibbon reopens here)
   if(DbGetSettingInt("ui.lastPinnedX", v)) m_lastPinnedX = (int)v;
   if(DbGetSettingInt("ui.lastPinnedY", v)) m_lastPinnedY = (int)v;
   if(DbGetSettingInt("ui.lastPinnedValid", v)) m_lastPinnedValid = (v != 0);
   if(DbGetSettingInt("ui.pinnedWidthPx", v)) m_pinnedUserWidthPx = (int)v;
   //--- Settings window: exact dragged position (ShowSettingsForObject reopens here when re-shown)
   if(DbGetSettingInt("ui.savedSettingsX", v)) m_savedSettingsX = (int)v;
   if(DbGetSettingInt("ui.savedSettingsY", v)) m_savedSettingsY = (int)v;
   //--- Pinned tool set is loaded earlier in LoadUiChrome (before sidebar height is computed)
   string csv = "";
   //--- Per-category last-used tool memory (CSV in category order; runs after the catalog build)
   if(DbGetSetting("ui.lastUsedTools", csv) && csv != "")
     {
      string toks[];
      const int n = StringSplit(csv, ',', toks);
      for(int i = 0; i < n && i < CAT_COUNT; i++)
         m_lastUsedToolPerCategory[i] = (TOOL_TYPE)(int)StringToInteger(toks[i]);
     }
  }

Here, we create the "SaveUiState" function to capture the entire session in a single pass, with each value riding the key-and-value helpers from the previous step. We write the theme and the sidebar's snap state, position, and user-resized height; the properties ribbon's last-dragged position; the pinned ribbon's open state, position, and resized width; and the settings window's open state, owner object, and saved position. We then store the pinned tool set as a comma-separated list of tool IDs, the per-category last-used tools the same way, the active tool, and this symbol's current selection under a symbol-keyed entry.

We split the restore across two functions because creating the canvases resets some of the very state we want to keep, so the order in which we read things back matters. We run "LoadUiChrome" first, before the chart paints anything, to bring back the members that shape the layout.

We restore the theme, the snap state, and the panel position in "LoadUiChrome", clamping the position into the current chart so a saved spot from a larger window never lands off-screen. We also restore the resized sidebar height, and — importantly — we load the pinned tool set here rather than later, because the sidebar height calculation needs to know whether the pinned tile exists so its space is reserved on every initialization, including a timeframe switch.

We run "LoadUiWindows" after the canvases are created, since their creation resets the window positions we are about to set. We restore both ribbons' last-dragged positions, the pinned ribbon's resized width, and the settings window's saved position, and we read the per-category last-used memory once the tool catalog has been built. Each window's show routine then reopens at the position we restored. With the session save and restore defined, we create a function to restore session states and wire these handles in the initialization logic.

Re-Arming the Session on Startup

We create "RestoreSessionState" to re-arm the live session at the end of initialization — reopening the pinned ribbon, the active tool or selection, and the settings window exactly as they were — and we slot it and the loaders into "Init" in a deliberate order.

//+------------------------------------------------------------------+
//| Re-arm the live session at the end of Init                       |
//+------------------------------------------------------------------+
void CToolsSidebar::RestoreSessionState()
  {
   //--- Quietly skip when the database is unavailable
   if(!DbOpen()) return;
   long v = 0;
   //--- Pinned ribbon visibility (its position recreates at the default spot inside ShowPinnedRibbon)
   if(DbGetSettingInt("ui.pinnedVisible", v) && v != 0 && PinnedCount() > 0)
      ShowPinnedRibbon();
   //--- Re-arm the active tool, or restore the per-symbol selection when no tool was armed
   long tool = (long)TOOL_NONE;
   if(DbGetSettingInt("ui.activeTool", tool) && tool != (long)TOOL_NONE)
      ToggleTool((TOOL_TYPE)(int)tool);
   else if(DbGetSettingInt("sel." + _Symbol, v) && v >= 0)
     {
      //--- Reselect only when the object still exists and is visible on this period
      const int idx = FindObjectIndexById((int)v);
      const int tfBit = TimeframeBitForPeriod((ENUM_TIMEFRAMES)Period());
      if(idx >= 0 && m_drawnObjects[idx].visible && (m_drawnObjects[idx].timeframes & tfBit) != 0)
        {
         //--- Selection brings the properties ribbon back (at its persisted position) via the normal chain
         SelectObjectById((int)v);
         RedrawAllObjects();
        }
     }
   //--- If the Settings window was open on a still-valid, still-visible owner, reopen it exactly where it was
   long sVis = 0, sOwner = -1;
   if(DbGetSettingInt("ui.settingsVisible", sVis) && sVis != 0 &&
      DbGetSettingInt("ui.settingsOwner", sOwner) && sOwner >= 0)
     {
      const int sidx = FindObjectIndexById((int)sOwner);
      const int tfBit2 = TimeframeBitForPeriod((ENUM_TIMEFRAMES)Period());
      if(sidx >= 0 && m_drawnObjects[sidx].visible && (m_drawnObjects[sidx].timeframes & tfBit2) != 0)
        {
         //--- Selection first (so close/cancel returns to a selected object), then the window over it
         SelectObjectById((int)sOwner);
         ShowSettingsForObject((int)sOwner);
        }
     }
   //--- Single sidebar repaint AFTER all state is restored so the pinned tile + active tile highlight without needing a hover
   DrawSidebar(m_currentActiveTool);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Initialize all canvases and register chart event hooks           |
//+------------------------------------------------------------------+
bool CToolsSidebar::Init(long chartId)
  {
   //--- Existing logic
   
   //--- Restore ribbon/pinned/settings positions + pinned set NOW - after the canvas-create calls that reset them
   LoadUiWindows();

   //--- Initial render - flyout hidden, sidebar drawn at the configured position
   LoadPersistedObjects();
   //--- Re-arm the session exactly as it was: pinned ribbon, active tool, selection (ribbon follows)
   RestoreSessionState();
   return true;
  }

We have "RestoreSessionState" reopen the pinned ribbon first, but only when it was showing last time and at least one tool is still pinned, letting its show routine place it at the position we restored earlier. We then re-arm whatever we had active: when a tool was armed, we toggle it back on, and otherwise we fall back to restoring this symbol's last selection.

We guard that reselection carefully, and this is where persistence meets the timeframe-visibility feature. We restore the selection only when the object still exists, is visible, and has the current chart period set in its timeframe mask, because an object that is hidden on this timeframe should not quietly come back selected. We apply the same three-part guard to the settings window — when it was open on an owner that still exists and still shows in this period, we reselect that owner first so a later cancel returns to a selected object, and then we reopen the window over it.

We close the function with a single sidebar repaint after everything else is restored, so the pinned tile and the active tool tile light up immediately rather than waiting for us to move the cursor over them.

We wire these into "Init" in an order that respects what resets what. We call "LoadUiWindows" after the canvas-creation calls that would otherwise wipe the window positions, then "LoadPersistedObjects" to bring this symbol's drawings back, then "RestoreSessionState" to re-arm the session on top of the restored objects. With the lifecycle wired on the startup side, we can add the visualization tab.


Adding the Per-Timeframe Visibility Tab

The Timeframe Masks and the Period-to-Bit Helper

We open the visibility system by wiring the timeframe masks and the period-to-bit helper in the tools file, and by adding the new row type to the properties file.

//--- We wire the timeframe mask and group masks in the tools file

//--- Composite period-bit masks for the Minutes / Hours visibility groups
#define TOOLS_PALETTE_TF_MINUTES_MASK (OBJ_PERIOD_M1|OBJ_PERIOD_M2|OBJ_PERIOD_M3|OBJ_PERIOD_M4|OBJ_PERIOD_M5|OBJ_PERIOD_M6|OBJ_PERIOD_M10|OBJ_PERIOD_M12|OBJ_PERIOD_M15|OBJ_PERIOD_M20|OBJ_PERIOD_M30)
#define TOOLS_PALETTE_TF_HOURS_MASK   (OBJ_PERIOD_H1|OBJ_PERIOD_H2|OBJ_PERIOD_H3|OBJ_PERIOD_H4|OBJ_PERIOD_H6|OBJ_PERIOD_H8|OBJ_PERIOD_H12)

//+------------------------------------------------------------------+
//| Map a chart period to its OBJ_PERIOD_* visibility-mask bit       |
//+------------------------------------------------------------------+
int TimeframeBitForPeriod(ENUM_TIMEFRAMES period)
  {
   //--- One case per standard MT5 chart period
   switch(period)
     {
      case PERIOD_M1:  return OBJ_PERIOD_M1;
      case PERIOD_M2:  return OBJ_PERIOD_M2;
      case PERIOD_M3:  return OBJ_PERIOD_M3;
      case PERIOD_M4:  return OBJ_PERIOD_M4;
      case PERIOD_M5:  return OBJ_PERIOD_M5;
      case PERIOD_M6:  return OBJ_PERIOD_M6;
      case PERIOD_M10: return OBJ_PERIOD_M10;
      case PERIOD_M12: return OBJ_PERIOD_M12;
      case PERIOD_M15: return OBJ_PERIOD_M15;
      case PERIOD_M20: return OBJ_PERIOD_M20;
      case PERIOD_M30: return OBJ_PERIOD_M30;
      case PERIOD_H1:  return OBJ_PERIOD_H1;
      case PERIOD_H2:  return OBJ_PERIOD_H2;
      case PERIOD_H3:  return OBJ_PERIOD_H3;
      case PERIOD_H4:  return OBJ_PERIOD_H4;
      case PERIOD_H6:  return OBJ_PERIOD_H6;
      case PERIOD_H8:  return OBJ_PERIOD_H8;
      case PERIOD_H12: return OBJ_PERIOD_H12;
      case PERIOD_D1:  return OBJ_PERIOD_D1;
      case PERIOD_W1:  return OBJ_PERIOD_W1;
      case PERIOD_MN1: return OBJ_PERIOD_MN1;
     }
   //--- Defensive default: unknown periods behave as visible-if-shown-anywhere
   return OBJ_ALL_PERIODS;
  }

//--- We define the rows type in properties file alongside the other Enum types

//+------------------------------------------------------------------+
//| Widget-type enum - drives which UI widget renders each property  |
//+------------------------------------------------------------------+
enum PROP_TYPE
  {
   //--- Existing properties

   PROP_LEVEL_LIST,   // meta descriptor that expands at runtime into N compact rows + "Add level" row
   PROP_TF_ROW,       // synthesized timeframe-visibility row (master/group/leaf checkbox)
   PROP_ACTION        // sentinel for action-button ribbon icons (Settings, Remove)
  };

//--- Settings-window tab group names
#define PROP_GROUP_STYLE        "Style"
#define PROP_GROUP_TEXT         "Text"
#define PROP_GROUP_COORDS       "Coordinates"
#define PROP_GROUP_VISIBILITY   "Visibility"

First, we define two composite masks, one covering the minute periods and one covering the hour periods, each formed as the bitwise combination of its member period flags. MetaTrader 5 already represents an object's per-period visibility as a set of these period bits, and our object's timeframe field holds exactly such a mask, so grouping the minute and hour bits this way lets a single group checkbox flip every period it covers in one move. See this grouping in the terminal.

MT5 TIMEFRAME GROUPS

Then, we create "TimeframeBitForPeriod" to translate the chart's current period into the single visibility bit that represents it, with one case for each standard period the platform offers. We give it a defensive fallback to all periods for anything it does not recognize, so an unexpected period can never hide an object by accident; the safe behavior is to keep the object showing rather than make it vanish.

We add "PROP_TF_ROW" to the widget-type enumeration in the properties file, sitting alongside the existing property types. This is the type the settings window will use for the synthesized master, group, and leaf rows of the visibility editor, so the renderer and the click handler can recognize a timeframe row and treat it differently from an ordinary property chip. The four tab-group name constants beside it sort properties into their tabs, and the visibility group already existed from the settings window we built earlier; what we are doing now is giving that tab real rows to show. With the mask vocabulary and the row type defined, we move on to building the rows themselves. We do this in the settings file that houses the window.

Building the Visibility Rows

We build the Visibility tab's rows entirely at runtime, and rather than invent a new mechanism we lean on the same synthesized-row approach the level lists already use, carried by the two expansion-state fields that remember which groups are open.

//--- We build the synthesized rows in the settings file

//+------------------------------------------------------------------+
//| CSettingsWindow - Settings dialog inheriting from CRibbon        |
//+------------------------------------------------------------------+
class CSettingsWindow : public CRibbon
  {
protected:
   //--- Existing members
   
   //--- Visibility-tab expansion state (Minutes / Hours leaf lists)
   bool            m_tfMinutesExpanded;
   bool            m_tfHoursExpanded;

  };

//+------------------------------------------------------------------+
//| Append one synthesized timeframe-visibility row descriptor       |
//+------------------------------------------------------------------+
void CSettingsWindow::AppendTimeframeRow(string rowId, string rowLabel,
                                          int rowMask, string role)
  {
   //--- Fully initialize the synth descriptor (locals are not zeroed)
   SToolProperty row;
   row.id              = rowId;
   row.label           = rowLabel;
   row.type            = PROP_TF_ROW;
   row.group           = PROP_GROUP_VISIBILITY;
   row.showInRibbon    = false;
   row.showInSettings  = true;
   row.tooltip         = "";
   row.defaultColor    = clrBlack;
   row.defaultInt      = 0;
   row.defaultDouble   = 0.0;
   row.defaultString   = "";
   row.defaultBool     = false;
   row.minValue        = 0.0;
   row.maxValue        = 0.0;
   row.stepValue       = 0.0;
   row.decimals        = 0;
   row.subVisibleId    = "";
   row.subValueId      = "";
   row.subColorId      = "";
   row.subWidthId      = "";
   row.subStyleId      = "";
   //--- levelIdx carries the row's OBJ_PERIOD_* bitmask; prefix carries its role
   row.levelIdx        = rowMask;
   row.isAddLevelRow   = false;
   row.levelListPrefix = role;
   //--- Append to the synth array + reference it via negative encoding
   const int sySz = ArraySize(m_synthRowDescriptors);
   ArrayResize(m_synthRowDescriptors, sySz + 1);
   m_synthRowDescriptors[sySz] = row;
   const int rsz = ArraySize(m_activeTabRowIdxs);
   ArrayResize(m_activeTabRowIdxs, rsz + 1);
   m_activeTabRowIdxs[rsz] = -(sySz + 1);
  }

//+------------------------------------------------------------------+
//| Build the Visibility tab's synthesized timeframe row list        |
//+------------------------------------------------------------------+
void CSettingsWindow::BuildTimeframeVisibilityRows()
  {
   //--- Master all-or-none toggle row
   AppendTimeframeRow("tf:all", "All timeframes", OBJ_ALL_PERIODS, "master");
   //--- Minutes group row + its leaf rows when expanded
   AppendTimeframeRow("tf:min", "Minutes", TOOLS_PALETTE_TF_MINUTES_MASK, "group");
   if(m_tfMinutesExpanded)
     {
      AppendTimeframeRow("tf:M1",  "M1",  OBJ_PERIOD_M1,  "leaf");
      AppendTimeframeRow("tf:M2",  "M2",  OBJ_PERIOD_M2,  "leaf");
      AppendTimeframeRow("tf:M3",  "M3",  OBJ_PERIOD_M3,  "leaf");
      AppendTimeframeRow("tf:M4",  "M4",  OBJ_PERIOD_M4,  "leaf");
      AppendTimeframeRow("tf:M5",  "M5",  OBJ_PERIOD_M5,  "leaf");
      AppendTimeframeRow("tf:M6",  "M6",  OBJ_PERIOD_M6,  "leaf");
      AppendTimeframeRow("tf:M10", "M10", OBJ_PERIOD_M10, "leaf");
      AppendTimeframeRow("tf:M12", "M12", OBJ_PERIOD_M12, "leaf");
      AppendTimeframeRow("tf:M15", "M15", OBJ_PERIOD_M15, "leaf");
      AppendTimeframeRow("tf:M20", "M20", OBJ_PERIOD_M20, "leaf");
      AppendTimeframeRow("tf:M30", "M30", OBJ_PERIOD_M30, "leaf");
     }
   //--- Hours group row + its leaf rows when expanded
   AppendTimeframeRow("tf:hr", "Hours", TOOLS_PALETTE_TF_HOURS_MASK, "group");
   if(m_tfHoursExpanded)
     {
      AppendTimeframeRow("tf:H1",  "H1",  OBJ_PERIOD_H1,  "leaf");
      AppendTimeframeRow("tf:H2",  "H2",  OBJ_PERIOD_H2,  "leaf");
      AppendTimeframeRow("tf:H3",  "H3",  OBJ_PERIOD_H3,  "leaf");
      AppendTimeframeRow("tf:H4",  "H4",  OBJ_PERIOD_H4,  "leaf");
      AppendTimeframeRow("tf:H6",  "H6",  OBJ_PERIOD_H6,  "leaf");
      AppendTimeframeRow("tf:H8",  "H8",  OBJ_PERIOD_H8,  "leaf");
      AppendTimeframeRow("tf:H12", "H12", OBJ_PERIOD_H12, "leaf");
     }
   //--- Single-period rows (Daily / Weekly / Monthly)
   AppendTimeframeRow("tf:D1",  "Daily",   OBJ_PERIOD_D1,  "single");
   AppendTimeframeRow("tf:W1",  "Weekly",  OBJ_PERIOD_W1,  "single");
   AppendTimeframeRow("tf:MN1", "Monthly", OBJ_PERIOD_MN1, "single");
  }

//--- Build the rows when the visibility tab is active

//+------------------------------------------------------------------+
//| Build the row-index list for the currently active tab            |
//+------------------------------------------------------------------+
void CSettingsWindow::RefreshActiveTabRows()
  {

   //--- Existing logic
   
   //--- Visibility tab is fully synthesized - no registered descriptors back it
   if(activeGroup == PROP_GROUP_VISIBILITY)
     {
      BuildTimeframeVisibilityRows();
      return;
     }

  }

Here, we create "AppendTimeframeRow" to build one timeframe-row descriptor and push it onto the active tab. We fully initialize the descriptor by hand, since a local structure is not zeroed for us, and we mark it as a visibility-group row that shows only in the settings window. Rather than add new fields, we reuse two that the level-list rows already carry: we store the row's period bitmask in the level-index field and the row's role — master, group, leaf, or single — in the prefix field. We then append it to the synthesized-descriptor array and reference it from the row list through the same negative encoding, so the existing row resolver hands it back without needing a special case.

We drive the whole list from "BuildTimeframeVisibilityRows". We start with a master "All timeframes" toggle, then add the Minutes group, followed by its per-period leaves when that group is expanded, the Hours group and its leaves the same way, and finally the three single rows for Daily, Weekly, and Monthly. The two expansion flags decide whether each group reveals its leaves, which is what gives the tab its collapsible feel; we open Minutes to reach M5 without the full list of every period crowding the panel.

We hook the builder into "RefreshActiveTabRows" so that when the active tab is the visibility group, we call "BuildTimeframeVisibilityRows" and return immediately. We treat this tab differently from the others because it is fully synthesized; no registered descriptors back it, unlike Style or Text, so the row list comes entirely from the builder rather than from filtering the tool's property list. With the rows built, we move on to rendering a timeframe row.

Placing, Rendering, and Toggling a Timeframe Row

We place the checkbox, route the row through the renderer, and wire the toggle; and the role-based mask math is the heart of it.

//+------------------------------------------------------------------+
//| Checkbox rect for a timeframe-visibility row (render + hit zone) |
//+------------------------------------------------------------------+
void CSettingsWindow::GetTfCheckboxRect(int rowSlot, const SToolProperty &prop,
                                         int &outL, int &outT, int &outR, int &outB)
  {
   //--- Row bounds in window-local space
   int rL, rT, rR, rB;
   GetRowRect(rowSlot, rL, rT, rR, rB);
   //--- Groups reserve a chevron slot; leaves indent under their group
   const int chkSize = 22;
   int x = rL + 6;
   if(prop.levelListPrefix == "group") x += 18;
   if(prop.levelListPrefix == "leaf")  x += 22;
   outL = x;
   outT = rT + (m_settingsRowH - chkSize) / 2;
   outR = outL + chkSize;
   outB = outT + chkSize;
  }

//+------------------------------------------------------------------+
//| Draw all body rows for the active tab (dispatch to Text/Coords)  |
//+------------------------------------------------------------------+
void CSettingsWindow::DrawSettingsRows()
  {
   //--- Existing logic
   
   for(int s = 0; s < n; s++)
     {
      //--- Compact + timeframe rows are self-contained; the rest get a left label
      if(prop.type != PROP_COMPACT_ROW && prop.type != PROP_TF_ROW)
        {
         const int rLH = m_canvasSettings.TextHeight(prop.label);
         const int labelY = boxT + rT + (m_settingsRowH - rLH) / 2;
         m_canvasSettings.TextOut(boxL + rL, labelY,
                                    prop.label,
                                    ColorToARGB(m_themeColors.flyoutTextColor, 220));
        }
      //--- Delegate chip rendering by type
      RenderRowChip(s, prop);
     }
  }

//--- We wire the interaction to toggle the row same as others in the settings interact file

//+------------------------------------------------------------------+
//| SettingsMouseDown - click router for the Settings window         |
//+------------------------------------------------------------------+
bool CSettingsWindow::SettingsMouseDown(int mouseX, int mouseY)
  {
      //--- Existing logic
      
      //--- PROP_TF_ROW -> checkbox toggles period bits; group rows expand elsewhere
      if(prop.type == PROP_TF_ROW)
        {
         //--- Current mask + this row's role bits
         int tfMask = OBJ_ALL_PERIODS;
         GetObjectProperty(m_settingsOwnerObjectId, "timeframes", tfMask);
         const int rowBits = prop.levelIdx;
         //--- Checkbox click -> recompute the mask by role and apply it live
         int ckL, ckT, ckR, ckB;
         GetTfCheckboxRect(rowSlot, prop, ckL, ckT, ckR, ckB);
         if(lx >= ckL && lx <= ckR && ly >= ckT && ly <= ckB)
           {
            int newMask = tfMask;
            if(prop.levelListPrefix == "master")
               newMask = (tfMask == OBJ_ALL_PERIODS) ? OBJ_NO_PERIODS : OBJ_ALL_PERIODS;
            else if(prop.levelListPrefix == "group")
               newMask = ((tfMask & rowBits) == rowBits) ? (tfMask & ~rowBits) : (tfMask | rowBits);
            else
               newMask = tfMask ^ rowBits;
            SetObjectProperty(m_settingsOwnerObjectId, "timeframes", newMask, false);
            RedrawSettings(); ChartRedraw();
            return true;
           }
         //--- Any other click on a group row toggles its expansion state
         if(prop.levelListPrefix == "group")
           {
            if(prop.id == "tf:min") m_tfMinutesExpanded = !m_tfMinutesExpanded;
            else                    m_tfHoursExpanded   = !m_tfHoursExpanded;
            RefreshActiveTabRows();
            RecalcSettingsSize();
            ApplySettingsPosition();
            RedrawSettings(); ChartRedraw();
            return true;
           }
         //--- Timeframe row is exclusive - swallow unhandled clicks
         return true;
        }

   //--- Any other click inside the window is swallowed (so events don't leak to the chart)
   return true;
  }

Here, we create "GetTfCheckboxRect" to position the checkbox inside the row, indenting it by the row's role. A group row reserves a slot for its chevron, and a leaf row indents further beneath its group, so the indentation alone conveys the master-group-leaf hierarchy without any connecting lines. The same rectangle serves both the render and the hit zone, so the checkbox we draw and the checkbox we click are guaranteed to line up.

We let the row loop in "DrawSettingsRows" treat a timeframe row the same way it treats a compact row; both are self-contained, so we skip the left-hand label that ordinary properties receive and hand the row straight to "RenderRowChip", which draws the chevron, checkbox, and label itself.

We wire the toggle in the "PROP_TF_ROW" branch of "SettingsMouseDown", where the mask math lives. We read the object's current timeframe mask, take this row's stored period bits, and on a checkbox click, we recompute the mask according to the row's role. The master flips the whole mask between all periods and none. A group sets all of its member bits when any are missing and clears them when all are present, which makes the group checkbox an all-or-nothing toggle for the periods beneath it. A leaf or single row flips only its own bit. We then write the new mask straight back to the object, so the change previews live on the chart behind the window.

We treat any other click on a group row as an expand-or-collapse; we flip that group's expansion flag, rebuild the row list, resize the window to the new height, and repaint. And because a timeframe row owns its full width, we swallow any unhandled click on it so the event never leaks through to the chart. Upon compilation, we get the following outcome.

EXPANDED HOURS ROW

With the rows built, rendered, and toggled, the only piece left is the transition from an Expert Advisor to an Indicator, to be used like a utility tool in a workspace. For this, we created an indicator file and simply registered it as an indicator and left the OnCalculate event handler empty since it is not needed.

The Indicator Entry Point

We cap the implementation with the entry point itself, now built as an indicator rather than an Expert Advisor, so the toolkit occupies the chart as a utility that sits beside anything else running there.

//+------------------------------------------------------------------+
//|                                        Tools Palette Part 11.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict
#property indicator_chart_window
//--- Canvas/object drawing, not plot buffers
#property indicator_buffers 0
#property indicator_plots 0

//--- Pull in the sidebar shell header that defines the CToolsSidebar class
#include "ToolsPalette_Shell.mqh"

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CToolsSidebar g_sidebar; // Top-level sidebar that owns every canvas and routes chart events

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Register an explicit short name so the close button can delete this indicator by name
   IndicatorSetString(INDICATOR_SHORTNAME, "Tools Palette Part 11");
   //--- Spin up sidebar canvases, register event hooks, and paint the panel
   if(!g_sidebar.Init(ChartID()))
     {
      //--- Report the initialization failure to the experts journal
      Print("Tools Palette Part 11: Failed to initialize. Check journal for details.");
      //--- Abort startup by returning a failure code to the terminal
      return(INIT_FAILED);
     }
   //--- Force an immediate chart redraw so the sidebar appears on load
   ChartRedraw();
   //--- Signal a successful initialization to the terminal
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Tear down every sidebar canvas and free all associated resources
   g_sidebar.Destroy();
   //--- Refresh the chart so no leftover sidebar artifacts remain visible
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   //--- Drawing utility - no per-bar calculation needed
   return(rates_total);
  }

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Forward every chart event straight to the sidebar dispatcher
   g_sidebar.OnEvent(id, lparam, dparam, sparam);
  }

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Pass each timer tick to the sidebar so the label edit cursor can blink
   g_sidebar.OnTimer();
  }
//+------------------------------------------------------------------+

First, we declare it a chart-window indicator with zero buffers and zero plots, because it draws entirely through canvases and chart objects rather than plotting a data series. This is the change that lets it share a chart with an Expert Advisor and other indicators at the same time, instead of claiming the single Expert Advisor slot the earlier parts occupied; a meaningful gain now that the chart is meant to be a durable, shared workspace. We set indicator_plots to 0 since none are needed; otherwise, the compiler issues a warning as below.

INDICATOR PLOT REQUIREMENT WARNING

Then, we register an explicit short name in OnInit so the sidebar's close button can later remove this indicator by name, then we spin up the sidebar through "Init", which now also opens the database, loads this symbol's persisted objects, and re-arms the saved session. We force an immediate redraw so the panel appears the moment the tool loads rather than on the first chart event.

We leave OnCalculate empty of any per-bar work and simply return the bar count, since a drawing utility has nothing to compute each tick; this is what stands in for the per-tick handler the Expert Advisor version used. We forward every chart event straight to the sidebar dispatcher, pass each timer tick through so the label-edit caret can blink, and tear every canvas down in OnDeinit. With the program running as an indicator and the persistence and visibility systems both in place, we move on to seeing the whole thing work on the chart.


Visualization

We compile the indicator, attach it to the chart, draw a few objects, set some to specific timeframes, then reload the terminal to confirm everything returns exactly as we left it.

During testing, the drawings, their styles, the theme, the panel and ribbon positions, and the pinned set all came back after a restart, with each object reloaded in its original creation order. Opening an object's Visibility tab and unchecking periods made it appear and vanish as we switched the chart timeframe; a group checkbox toggled all its members at once, the master flipped everything, and the chevrons expanded the Minutes and Hours lists. Setting an object to show only during certain periods survived the reload, and converting the program to an indicator let it run on the same chart as an Expert Advisor without conflict.


Conclusion

In conclusion, we have given the canvas drawing layer a memory and a sense of context. We added an SQLite persistence layer that serializes every drawn object into a versioned payload, stores it per symbol alongside a key-and-value settings table, loads it back on startup, and writes it only when a dirty flag says something changed. We wired that layer into the session lifecycle so the theme, the panel, and both ribbons' positions, the pinned set, the active tool, and the selection all survive a reload, restoring them in the right order around the canvas creation that would otherwise reset them. We then built a per-timeframe visibility tab; a synthesized tree of master, group, and leaf rows that edit each object's period mask; and a render-time filter that shows an object only on the periods we chose. We also converted the program from an Expert Advisor into an indicator, so the toolkit now runs on the chart as a utility beside anything else. After reading this article, you will be able to:

  • Build an SQLite persistence layer with versioned object serialization, per-symbol storage, and a key-and-value settings table, loading on startup and saving only when a dirty flag marks the state stale.
  • Persist and restore a full interface session; theme, panel, and window positions, pinned set, active tool, and selection; sequencing the restore correctly around the canvas creation that resets it.
  • Add a per-timeframe visibility system with a synthesized tab of master, group, and leaf rows that edit a period mask, and filter objects at render time so each shows only on the timeframes you chose.

With persistence and per-timeframe visibility in place and the toolkit running as an indicator, the workspace now closes and reopens exactly as we left it, with every drawing showing only where it belongs. We now have a concrete drawing utility tool that can be optimized and expanded to house more features.


Attachments

S/N
Name
Type
Description
1 Tools Palette Part 11.mq5 Indicator Main entry point, now a chart-window indicator, that owns the global sidebar instance and forwards initialization, deinitialization, calculation, chart event, and timer callbacks to it.
2 ToolsPalette_Annotations.mqh Include File Annotation tools covering Text, Arrow, Arrow Marker, Arrow Up, Arrow Down, Note, Price Note, Callout, and Comment, with bold-aware text measurement threaded through the shared text helpers.
3 ToolsPalette_Channels.mqh Include File Channels, pitchforks, and Gann tools library, now delegating dashed strokes to the shared width-proportional dashed renderer.
4 ToolsPalette_Crosshair.mqh Include File Crosshair manager that owns the reticle, magnifier, cross-line, and measurement canvases plus their axis labels.
5 ToolsPalette_Database.mqh Include File SQLite persistence layer covering versioned object serialization and deserialization, the open and close lifecycle, the per-symbol object store, dirty-gated saving, loading on startup, and the key-and-value settings helpers.
6 ToolsPalette_Engine_Edit.mqh Include File Drawing engine method bodies for the in-place label editing subsystem covering edit lifecycle, caret operations, selection model, and chart keyboard override.
7 ToolsPalette_Engine_Interact.mqh Include File Drawing engine method bodies for pointer-mode interaction covering hit testing dispatch, handle reshape logic, drag move and release, and selection management.
8 ToolsPalette_Engine_Properties.mqh Include File Drawing engine method bodies for the property get and set API, snapshot and restore, per-level add and remove, and the timeframe mask property, now marking the state dirty on every committed change.
9 ToolsPalette_Engine_Render.mqh Include File Drawing engine method bodies for the render pipeline, now filtering objects by their per-timeframe mask so each shows only on the periods it was set for.
10 ToolsPalette_Fibonacci.mqh Include File Fibonacci tools library covering retracement, expansion, channel, time zone, speed resistance fan, and speed resistance arcs.
11 ToolsPalette_Lines.mqh Include File Base line tools class providing every line drawing routine plus the shared handle renderer and tool icon dispatch.
12 ToolsPalette_Primitives.mqh Include File Foundational pixel primitives, now with width-proportional dashed-line rendering, covering alpha-compositing pixel set, anti-aliased lines, supersampled fills, the theme manager, and the input parameter declarations.
13 ToolsPalette_Properties.mqh Include File Property descriptor system, now carrying the timeframe-row type, defining the property type enum, the descriptor struct, the per-tool registration functions, and the build dispatcher.
14 ToolsPalette_PropertyWidgets.mqh Include File Property-editing widget renderers covering the color picker, the line width and style popovers, the compact rows, and the ribbon icons.
15 ToolsPalette_RibbonPinned.mqh Include File Pinned-tools ribbon: a floating, draggable, user-resizable, horizontally scrollable bar of pinned tool icons with a persistent offscreen clipping canvas.
16 ToolsPalette_RibbonProperties.mqh Include File Per-object property-editing ribbon that pops up beside the selection and opens the color, width, and style popovers wired to the engine.
17 ToolsPalette_Settings.mqh Include File Tabbed settings window layout and rendering, now with the synthesized per-timeframe Visibility tab of master, group, and leaf rows.
18 ToolsPalette_Settings_Interact.mqh Include File Settings window interaction covering mouse routing, inline coordinate and float editing, the sub-popover dispatch, and the timeframe-row toggle and expansion handling.
19 ToolsPalette_Shapes.mqh Include File Shape tools library covering Rectangle, Triangle, Rotated Rectangle, Rotated Ellipse, Path, Circle, Arc, and Curve.
20 ToolsPalette_Shell.mqh Include File Sidebar shell that routes chart events and drives the timer, now saving and restoring the full interface session and sequencing the object load and session re-arm during initialization.
21 ToolsPalette_Sidebar.mqh Include File Sidebar renderer and flyout panel carrying the per-row pushpin affordance and the Pinned category tile, with persistent scratch canvases on the hover and redraw paths.
22 ToolsPalette_Tools.mqh Include File Tool registry plus the drawing engine class, now holding the timeframe mask field, the period-to-bit helper, the database handle and dirty flag, and the pinned-tools set and its management API.
23 Tools Palette Part 11.zip Archive A ready-to-extract archive containing all 22 project files in a single folder. Unzip it into your MetaTrader 5 terminal data folder; the files will be placed under MQL5/Experts/ with every file in its correct location, ready to compile.
Attached files |
Analyzing the Hourly Movement of Trading Symbols and Their Spreads in MetaTrader 5 Analyzing the Hourly Movement of Trading Symbols and Their Spreads in MetaTrader 5
The ProSpread seasonality index indicator with a Moving Average is a technical analysis tool that identifies seasonal patterns in price movements, analyzes price behavior during specific trading hours and is able to work with either a single instrument or a spread between two assets. It also visualizes the statistical probability of directional movements.
Measuring What Matters (Part 2): Building the Covariance Matrix: Eigenvalue Decomposition and Risk Factor Analysis in MQL5 Measuring What Matters (Part 2): Building the Covariance Matrix: Eigenvalue Decomposition and Risk Factor Analysis in MQL5
In Part 2, we introduce a reusable CCovarianceMatrix class that computes and stores a covariance matrix from raw return series using MQL5's native Cov() method. We verify symmetry, print a labeled matrix grid, and call Eig() to obtain eigenvalues and eigenvectors. Readers see how symbols co-move and which factors drive variance, enabling clearer portfolio diagnostics and reuse in scripts or EAs.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Building a Traditional Daily Pivot Point Indicator in MQL5 Building a Traditional Daily Pivot Point Indicator in MQL5
This article develops a rule-based daily pivot point indicator in MQL5 that uses the previous trading day's high, low, and close values to generate pivot, support, and resistance levels. It details historical data retrieval, pivot computation, chart object management, configurable label rendering, and automatic level updates as new trading days begin. The completed indicator displays multiple historical pivot sessions on the main chart for technical analysis on daily and lower timeframes.