---
title: "Display Construction — Canvas, Dashboard, Dynamics, Symbols & CodeBehind"
tags: [display, canvas, dashboard, symbol, navigation, hmi, ui, layout, dynamics, codebehind]
description: "Build displays from scratch: Canvas and Dashboard layouts, zone-based Canvas positioning, symbol placement with SymbolLabel binding, visual dynamics (color, visibility, rotation), ActionDynamic click handling, header navigation for multi-page solutions, CodeBehind for display logic, and asset-driven navigation"
version: "...
4.0"
author: "Tatsoft"
---
Build complete FrameworX displays: choose between Canvas and Dashboard layouts, place controls and symbols with proper tag binding, wire visual dynamics (color, visibility, rotation based on tag values), configure ActionDynamic for navigation, write CodeBehind for display place page navigation buttons in the persistent Header display, write CodeBehind for display lifecycle events, and set up asset-driven navigation patterns.
...
list_dynamics() with no parameter is the door to the entire dynamics system — it lists every available dynamic type organized by category with minimal examples.
Fetch the display schema and decide Canvas vs Dashboard:
get_table_schema('DisplaysList')
list_elements('Canvas') or list_elements('Dashboard')
Canvas — absolute positioning (Left/Top/Width/Height). Use for:
Dashboard — responsive grid (Columns/Rows/Cells). Use for:
PanelType property is REQUIRED — omitting it silently defaults to Canvas, which causes confusing results if you intended a Dashboard.
Canvas uses absolute positioning with Elements array. Each element needs Left, Top, Width, Height.
...
The correct format for write_objects on DisplaysList uses top-level properties directly. The identifier field is Name.
Correct — flat format, everything at top level:
{
"Name": "MyDisplay",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Size": "1366 x 720",
"Elements": [
{
"Type": "Rectangle",
"Left": 100, "Top": 100, "Width": 200, "Height": 150
}
]
}
...
Fetch the display schema and decide Canvas vs Dashboard:
get_table_schema('DisplaysListlist_elements('TextBlock,CircularGauge,Rectangle')
list_elements('ShapesCanvas') -- all drawing primitives
or list_elements('InteractionDashboard') -- buttons, inputs, toggles
list_elements('Charts') -- TrendChart, BarChart, PieChart
list_elements('Gauges') -- CircularGauge, LinearGauge, BulletGauge
Batch lookups with comma-separated names:
list_elements('TrendChart,CircularGauge,TextBlock')
A concrete Canvas display with shapes, text, a symbol, and a dynamic:
{
"table_type": "DisplaysList",
"data": [{
"ObjectName": "ProcessView",
"PanelType": "Canvas",
"Size": "1366 x 728",
"OnResize": "StretchFill",
"Elements": [
{
"Type": "TextBlock",
"Text": "Process Overview",
"Left": 50, "Top": 20, "Width": 300, "Height": 30,
"FontSize": 22
},
{
"Type": "Rectangle",
"Left": 50, "Top": 80, "Width": 200, "Height": 150,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Tank1/ValveOpen",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF4CAF50" }
]
}
}
]
},
{
"Type": "TextBlock",
"LinkedValue": "{@Tag.Plant/Tank1/Level.Value}",
"Left": 100, "Top": 160, "Width": 100, "Height": 25
},
{
"Type": "Symbol",
"SymbolName": "Wizard/PUMP",
"Left": 300, "Top": 120, "Width": 80, "Height": 80,
"SymbolLabels": [
{ "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression" }
]
},
{
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Pressure.Value",
"Left": 450, "Top": 80, "Width": 180, "Height": 160,
"Minimum": 0, "Maximum": 100
}
]
}]
}
Key Canvas rules:
Size: "1366 x 728" for standard content areaOnResize: "StretchFill" scales proportionally to fill the layout regionDashboard uses a grid with DashboardDisplay (columns/rows definition) and Cells array.
list_elements('Dashboard')
Canvas — absolute positioning (Left/Top/Width/Height). Use for:
Dashboard — responsive grid (Columns/Rows/Cells). Use for:
PanelType property is REQUIRED — omitting it silently defaults to Canvas, which causes confusing results if you intended a Dashboard.
Decision guide: If the display is primarily data monitoring (gauges, trends, grids), prefer Dashboard — it handles spacing automatically. Use Canvas only when process flow diagrams, P&ID schematics, or custom shapes/lines are needed. For mixed needs: Dashboard for the main monitoring page, Canvas for process overview pages.
...
AI agents consistently produce poorly laid out Canvas displays when placing elements with arbitrary coordinates. Always use the zone-based approach described here.
NEVER start by placing individual elements. Instead:
Before writing ANY element, mentally divide the 1366×720 canvas into rectangular zones. Each zone represents a process area, equipment group, or information panel.
Standard zone layout for a process overview (1366×720):
PAGE TITLE (x:0, y:0, w:1366, h:50) ACTION BTNs | ||||||
|---|---|---|---|---|---|---|
| Zone 2 | Zone 2 | Zone 2 | Zone 2 | Zone 2 | Zone 2 |
BOTTOM PANEL: Trend, Alarms, KPIs, or ML Status (h:300+) | ||||||
Note: The title bar is for the page title and optional in-page action buttons (start/stop, acknowledge). Page-to-page navigation buttons go in the Header display, not here (see Step 10b).
Zone sizing rules:
For N process stages on a 1366×720 canvas:
Available width = 1366 - 20 (left/right margin 10px each) = 1346
Available height = 720 - 50 (title) - 10 (top margin) = 760
Zone width = (Available width - (N-1) × 15) / N
Zone height = 320 (for process row)
Zone startY = 60 (below title)
Zone[i].Left = 20 + i × (zoneWidth + 15)
Zone[i].Top = 60
Zone[i].Width = zoneWidth
Zone[i].Height = 320
Bottom panel:
Left = 20, Top = 400, Width = 1326, Height = 658 - 320 - 20 = 318
Example: 6 process stages
Zone width = (1326 - 5×15) / 6 = 208px each
Zone 0: Left=20, Top=60, W=208, H=320
Zone 1: Left=243, Top=60, W=208, H=320
Zone 2: Left=466, Top=60, W=208, H=320
Zone 3: Left=689, Top=60, W=208, H=320
Zone 4: Left=912, Top=60, W=208, H=320
Zone 5: Left=1135, Top=60, W=208, H=320
Bottom: Left=20, Top=400, W=1326, H=308
Every process zone MUST have a background Rectangle for visual grouping. Use themed colors — never hardcode colors for zone backgrounds.
Standard zone background — use PanelBackground theme:
{
"Type": "Rectangle",
"Left": 20, "Top": 60, "Width": 208, "Height": 320,
"FillTheme": "PanelBackground",
"Stroke": "#FF909090", "StrokeThickness": 1
}
CRITICAL theming rules for zone backgrounds:
FillTheme: "PanelBackground" — this respects Light/Dark theme switching automatically"Fill": "#0A000000" — these break in Dark themeFill + FillTheme: "" when you need process-specific meaning (e.g., water blue for a tank zone)Zone backgrounds must be placed FIRST in the Elements array so they render behind all other content.
Every process zone follows the same internal layout pattern. Coordinates are offsets from the zone's Left/Top origin:
| Panel | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
|
Symbol centering formula:
Symbol.Left = Zone.Left + (Zone.Width - Symbol.Width) / 2
NEVER put value and unit in separate TextBlocks side by side. Instead, use ONE TextBlock with composite LinkedValue:
{
"Type": "TextBlock",
"LinkedValue": "Flow: {@Tag.Plant/Intake/FIT_1001} GPM",
"Left": 30, "Top": 150, "Width": 170, "Height": 24,
"FontSize": 14
}
Patterns:
"Flow: {@Tag.X} GPM" — label + value + unit"{@Tag.X} NTU" — value + unit only"pH: {@Tag.X}" — label + value (no unit)Show process flow direction between zones using TextBlock arrows:
{
"Type": "TextBlock",
"Text": "→",
"Left": 228, "Top": 190, "Width": 15, "Height": 30,
"FontSize": 20
}
Arrow placement: horizontally centered in the gap between zones, vertically aligned with the symbol center.
The bottom panel (below process zones) should contain ONE of:
Option A: Trend + KPI sidebar
Bottom Panel | |
|---|---|
TrendChart (w:700, h:280) |
|
Option B: Trend + Alarm split
Bottom Panel | |
|---|---|
TrendChart (w:660, h:280) |
|
Option C: Full-width trend with status bar
Bottom Panel (h:30) | |
|---|---|
Status: Score: 0.023 Confidence: 0.98
|
Bottom panel sections should also use PanelBackground theme background rectangles.
...
| Element | Minimum Size | Recommended |
|---|---|---|
| Wizard Symbol | 70×70 | 80×80 to 100×100 |
| Library Symbol | 60×60 | 80×80 |
| Section Title | FontSize 14 | FontSize 15–16 |
| Value Text | FontSize 13 | FontSize 14 |
| Unit Label | FontSize 11 | FontSize 12 |
| Gauge (Circular) | 150×150 | 180×180 |
| TrendChart | 300×180 | 500×200 |
| AlarmViewer | 400×150 | 600×200 |
| Button | 100×35 | 130×40 |
...
Dashboard uses a grid with DashboardDisplay (columns/rows definition) and Cells array.
list_elements('Dashboard')
{
"table_type": "DisplaysList",
"data": [{
"Name": "MainPage",
"PanelType": "Dashboard",
"DashboardDisplay": {
"Columns": ["*", "*", "*"],
"Rows": ["*", "*"]
},
"Cells": [
{
"Row": 0, "Col": 0,
"Cell": { "HeaderLink": "Tank 1 Level" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Level.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 1,
"Cell": { "HeaderLink": "Tank 1 Temp" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 2,
"Cell": { "HeaderLink": "Trend" },
"Content": {
"Type": "TrendChart",
"Duration": "5m",
"Pens": {
"Type": "TrendPenList",
"Children": [
{ "Type": "TrendPen", "LinkedValue": "@Tag.Plant/Tank1/Level", "PenLabel": "Level", "Stroke": "#FF2196F3", "Auto": true }
]
}
}
},
{
"Row": 1, "Col": 0, "ColSpan": 3,
"Cell": { "HeaderLink": "Equipment Status" },
"Content": {
"Type": "Symbol",
"SymbolName": "Wizard/TANK",
"SymbolLabels": [
{ "Type": "SymbolLabel", "Key": "Value", "LabelName": "Value", "LabelValue": "@Tag.Plant/Tank1/Level.Value", "FieldType": "Expression" }
]
}
}
]
}]
}
Key Dashboard rules:
DashboardDisplay defines the grid: Columns and Rows arrays (use "*" for proportional sizing)Cell references Row/Col (0-based) and contains a Content elementColSpan/RowSpan for multi-cell elementsCell.HeaderLink adds a header label to the cell...
Browse available symbols:
list_elements('Symbol') -- all symbol categories
list_elements('Symbol/HMI') -- HMI symbol library
list_elements('Symbol/HMI/Equipment') -- equipment symbols
list_elements('Library') -- all library folders
list_elements('Wizard') -- Wizard symbol schema
Symbols are reusable components with parameterized inputs. When you place a symbol, you use SymbolLabels to connect the symbol's internal @Label. parameters to actual @Tag. values.
@Label.State, @Label.Speed, etc.@Label. key to a real @Tag. value.Concrete example — placing a Pump symbol:
{
"Type": "Symbol",
"SymbolName": "Wizard/PUMP",
"Left": 200, "Top": 300, "Width": 80, "Height": 80,
"SymbolLabels": [
{ "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression" },
{ "Type": "SymbolLabel", "Key": "Speed", "LabelName": "Speed", "LabelValue": "@Tag.Plant/Pump1/Speed.Value", "FieldType": "Expression" }
]
}
CRITICAL: Use @Tag. bindings in SymbolLabels. NEVER use @Label. when placing symbols in displays — @Label. is only for symbol internal definitions (in DisplaysSymbols).
WizardSymbols — always available, no import needed: TANK, VALVE, PUMP, MOTOR, BLOWER. Place with SymbolName: "Wizard/PUMP".
Library symbols (~1,600 available) auto-import when referenced by SymbolName. No manual import step needed — just reference the path (e.g., "HMI/Equipment/CentrifugalPump").
Sizing: Symbols are vector-based and scale to any proportional size. Use 40x40 for compact, 80x80 for medium, 120x120 for large — the library default is just a starting point. On Canvas, always use minimum 80×80 for process overview displays (see Step 3 minimum sizes).
...
Dynamics attach runtime behaviors to ANY element. They decouple the visual effect from the data binding, so any property of any control can become live.
Discovery: Call list_dynamics() with no parameter to see all available dynamic types, or narrow by category:
list_dynamics() -- all dynamics by category
list_dynamics('Color') -- color-changing dynamics
list_dynamics('Visibility') -- show/hide dynamics
list_dynamics('Animation') -- rotation, position, scaling
list_dynamics('Action') -- click/navigation dynamics
list_dynamics('FillColorDynamic') -- full schema for a specific dynamic
The most common visual dynamic. Changes an element's fill color based on a tag value.
Pattern: A Dynamics array on the element, containing one or more dynamic objects.
{
"Type": "Rectangle",
"Left": 100, "Top": 200, "Width": 60, "Height": 60,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [{
"table_type": "DisplaysList",
"data": [{
"ObjectName": "MainPage",
"PanelType": "Dashboard",
"DashboardDisplay": {
"Columns": ["*", "*", "*"],
"Rows": ["*", "*"]
},
"Cells": [
{
"Row": 0, "Col": 0,
"Cell": { "HeaderLink": "Tank 1 Level" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Level.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 1,
"Cell": { "HeaderLink": "Tank 1 Temp" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 2,
"Cell": { "HeaderLink": "Trend" },
"Content": {
"Type": "TrendChart",
"Duration": "5m",
"Pens": {
{ "Type": "TrendPenListChangeColorItem",
"ChangeLimit": "Children": [
0, "LimitColor": "#FF808080" },
{ "Type": "TrendPenChangeColorItem", "LinkedValueChangeLimit": "@Tag.Plant/Tank1/Level"1, "PenLabelLimitColor": "Level", "Stroke": "#FF2196F3", "Auto": true }#FF00FF00" }
]
}
}
]
}
For analog values (temperature, pressure), use graduated thresholds:
{
"Type": "FillColorDynamic",
}"LinkedValue": "@Tag.Plant/Tank1/Temperature",
}"ChangeColorItems": {
}"Type": "ColorChangeList",
{"Children": [
{ "RowType": 1"ChangeColorItem", "ColChangeLimit": 0, "ColSpanLimitColor": "#FF2196F3" 3},
{ "Type": "ChangeColorItem", "CellChangeLimit": {60, "HeaderLinkLimitColor": "Equipment Status#FFFFEB3B" },
{ "ContentType": {
"Type"ChangeColorItem", "ChangeLimit": 80, "LimitColor": "Symbol#FFFF9800" },
{ "SymbolNameType": "Wizard/TANKChangeColorItem",
"ChangeLimit": "SymbolLabels": [95, "LimitColor": "#FFF44336" }
{]
}
}
{
"Type": "SymbolLabelTextBlock",
"KeyText": "ValueALARM",
"LabelNameLeft": "Value", "LabelValue": "@Tag.Plant/Tank1/Level.Value"100, "Top": 50, "FieldTypeWidth": 80, "ExpressionHeight": }25,
]"Dynamics": [
}{
}
]
}]
}
Key Dashboard rules:
DashboardDisplay defines the grid: Columns and Rows arrays (use "*" for proportional sizing)Cell references Row/Col (0-based) and contains a Content elementColSpan/RowSpan for multi-cell elementsCell.HeaderLink adds a header label to the cellBrowse available symbols:
list_elements('Symbol') -- all symbol categories
list_elements('Symbol/HMI') -- HMI symbol library
list_elements('Symbol/HMI/Equipment') -- equipment symbols
list_elements('Library') -- all library folders
list_elements('Wizard') -- Wizard symbol schema
Symbols are reusable components with parameterized inputs. When you place a symbol, you use SymbolLabels to connect the symbol's internal @Label. parameters to actual @Tag. values. Here's how the mapping works:
@Label.State, @Label.Speed, etc.@Label. key to a real @Tag. value.Concrete example — placing a Pump symbol:
"Type": "VisibilityDynamic",
"LinkedValue": "@Tag.Plant/Tank1/AlarmActive"
}
]
}
Element is visible when the linked value is non-zero/true, hidden when zero/false.
{
"Type": "Symbol",
"SymbolName": "Wizard/BLOWER",
"Left": 200, "Top": 100, "Width": 80, "Height": 80,
"Dynamics": [
{
"Type": "RotationDynamic",
"LinkedValue": "@Tag.Plant/Fan1/Speed",
"MinAngle": 0, "MaxAngle": 360,
"MinValue": 0, "MaxValue": 100
}
]
}
Elements can have multiple dynamics simultaneously:
{
"Type": "Rectangle",
"Left": 100, "Top": 200, "Width": 60, "Height": 60,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{
"Type": "Symbol",
"SymbolName": "Wizard/PUMP",
"Left": 200, "Top": 300, "Width": 80, "Height": 80,
"SymbolLabels": [
{ "Type": "SymbolLabelChangeColorItem", "KeyChangeLimit": "State"1, "LabelNameLimitColor": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression"#FF4CAF50" }
]
}
},
{
"Type": "SymbolLabelVisibilityDynamic",
"Key": "Speed", "LabelName": "Speed", "LabelValue "LinkedValue": "@Tag.Plant/Pump1/Speed.Value", "FieldType": "Expression"Enabled"
}
]
}
The Key matches the symbol's internal @Label. name. The LabelValue provides the actual tag binding using @Tag. prefix. The symbol internally resolves @Label.State → the value of @Tag.Plant/Pump1/Running.Value.
CRITICAL: Use @Tag. bindings in SymbolLabels. NEVER use @Label. when placing symbols in displays — @Label. is only for symbol internal definitions (in DisplaysSymbols).
WizardSymbols — always available, no import needed: TANK, VALVE, PUMP, MOTOR, BLOWER. Place with SymbolName: "Wizard/PUMP". These provide easy customization through SymbolLabels for common industrial equipment.
Library symbols (~1,600 available) auto-import when referenced by SymbolName. No manual import step needed — just reference the path (e.g., "HMI/Equipment/CentrifugalPump").
Sizing: Symbols are vector-based and scale to any proportional size. Use 40x40 for compact, 80x80 for medium, 120x120 for large — the library default is just a starting point.
Dynamics attach runtime behaviors to ANY element. They decouple the visual effect from the data binding, so any property of any control can become live.
Discovery: Call list_dynamics() with no parameter to see all available dynamic types, or narrow by category:
Always verify dynamic schemas: Call list_dynamics('FillColorDynamic') (or whichever type) for the exact property names and structure.
...
Typically Navigation is only in Headers displays
For button/navigation behavior:
list_dynamics('ActionDynamic')
Simple format — single action, auto-mapped to MouseLeftButtonDown:
{
"Type": "Button",
"Text": "Go to Details",
"Left": 50, "Top": 400, "Width": 150, "Height": 40,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "DetailPage"
}
}
Nested format — multi-event, explicit mouse event mapping:
{ "ActionDynamic": [ { "Event": "MouseLeftButtonDown", "Actions": [list_dynamics()-- all dynamics by category list_dynamics('Color'){ "Type": "NavigateToDisplay", "DisplayName": "DetailPage" } ] }-- color-changing dynamics list_dynamics('Visibility') -- show/hide dynamics list_dynamics('Animation') -- rotation, position, scaling list_dynamics('Action') -- click/navigation dynamics list_dynamics('FillColorDynamic') -- full schema for a specific dynamic
The most common visual dynamic. Changes an element's fill color based on a tag value.
Pattern: A Dynamics array on the element, containing one or more dynamic objects. Each dynamic has a LinkedValue (data source) and behavior definition.
]
}
...
Displays are document objects — full replacement on write.
Creating a new display — complete example:
{
"table_type": "DisplaysList",
"data": [{
"Name": "ProcessOverview",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Size": "1366 x 720",
"Elements": [
{
{
"Type": "Rectangle",
"Left": 10020, "Top": 20060, "Width": 60208, "Height": 60320,
"DynamicsFillTheme": [
"PanelBackground",
{
"TypeStroke": "FillColorDynamic#FF909090", "StrokeThickness": 1
"LinkedValue": "@Tag.Plant/Pump1/Running"},
"ChangeColorItems": {
"Type": "ColorChangeListTextBlock",
"ChildrenText": ["Process Overview",
"Left": 20, { "TypeTop": 10, "ChangeColorItemWidth": 400, "ChangeLimitHeight": 0, "LimitColor35,
"FontSize": "#FF808080" },20
}
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF00FF00" }
]
}
}
]
}
This makes the rectangle gray when Running = 0 (stopped) and green when Running = 1 (running). The ChangeLimit values define thresholds — the color applies when the tag value reaches that limit.
]
}]
}
Modifying an existing display — read first, modify, write back:
get_objects('DisplaysList', names=['MainPage'], detail='full')
MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.
...
CodeBehind is CLIENT-SIDE C# or VB.NET code embedded in each display for lifecycle events and operator interaction.
Lifecycle methods:
DisplayOpening() — runs once when the display loadsDisplayIsOpen() — runs cyclically while displayed (interval set by IsOpenInterval)DisplayClosing() — runs once when the display closesDialogOnOK() — runs when a dialog's OK button is clickedCodeBehind is stored in the Contents field of the display, formatted as {Language}\r\n{Code}.
Minimal example — a button click that writes a tag valueFor analog values (temperature, pressure), use graduated thresholds:
{
"Typetable_type": "FillColorDynamicDisplaysList",
"LinkedValuedata": "@Tag.Plant/Tank1/Temperature",
"ChangeColorItems": [{
"TypeName": "ColorChangeListControlPage",
"ChildrenPanelType": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF2196F3" },
Canvas",
{ "TypeSize": "ChangeColorItem1366 x 720",
"ChangeLimit": 60, "LimitColorContents": "#FFFFEB3B" },
{ "Type": "ChangeColorItem", "ChangeLimit": 80, "LimitColor": "#FFFF9800" },
{ "Type": "ChangeColorItem", "ChangeLimit": 95, "LimitColor": "#FFF44336" }
]
}
}
{
CSharp\r\nvoid DisplayOpening()\r\n{\r\n // Initialize display state\r\n}\r\n\r\nvoid ButtonStart_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 1;\r\n}\r\n\r\nvoid ButtonStop_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 0;\r\n}",
"Elements": [
{
"Type": "TextBlockButton",
"Text": "ALARMStart Pump",
"Left": 10050, "Top": 50100, "Width": 80120, "Height": 2540,
"Dynamics": [
{"ClickEvent": "ButtonStart_Click"
"Type": "VisibilityDynamic"},
"LinkedValue": "@Tag.Plant/Tank1/AlarmActive"{
}
]
}
Element is visible when the linked value is non-zero/true, hidden when zero/false.
{
"Type": "SymbolButton",
"SymbolName "Text": "Wizard/BLOWERStop Pump",
"Left": 200, "Top": 100, "Width": 80120, "Height": 80,
"Dynamics": [
{
"Type": "RotationDynamic",
40,
"LinkedValueClickEvent": "@Tag.Plant/Fan1/Speed",
ButtonStop_Click"
},
"MinAngle": 0, "MaxAngle": 360, {
"MinValueType": 0, "MaxValueTextBlock": 100,
}
]
}
Elements can have multiple dynamics simultaneously:
{
"TypeLinkedValue": "Rectangle",
{@Tag.Plant/Pump1/Running.Value}",
"Left": 10050, "Top": 200160, "Width": 60200, "Height": 60,25
"Dynamics": [
{}
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF4CAF50" }
]
}
},
{
"Type": "VisibilityDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Enabled"
}
]
}
Always verify dynamic schemas: Call list_dynamics('FillColorDynamic') (or whichever type) for the exact property names and structure. The examples above are patterns — the schema is the source of truth.
For button/navigation behavior:
list_dynamics('ActionDynamic')
ActionDynamic supports two formats:
Simple format — single action, auto-mapped to MouseLeftButtonDown:
{
"Type": "Button",
"Text": "Go to Details",
"Left": 50, "Top": 400, "Width": 150, "Height": 40,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "DetailPage"
}
}
Nested format — multi-event, explicit mouse event mapping:
{
"ActionDynamic": [
{
"Event": "MouseLeftButtonDown",
"Actions": [
{ "Type": "NavigateToDisplay", "DisplayName": "DetailPage" }
]
}
]
}
Common pattern: navigation button = ActionDynamic + ShineDynamic for visual click feedback.
For all available dynamics:
list_dynamics() -- lists all dynamic types
Displays are document objects — full replacement on write.
Creating a new display: Write the full object as shown in Steps 2–3.
Modifying an existing display — read first, modify, write back:
get_objects('DisplaysList', names=['MainPage'], detail='full')
Then modify the content and write the full object.
MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.
CodeBehind is CLIENT-SIDE C# or VB.NET code embedded in each display for lifecycle events and operator interaction.
Lifecycle methods:
DisplayOpening() — runs once when the display loadsDisplayIsOpen() — runs cyclically while displayed (interval set by IsOpenInterval)DisplayClosing() — runs once when the display closesDialogOnOK() — runs when a dialog's OK button is clickedCodeBehind is stored in the Contents field of the display, formatted as {Language}\r\n{Code}.
Minimal example — a button click that writes a tag value:
{
"table_type": "DisplaysList",
"data": [{
"ObjectName": "ControlPage",
"PanelType": "Canvas",
"Size": "1366 x 728",
"Contents": "CSharp\r\nvoid DisplayOpening()\r\n{\r\n // Initialize display state\r\n}\r\n\r\nvoid ButtonStart_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 1;\r\n}\r\n\r\nvoid ButtonStop_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 0;\r\n}",
"Elements": [
{
"Type": "Button",
"Text": "Start Pump",
"Left": 50, "Top": 100, "Width": 120, "Height": 40,
"ClickEvent": "ButtonStart_Click"
},
{
"Type": "Button",
"Text": "Stop Pump",
"Left": 200, "Top": 100, "Width": 120, "Height": 40,
"ClickEvent": "ButtonStop_Click"
},
{
"Type": "TextBlock",
"LinkedValue": "{@Tag.Plant/Pump1/Running.Value}",
"Left": 50, "Top": 160, "Width": 200, "Height": 25
}
]
}]
}
Key CodeBehind rules:
Contents field format: first line is language (CSharp or VBdotNet), followed by \r\n, then code@ prefix for runtime object access in code: @Tag.Path/Name.Value, @Client.Context.AssetPathSee the Scripts and Expressions skill for full CodeBehind and server-side scripting guidance.
Layout regions: Header, Footer, Menu, Submenu, Content.
get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
The Startup layout defines which display loads into the Content region (typically MainPage). Only modify if you need:
Layouts are document objects — read first, modify, write back.
Dependencies: DisplaysSymbols → DisplaysList → DisplaysLayouts
Common pattern for plant-wide navigation with dynamic content:
Architecture:
Client.Context updates → content displays reactStatic binding (fixed tag path):
@Tag.Area1/Line1/State
Dynamic binding (resolves based on selected asset):
Asset(Client.Context.AssetPath + "State1")
This allows a single display template to show data for whichever asset the operator selects in the navigation tree.
Setting Up Asset Navigation:
Plant/Line1/Motor1, Plant/Line2/Motor1 with same member names)Asset() syntax in display bindings for dynamic resolutionbrowse_runtime_properties('Client') at design time to explore the Client namespace and Context propertiesWhen setting custom colors on elements, be aware that theme overrides can silently replace your custom values at runtime.
Best practice: OMIT color properties to use themed defaults. Only specify colors when intentionally overriding for process-specific meaning (alarm red, water blue, etc.).
Pattern — set color AND clear theme (when you must override):
{
"Fill": "#FF3498DB",
"FillTheme": ""
}
If you only set Fill without clearing FillTheme, the theme engine may override your color. Always clear the corresponding theme property when setting custom colors.
Use list_elements('ThemeColors') for available named theme brushes (StateOK, StateAlarm, Water, AlarmHighPriority, etc.).
]
}]
}
Key CodeBehind rules:
Contents field format: first line is language (CSharp or VBdotNet), followed by \r\n, then code@ prefix for runtime object access in code: @Tag.Path/Name.Value, @Client.Context.AssetPathSee the Scripts and Expressions skill for full CodeBehind and server-side scripting guidance.
...
Layout regions: Header, Footer, Menu, Submenu, Content.
get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
The Startup layout defines which display loads into the Content region (typically MainPage). Only modify if you need:
Layouts are document objects — read first, modify, write back. Dependencies: DisplaysSymbols → DisplaysList → DisplaysLayouts
...
When a solution has multiple content pages (Dashboard, ProcessOverview, AlarmPage, TrendPage, etc.), navigation buttons belong in the Header display — not on each content page.
The Header region is persistent — it stays visible while the Content region changes. Placing navigation here means every page gets consistent navigation without duplicating buttons on each content page.
Read the Startup layout to find the Header display name:
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
Read the Header display (it's a Canvas page assigned to the Header region):
get_objects('DisplaysList', names=['Header'], detail='full')
Add navigation buttons to the Header display — right-aligned in the header bar:
{
"Type": "Button",
"Text": "Dashboard",
"Left": 900, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "MainPage"
}
},
{
"Type": "Button",
"Text": "Process",
"Left": 1020, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "ProcessOverview"
}
},
{
"Type": "Button",
"Text": "Alarms",
"Left": 1140, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "AlarmPage"
}
}
Write back the modified Header display (read-modify-write — it's a document object).
Do NOT put page navigation buttons on content pages (Canvas or Dashboard). Content pages should only contain buttons for in-page actions (start/stop, acknowledge, open popup). Page-to-page navigation is always in the Header.
Exception: If the solution has NO layout (single standalone page), navigation buttons can go on the content page itself — but this is unusual for production solutions.
...
Common pattern for plant-wide navigation with dynamic content:
Architecture:
Client.Context updates → content displays reactStatic binding (fixed tag path):
@Tag.Area1/Line1/State
Dynamic binding (resolves based on selected asset):
Asset(Client.Context.AssetPath + "State1")
This allows a single display template to show data for whichever asset the operator selects in the navigation tree.
Setting Up Asset Navigation:
Plant/Line1/Motor1, Plant/Line2/Motor1 with same member names)Asset() syntax in display bindings for dynamic resolutionbrowse_runtime_properties('Client') at design time to explore the Client namespace and Context properties...
When setting custom colors on elements, be aware that theme overrides can silently replace your custom values at runtime.
Key theme colors for displays:
| Theme Name | Purpose | Usage |
|---|---|---|
PageBackground | Display page background | Set on display properties |
PanelBackground | Zone/section backgrounds | Default for zone Rectangles |
PopupBackground | Higher contrast panels | Use for callouts, modal-like zones |
ControlBackground | Control backgrounds | Auto-applied to gauges, charts |
TextForeground | Text color | Auto-applied to TextBlocks |
DefaultBorder | Border color | Auto-applied to control borders |
Best practice: OMIT color properties to use themed defaults. Only specify colors when intentionally overriding for process-specific meaning (alarm red, water blue, etc.).
Pattern — set color AND clear theme (when you must override):
{
"Fill": "#FF3498DB",
"FillTheme": ""
}
If you only set Fill without clearing FillTheme, the theme engine may override your color. Always clear the corresponding theme property when setting custom colors.
Use list_elements('ThemeColors') for available named theme brushes (StateOK, StateAlarm, Water, AlarmHighPriority, etc.).
...
Before writing any Canvas display, verify:
FillTheme: "PanelBackground""Flow: {@Tag.X} GPM")PanelBackground theme RectanglesName (not ObjectName)PanelType is set at top level (never inside JsonFormat)...
| Mistake | Why It Happens | How to Avoid |
|---|---|---|
Using DisplaysDraw as table_type | Confused with DisplaysList | DisplaysDraw is the visual editor UI, not a writable table |
| Omitting PanelType | Defaults silently to Canvas | Always set PanelType explicitly at top level |
Using ObjectName instead of Name | Hallucinated field name | The identifier field is Name |
Using JsonFormat wrapper | Outdated bug workaround | NEVER use JsonFormat. All properties go at top level |
Using @Label. in display elements | Confused with symbol definitions | @Label. is only for DisplaysSymbols internals. Use @Tag. when placing symbols |
| Not discovering dynamics first | Doesn't know what's available | Call list_dynamics() with no parameter to see all dynamic types |
| Screenshots for self-validation | Habit from other tools | Trust write_objects success. User sees live updates |
| Setting colors without clearing theme | Theme overrides custom colors | Set value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} — or just omit to use themed defaults |
| Sending partial display content | Forget it's a document object | Always read-modify-write for existing displays |
| Canvas positioning in Dashboard | Mixing layout paradigms | Canvas uses Left/Top + Elements; Dashboard uses Row/Col + Cells |
| Referencing symbols with wrong path | Incomplete library path | Browse with list_elements('Symbol/HMI') to get exact paths |
| Guessing dynamic property names | Different dynamics have different schemas | Always call list_dynamics('DynamicTypeName') for exact schema |
| CodeBehind in wrong Contents format | Missing language prefix | Contents must start with CSharp\r\n or VBdotNet\r\n before code |
| All Canvas content in top-left | No zone planning | Calculate zones FIRST to fill the full canvas |
| Symbols at 40×40 or 50×50 | Copying library defaults | Minimum 80×80 on process overview displays |
| Value and Unit as separate TextBlocks | Seems logical but misaligns | Single TextBlock: "{@Tag.X} GPM" |
| No background rectangles on zones | Doesn't know about visual grouping | Always add Rectangle with FillTheme: "PanelBackground" |
| Hardcoded fill colors on zone backgrounds | Breaks in Dark theme | Use FillTheme: "PanelBackground" instead |
| FontSize 10–11 for values | Default seemed fine | Minimum 13, prefer 14 for operator readability |
| Navigation buttons on content pages | Doesn't know about Header pattern | Page navigation goes in the Header display (Step 10b). Content pages only get in-page action buttons |
| Mistake | Why It Happens | How to Avoid |
Using DisplaysDraw as table_type | Confused with DisplaysList | DisplaysDraw is the visual editor UI, not a writable table |
| Omitting PanelType | Defaults silently to Canvas | Always set PanelType explicitly |
Using @Label. in display elements | Confused with symbol definitions | @Label. is only for DisplaysSymbols internals. Use @Tag. when placing symbols |
| Not discovering dynamics first | Doesn't know what's available | Call list_dynamics() with no parameter to see all dynamic types |
| Screenshots for self-validation | Habit from other tools | Trust write_objects success. User sees live updates |
| Setting colors without clearing theme | Theme overrides custom colors | Set value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} — or just omit to use themed defaults |
| Sending partial display content | Forget it's a document object | Always read-modify-write for existing displays |
| Canvas positioning in Dashboard | Mixing layout paradigms | Canvas uses Left/Top + Elements; Dashboard uses Row/Col + Cells |
| Referencing symbols with wrong path | Incomplete library path | Browse with list_elements('Symbol/HMI') to get exact paths |
| Guessing dynamic property names | Different dynamics have different schemas | Always call list_dynamics('DynamicTypeName') for exact schema |
| CodeBehind in wrong Contents format | Missing language prefix | Contents must start with CSharp\r\n or VBdotNet\r\n before code |
...
| Display Action | Tool Call |
|---|---|
| Get display schema | get_table_schema('DisplaysList') |
| Get Canvas structure | list_elements('Canvas') |
| Get Dashboard structure | list_elements('Dashboard') |
| Get element schema | list_elements('ElementName') |
| Browse all dynamics | list_dynamics() |
| Get specific dynamic schema | list_dynamics('FillColorDynamic') |
| Browse symbols | list_elements('Symbol/HMI') or list_elements('Library') |
| Browse Wizard symbols | list_elements('Wizard') |
| Browse theme colors | list_elements('ThemeColors') |
| Read existing display | get_objects('DisplaysList', names=['PageName'], detail='full') |
| Write display | write_objects('DisplaysList', data=[...]) |
| Read layout (for Header name) | get_objects('DisplaysLayouts', names=['Startup'], detail='full') |
| Read Header for nav buttons | get_objects('DisplaysList', names=['Header'], detail='full') |
| Navigate Designer to display | designer_action('navigate', 'Display.PageName') |
| Browse runtime properties | browse_runtime_properties('Client') |
...