LineGridCanvasPanel: Creating Grids, Timelines And Piano Rolls
What this covers
- What
LineGridCanvasPanelis for - The core building blocks
- How line grids are rendered
- How visible-only rendering works
- How child widgets are positioned on the grid
- What
LineGridChildWidgetis expected to provide - What to consider when a grid is flipped
- Where scrolling and zooming fit into the system
What LineGridCanvasPanel Solves
UAxLineGridCanvasPanel is used when a UI needs to display content against a logical grid.
Examples include:
- Timelines
- Piano rolls
- Sequencers
- Track editors
- Bar / beat rulers
- Time-based editors
- Row-and-column editors
The important problem it solves is:
How to draw a large logical grid while only rendering the part that is currently visible, and how to place widgets on that grid using logical units instead of raw pixels.
A normal canvas places widgets using physical position and size.
A line grid canvas places widgets using logical meaning.
For example:
For a piano roll:
X0 = Beat 4, X1 = Beat 8
y0 = Midi Note 60 , y1 = Midi Note 61
For Timeline/Sequencer:
X0 = 12.5 seconds, X1= 18.0 seconds
Y0 = Track2 , Y1 = Track3
The panel converts those logical values into the physical position and size needed by UMG.
This article gets a bit technical, Its recommended you can jump to the
What To Adjust First section
If you’re trying to recap or just get started quickly. Below we’ll cover the technicalities/low level functionalities of the system in case you need to debug or extend it.
Core Building Blocks
There are three main pieces:
1. LineGridCanvasPanel (UAxLineGridCanvasPanel)
This is the UMG panel.
It is responsible for:
- Owning the line grid definitions
- Creating runtime
UAxLineGridobjects - Updating those runtime objects when properties change
- Rendering the visible part of each grid
- Resolving which logical units are currently in view
- Positioning grid-aware child widgets
- Giving derived classes hooks to spawn and remove widgets based on the visible range
This class extends UAxSmartCanvasPanel, so it still participates in the Smart Canvas model, but it adds line-grid rendering and logical child positioning.
Where to look in code
- Main panel class:
UAxLineGridCanvasPanel
- Runtime grid storage:
LineGridsLineGridObjects
- Runtime setup:
SynchronizeProperties()MakeAndInsertLineGridObject()GetLineGridObject()
2. LineGrid (UAxLineGrid)
A UAxLineGrid represents one group of repeated lines.
A line grid can represent:
- Beats
- Bars
- Seconds
- Frames
- Tracks
- Notes
- Any other logical unit you define
A line grid has an ID. That ID matters because child widgets use it to say what units their X and Y values are in.
A line grid also controls:
- Orientation
- Line spacing
- Whether logical units are flipped
- Logical offset
- Line drawing
- Tick lines
- Text labels
- Background drawing
- Parent / child grid relationships
Parent and child line grids
A line grid can be a child of another line grid.
For example:
- Bars can be the parent grid
- Beats can be a child grid
- Beat spacing can be calculated from bar spacing
This is useful during zooming because you usually only want to change the parent grid’s line spacing. Child grids can then update from the parent.
Where to look in code
- Grid object:
UAxLineGrid
- Grid properties:
FAxLineGridProperties
- Parent / child grid functions:
AddChildLineGrid()ClearChildLineGrids()UpdateChildLineGridProperties()
- Position conversion:
GetGeometryPositionOfLogicalPosition()GetLogicalIndexAtGeometryPosition()GetLogicalIndexesInView()
Rendering Model
The line grid is not rendered as one giant prebuilt texture.
Instead, it is drawn during the paint pass.
The simplified flow is:
Slate paint starts
↓
Visible local rect is calculated
↓
Panel resolves logical units in view
↓
Derived classes may remove out-of-view widgets
↓
Derived classes may spawn in-view widgets
↓
Child widget geometry is updated
↓
Visible grid lines, backgrounds, labels, and ticks are drawn
This is very important for performance because the panel can be larger than the area currently visible to the user.
For example, a timeline might be 80,000 pixels wide, but the user may only see a 1,200 pixel area.
The line grid canvas does not need to draw all 80,000 pixels worth of grid lines. It only needs to draw the part that intersects the visible display area.
Visible-Only Rendering
SAxLineGridCanvas calculates an intersecting rect.
That rect represents the part of the line grid canvas that is actually visible.
If the line grid canvas has no parent, the full local rect is considered visible.
If it has a parent, the Slate widget compares:
- The line grid canvas rect
- The parent rect
The result is converted back into the line grid canvas local space.
This is why moving or resizing the line grid canvas does not automatically make drawing more expensive.
The cost is based mainly on the visible rect and the line spacing, not the total logical size of the canvas.
For example:
Canvas size: 30,000 px wide
Visible display area: 1,000 px wide
Line spacing: 100 px
Approximate rendered vertical lines:
1,000 / 100 = 10 visible lines
The panel only needs to render the visible line range.
Where to look in code
- Visible rect:
SAxLineGridCanvas::GetIntersectingRect()
- Rendering visible line grids:
SAxLineGridCanvas::RenderLineGrids()
- Pre-render hook:
UAxLineGridCanvasPanel::HandlePreRenderLineGrids()
- Visible logical range:
UAxLineGrid::GetLogicalIndexesInView()
Line Grid Properties
Each entry in LineGrids describes one grid to render.
The common properties to configure are:
ID
The unique logical unit name.
Examples:
Bars
Beats
Tracks
Pitch
Seconds
Frames
This ID is also what child widgets use when they say which units their X and Y values are in.
Orientation
Controls the direction of the repeated lines.
A horizontal orientation means the grid grows across the X axis and draws vertical-looking divisions.
A vertical orientation means the grid grows across the Y axis and draws horizontal-looking divisions.
For a piano roll, you may use:
Horizontal grid: Beats or Bars
Vertical grid: Pitch rows
For a timeline, you may use:
Horizontal grid: Seconds, Frames, Beats, or Bars
Vertical grid: TracksLineSpacing
The physical pixel spacing between grid lines.
This is the value that changes when the grid is zoomed.
A larger line spacing means the user is zoomed in.
A smaller line spacing means the user is zoomed out.
bFlipLogicalUnits
Controls whether logical values increase in the normal physical direction or the opposite direction.
This matters for cases like piano rolls where pitch may need to increase upward while Slate/UMG Y positions increase downward.
Drawing options
The grid can also draw:
- Lines
- Tick lines
- Text labels
- Alternating backgrounds
- Border lines
These are visual features layered on top of the same logical grid system.
Child Widgets On The Grid
The line grid canvas expects positioned children to be UAxLineGridChildWidget.
A UAxLineGridChildWidget is a SmartWidget specialization that represents a rectangle in logical grid units instead of pixels.
For example, a note widget in a piano roll might mean:
X0 = Beat 4
X1 = Beat 5
Y0 = Pitch 60
Y1 = Pitch 61
The child does not need to calculate its own pixel position.
Instead, the panel reads:
LogicalXUnitsIDLogicalYUnitsIDLogicalPositionRect
Then it resolves those values against the matching line grids.
The final physical CanvasPanelSlot position and size are set by the panel.
Child Unit Resolution
Each child can provide its own logical unit IDs.
For example:
LogicalXUnitsID = "Beats"
LogicalYUnitsID = "Pitch"
If a child does not provide a unit ID, the panel falls back to:
ChildWidgetsLayoutProperties.DefaultLogicalXUnitsID
ChildWidgetsLayoutProperties.DefaultLogicalYUnitsID
This is useful when most children use the same unit system.
For example, in a piano roll, most notes may use: Beats and Pitch exclusively
Then individual note widgets do not need to set their own unit IDs unless they need to override the default.
Important rule
The resolved unit IDs must match valid LineGrid IDs.
If the panel cannot resolve the child’s X or Y unit grid, it cannot position that child correctly.
Expected Child Widget Setup
A line grid child widget is expected to provide:
- A logical X range
- A logical Y range
- X and Y unit IDs, either directly or through the panel defaults
The anchoring rule is important.
Positioned line grid children must use (Top Left):
Anchors.Minimum = (0, 0)
Anchors.Maximum = (0, 0)
Alignment = (0, 0)
The reason is that the panel writes explicit top-left position and size values into the child’s UCanvasPanelSlot.
If the child uses different anchors or alignment, UMG will interpret the position differently.
The panel validates this and auto-corrects the slot at runtime, but it is still best to set it correctly in the widget blueprint.
Where to look in code
- Child widget type:
UAxLineGridChildWidget
- Logical unit getters:
GetLogicalXUnitsID()GetLogicalYUnitsID()
- Logical rect getter:
GetLogicalPositionRect()
- Logical value getters:
GetX0()GetX1()GetY0()GetY1()
- Child positioning:
UpdateChildWidgetsGeometry()UpdateChildWidgetGeometry()
- Slot validation:
ValidateAndAutoCorrectChildAnchoring()
Child Logical Rect To Physical Rect
During painting, the panel updates the child widgets.
The simplified conversion is:
Child logical X0/X1
↓
Resolve X LineGrid
↓
Convert logical X values to physical X positions
Child logical Y0/Y1
↓
Resolve Y LineGrid
↓
Convert logical Y values to physical Y positions
Final physical rect
↓
Set CanvasPanelSlot position and size
For a non-flipped grid:
Left = PositionOf(X0)
Right = PositionOf(X1)
Top = PositionOf(Y0)
Bottom = PositionOf(Y1)
For a flipped grid, the start and end values are intentionally swapped for that axis:
Left = PositionOf(X1)
Right = PositionOf(X0)
or
Top = PositionOf(Y1)
Bottom = PositionOf(Y0)
After that, the panel normalizes the physical rect so the left side is smaller than the right side, and the top side is smaller than the bottom side.
This keeps the final canvas slot valid even when logical direction is flipped.
Size Clamping And Breakpoints
After the logical rect is converted into physical space, the resulting child widget can become very small.
This often happens when the user zooms out.
For example:
A note that is 1 beat long may become only 4 pixels wide.
That may be too small for interaction.
ChildWidgetsLayoutProperties.ClampSizeAxis controls which axis may be expanded to preserve the child widget’s minimum input size.
This does not change the logical value of the child.
It only changes the rendered physical size so the widget remains usable.
BreakpointSize is a separate threshold.
The base panel does not automatically hide text or simplify visuals when the breakpoint is reached. Instead, child widgets or derived panels can use this threshold to decide when to render a simpler representation.
For example:
- Hide text when the note is too narrow
- Draw only a compact block
- Disable dense internal visuals
- Use a simpler hover target
In-View Logical Range
Before child widget geometry is updated, the panel resolves which logical X and Y units are currently visible.
These are controlled by:
ChildWidgetsLayoutProperties.LogicalInViewXUnitsID
ChildWidgetsLayoutProperties.LogicalInViewYUnitsID
These IDs are not required to match every child widget’s placement units.
Their job is to define the logical window used for visibility decisions.
For example, a child might be positioned in beats, but the panel may decide the visible range using bars.
The panel then calls:
RemoveOutOfViewWidgets()
SpawnInViewWidgets()
The base implementations are intentionally empty.
Derived classes decide what widget ownership policy they need.
For example:
- A piano roll may spawn note widgets for visible notes only
- A timeline may recycle clip widgets
- A sequencer may keep all widgets alive but hide out-of-view ones
- A track editor may spawn row headers separately from content widgets
Where to look in code
- In-view range:
HandlePreRenderLineGrids()
- Virtualization hooks:
RemoveOutOfViewWidgets()SpawnInViewWidgets()
Working With Flipped Grids
Flipped grids require extra care because logical direction and physical direction are not the same.
In a normal horizontal grid, larger logical X values usually move to the right.
In a flipped horizontal grid, larger logical X values may move to the left.
In a normal vertical grid, larger logical Y values usually move downward.
In a flipped vertical grid, larger logical Y values may move upward.
The UAxScrollableZoomableGridWidget and The LineGridCanvasPanel
Already handles most of this logic for you , but this information is for debugging incase it does not work as expectected.
Example: Piano Roll
A piano roll usually has two important grids:
Horizontal grid: Beats
Vertical grid: PitchA note widget might use:
LogicalXUnitsID = "Beats"
LogicalYUnitsID = "Pitch"
X0 = 4.0
X1 = 5.0
Y0 = 60
Y1 = 61If pitch should increase upward, the pitch grid will usually need flipped logical units.
That lets higher pitch values appear higher on the screen, even though UMG Y coordinates normally increase downward.
Extending The Panel
You usually extend UAxLineGridCanvasPanel when you need custom behavior around what appears in the visible range or how the grid is drawn.
Common extension points include:
RemoveOutOfViewWidgets()
Use this to remove, hide, or recycle widgets that no longer belong to the current visible logical range.
SpawnInViewWidgets()
Use this to create widgets for data that should now be visible.
RenderCurrentLine()
Use this if the line drawing behavior needs to change.
RenderLineLabel()
Use this to customize label rendering.
RenderLineTick()
Use this to customize tick line drawing.
GetLineLabel()
Use this when logical indexes need custom labels.
For example:
0 → Bar 1
1 → Bar 2
60 → C4
61 → C#4What To Adjust First
Create a UUserWidget Blueprint And Add A LineGridCanvasPanel, then start with these settings:
1. Define the line grids
Create line grid entries for the logical units you need.
Example:
Bars
Beats
Tracks
Pitch
Seconds
Frames
2. Set each grid orientation
Use horizontal orientation for units that grow across the X axis.
Use vertical orientation for units that grow across the Y axis.
3. Set line spacing
This controls how dense the grid appears.
It is also the main value affected by zooming.
4. Configure default child units
Set:
DefaultLogicalXUnitsID
DefaultLogicalYUnitsIDThis reduces setup work for child widgets.
5. Configure in-view units
Set:
LogicalInViewXUnitsID
LogicalInViewYUnitsIDThese control the logical range used for spawning and removing child widgets.
6. Check flipped axes
If your logical direction should run opposite to UMG’s physical direction, enable bFlipLogicalUnits on that grid.
This is most common on vertical pitch grids.
7. Set child anchors correctly
Line grid children should use top-left point anchors and top-left alignment.
At runtime as you spawn widgets , use this anchor. If testing while in the designer, use this anchor.
The expectation is you’ll be spawning children dynamically at runtime during SpawnInViewWidgets().
The panel can auto-correct this, but the widget blueprint/ dynamic code should still be authored correctly.
Where Scrolling And Zooming Fit
LineGridCanvasPanel knows how to draw grids and place children on those grids.
It does not, by itself, define the complete user-facing scroll and zoom experience.
Scrolling and zooming are usually handled by a wrapper widget that changes:
- The line grid canvas position
- The line grid canvas size
- The parent line grid spacing
- The normalized scroll value
- The normalized zoom value
At a higher level, you don’t need to worry about that, as you’re expected to use or inherit from
UAxScrollableZoomableGridWidget
and Add your custom LineGridCanvas to it.
This wrapper widget is pre setup to zoom and scroll whatever linegrid widget you hand to it.
Read the next article to understand how to scroll and zoom the LineGridCanvasPanel and how the UAxScrollableZoomableGridWidget works .