11 KiB
Creating and Displaying TULayoutView in Toon Boom
This document explains how to create custom views and display them in Toon Boom Harmony/Storyboard Pro using the TULayoutView system.
Overview
Toon Boom uses a hierarchical layout system:
- TULayoutManager - Central manager for all views and frames
- TULayoutArea - Metadata about a view type that can be instantiated
- TULayoutFrame - A window/panel containing tabbed view holders
- TULayoutViewHolder - Container holding 1-2 TULayoutView instances with optional splitter
- TULayoutView - Abstract base class for actual view content
Creating Custom Views by Subclassing TULayoutView
The recommended approach is to directly subclass TULayoutView and implement the required pure virtual methods.
Critical Discovery: How widget() Works
IMPORTANT: The widget() method (vtable slot 1) is called by TULayoutArea::add and the return value is treated as a TULayoutView*, NOT a QWidget*. The actual displayable widget is obtained later via getWidget().
This means:
widget()should returnthis(the view itself, cast to QWidget*)getWidget()should return the actual QWidget for display
Required Pure Virtual Methods
When subclassing TULayoutView, you must implement these 5 pure virtual methods:
// Slot 1: Returns THIS view as the "widget" - treated as TULayoutView* by caller
virtual QWidget *widget() = 0;
// Slots 3-4: Return the actual displayable QWidget
virtual const QWidget *getWidget() const = 0;
virtual QWidget *getWidget() = 0;
// Slot 29: Called when menus need refresh
virtual void triggerMenuChanged() = 0;
// Slot 31 (protected): Marker method
virtual void isTULayoutView() = 0;
Example: Complete Custom View Implementation
// test_frame.hpp
#pragma once
#include "toon_boom_layout.hpp"
class TestView : public TULayoutView {
public:
TestView();
~TestView() override;
// Pure virtuals - MUST implement all 5
QWidget *widget() override;
const QWidget *getWidget() const override;
QWidget *getWidget() override;
void triggerMenuChanged() override {}
// Override initiate to return this view
TULayoutView *initiate(QWidget *parent) override;
// Optional overrides
QString displayName() const override;
protected:
void isTULayoutView() override {}
private:
QFrame *m_frame; // The actual widget content
QVBoxLayout *m_mainLayout;
};
// test_frame.cpp
#include "test_frame.hpp"
#include <QtWidgets/QLabel>
TestView::TestView()
: TULayoutView() {
// Create the frame that will hold our content
m_frame = new QFrame();
m_frame->setMinimumSize(400, 300);
m_mainLayout = new QVBoxLayout(m_frame);
QLabel *label = new QLabel("Hello from custom view!");
label->setAlignment(Qt::AlignCenter);
m_mainLayout->addWidget(label);
}
QWidget *TestView::widget() {
// CRITICAL: TULayoutArea::add calls this via vtable[1] and expects
// a TULayoutView* return value, NOT QWidget*. The returned pointer
// is then used to call getWidget() for the actual widget.
// So we return `this` which IS-A TULayoutView*.
return reinterpret_cast<QWidget*>(static_cast<TULayoutView*>(this));
}
const QWidget *TestView::getWidget() const {
return m_frame;
}
QWidget *TestView::getWidget() {
return m_frame;
}
TULayoutView *TestView::initiate(QWidget *parent) {
// Return this view - it's already initialized
if (parent && m_frame) {
m_frame->setParent(parent);
}
return this;
}
QString TestView::displayName() const {
return QString("My Custom View");
}
TestView::~TestView() {
delete m_frame;
}
Registering Views with TULayoutManager
TULayoutManager::addArea Signature
bool TULayoutManager::addArea(
const char* typeName, // Type identifier (e.g., "Colour", "Node View")
const QString& displayName, // Translated display name
TULayoutView* view, // View instance (your TestView*)
bool visible, // Initially visible
bool createFrame, // Create new frame for this view
bool docked, // Is docked (not floating)
const QSize& minSize, // Minimum size
bool useMinSize, // Whether to use minimum size
bool isPlugin, // Is this a plugin area
bool defaultVisible, // Default visibility state
bool unknown // Unknown flag (usually true)
);
Complete Registration Example
void showCustomView() {
auto lm = PLUG_Services::getLayoutManager();
if (!lm) {
return;
}
// Create your custom view - it's a direct TULayoutView subclass
TestView* myView = new TestView();
// Register with the layout manager
bool success = lm->addArea(
"TestView", // typeName (unique ID)
QString("My Test View"), // displayName
myView, // TULayoutView* - pass directly, no offset needed!
true, // visible
true, // createFrame
true, // docked
QSize(500, 400), // minSize
true, // useMinSize
false, // isPlugin
true, // defaultVisible
true // unknown
);
if (success) {
// Raise the view to show it
auto area = lm->findArea(QString("TestView"));
if (area) {
lm->raiseArea(area, nullptr, false, QPoint(100, 100));
}
}
}
The Call Flow Explained
When you call TULayoutManager::addArea, the following happens:
- addArea stores your
TULayoutView*in a newTULayoutArea - When the view needs to be displayed, TULayoutArea::add is called
addcallsview->widget()(vtable slot 1) - expects TULayoutView return!*- The return value is passed to TULayoutViewHolder::addView
addViewcallsview->getWidget()to get the actual QWidget- The QWidget is reparented and displayed
This is why widget() must return this - the calling code treats the return as TULayoutView* to make further virtual calls.
View Toolbars
Some built-in views display a view-specific toolbar when they are the active tab (e.g., Drawing and Reference View).
This is driven by:
TULayoutView::toolbar()returning a non-nullQDomElementfor the toolbar definitionTULayoutView::m_toolbarInfo(LAY_ToolbarInfo) being non-default so the frame actually shows/updates the toolbar
Details and concrete implementations are documented in docs/TULayoutView_Toolbar_Integration.md.
Opening Views at Runtime
To programmatically show a view that's already registered:
// Using TULayoutManager::raiseArea
TULayoutView* view = layoutManager->raiseArea(
QString("TestView"), // Area name
targetFrame, // TULayoutFrame* (or nullptr for current)
true, // Create new instance if needed
QPoint(0, 0) // Position hint
);
Key Patterns
1. DPI Scaling
For views with minimum size requirements, use UT_DPI::scale():
QSize baseSize(260, 450);
QSize scaledSize = UT_DPI::scale(baseSize);
2. Common View Sizes (from HarmonyPremium analysis)
| View Type | Base Size |
|---|---|
| Colour | 260 × 450 |
| Coord. And Control Points | 260 × 300 |
| Layer Properties | 260 × 450 |
| Onion Skin | 480 × 300 |
| Timeline | 800 × 400 |
| Tool Properties | 260 × 450 |
| Top | 300 × 400 |
| Camera, Drawing, Node View, etc. | 0 × 0 (no minimum) |
3. Menu Registration
Set menus on views using TULayoutView::setMenu:
TULayoutView::setMenu(
layoutView,
actionManager, // AC_Manager*
"MENU_ID", // Menu identifier
MenuType::Primary // 0 = Primary, 1 = Secondary
);
TULayoutViewHolder Usage
The TULayoutViewHolder is a QWidget container that can hold 1-2 TULayoutView instances with an optional vertical splitter.
How Views Get Into TULayoutViewHolder
When you call TULayoutManager::addArea or TULayoutManager::raiseArea, the system:
- Creates a
TULayoutFrameif needed - Creates a
TULayoutViewHolderwithin the frame - Calls
TULayoutViewHolder::addView(view, splitterRatio)to add your view - The view's
getWidget()method is called to get the actual QWidget - The widget is reparented and added to the holder's internal layout
TULayoutViewHolder::addView
bool TULayoutViewHolder::addView(
TULayoutView* view, // View to add
double splitterRatio // Ratio for splitter (default 0.5)
);
Returns: true if added successfully, false if holder is full (max 2 views)
Internal Structure (from ToonBoomLayout.dll)
TULayoutViewHolder (sizeof = 0x70 = 112 bytes)
├── QWidget base class (0x00-0x27)
├── std::vector<TULayoutView*> m_views (+0x28, 24 bytes)
├── double m_savedSplitterRatio (+0x40, default 0.5)
├── UI_Splitter* m_splitter (+0x48, vertical orientation)
├── WID_VBoxLayout* m_leftLayout (+0x50)
├── WID_VBoxLayout* m_rightLayout (+0x58)
├── QFrame* m_leftFrame (+0x60)
└── QFrame* m_rightFrame (+0x68)
Behavior
- 0 views: Splitter hidden, empty container
- 1 view: View widget added directly to main layout, splitter hidden
- 2 views: First view in left frame, second in right frame, splitter visible
Important Notes
-
Direct Subclassing: Subclass
TULayoutViewdirectly - no need for complex multiple inheritance. -
widget() Returns this: The
widget()method must returnthis(cast to QWidget*) because the calling code treats it asTULayoutView*. -
getWidget() Returns Content: The
getWidget()method returns the actual displayable QWidget. -
TULayoutManager Access: Get the TULayoutManager via
PLUG_Services::getLayoutManager(). -
Memory Management: Views registered with
addAreaare managed by the layout system. Don't delete them manually.
Database Locations (HarmonyPremium.exe)
- TULayoutManager::addArea import:
0x140b22668 - TULayoutManager::raiseArea import:
0x140b22a18 - View creation function (example):
0x1400375C0(main session init)
Database Locations (ToonBoomLayout.dll)
- TULayoutView vtable:
0x180056f38 - TULayoutView constructor:
0x18002fc80 - TULayoutViewHolder constructor:
0x180031150 - TULayoutViewHolder::addView:
0x180031480 - TULayoutViewHolder::removeView:
0x180031620 - TULayoutViewHolder::nbViews:
0x180031610 - TULayoutViewHolder::splitterRatio:
0x180031890 - TULayoutViewHolder::updateWidgets:
0x1800319b0