MQL5 Trading Tools (Part 40): Adding SQLite Persistence and Per-Timeframe Visibility to the Canvas Drawing Layer
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:
- From a Session Toolkit to a Persistent Workspace
- Building the SQLite Persistence Layer
- Wiring Persistence into the Session Lifecycle
- Adding the Per-Timeframe Visibility Tab
- Visualization
- 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.

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.

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.

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.

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.

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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Analyzing the Hourly Movement of Trading Symbols and Their Spreads in MetaTrader 5
Measuring What Matters (Part 2): Building the Covariance Matrix: Eigenvalue Decomposition and Risk Factor Analysis in MQL5
Features of Experts Advisors
Building a Traditional Daily Pivot Point Indicator in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use