Building a Componentized Trading UI Framework - Part 1: Tab Drag-and-Drop, Addition/Deletion, and Widget Support
Table of Contents
- Overview
- Demo Preview
- Implementation Analysis
- Phase 1: Core Framework
- Toolbox-window synchronization
- Tab drag-and-drop mechanism
- Phase 2: Multi-window Drag-and-Drop
- Phase 3: Code Refactoring
- Phase 1: Core Framework
- Overview
It's been a while since I worked on UI features tied to business requirements, and I haven't implemented many flashy interactive effects recently. Over the past two days, I've created a simplified componentized UI framework inspired by Futu NiuNiu's trading interface. While this is just an initial implementation without styling or visual polish, the fundamental functionality is in place. This article marks the beginning of a series on componentized UI architecture, with more articles planned that will progressively add advanced features.
Beyond basic componentization, I'll also be covering advanced Futu NiuNiu features including candlestick charts, time-sharing charts, multi-day views, technical indicators, and watchlist functionality.
I've already developed complete implementations for watchlists and K-line charts. These are available as reusable controls that can be customized for specific requirements.
- Demo Preview
A visual demonstration communicates more than extensive descriptions. The following image shows a basic demo of the componentized UI framework. Although it lacks real market data, all fundamental interactive flows are implemented.
Key Features Demonstrated:
- The main window (
TemplateLayout) serves as a customizable tab container where tabs can be dragged and reordered within the componentized page - When a tab is dragged outside its parent component, a new
TemplateLayoutinstance is automatically created - New component pages support movable tab bars (the main component page has fixed positioning per requirements)
- The leftmost button on the tab bar opens a toolbox dialog
- The toolbox contains various mini-widgets; clicking them opens a
SubWindowinstance SubWindowinstances support selection highlighting- Widgets are scoped to their parent tab; switching tabs hides all widgets created under the previous tab
- Tabs can be dragged between different
TemplateLayoutinstances - Widget windows maintain a one-to-one relationship with their parent
TemplateLayoutand support synchronized movement—moving the main window repositiones all associated widgets
- Implementation Analysis
Developing this demo required approximately two days, with some structural changes to the code during iteration. The following sections detail the evolution of the implementation.
3.1 Phase 1: Core Framework
The initial phase focused on implementing a single main componentized interface. The architecture divides the system into several key components: the main Frame, top toolbar, draggable tab widget, central content area, toolbox dialog, and sub-windows.
Two primary challenges emerged during this phase:
- Synchronizing toolbox position with the main window
- Implementing tab drag-and-drop functionality
3.1.1 Toolbox-Main Window Synchronization
This problem required considerable thought before reaching a solution.
My initial approach involved overriding the parent window's moveEvent handler to transmit position deltas to the toolbox. However, implementation revealed that the toolbox lagged behind during movement—moveEvent apparently optimizes some call sequences, causing synchronization issues.
After stepping away for a break, a different approach emerged: instead of transmitting position changes to the toolbox, simply notify the toolbox that the parent window has moved. The toolbox can then reposition itself relative to the parent, maintaining their relative positioning.
The solution: The toolbox tracks its relative position to the parent window and updates this offset during its own movements. When the parent window moves, the toolbox computes its new absolute position based on the stored relative offset.
void ToolboxDialog::SyncWithParent()
{
if (QWidget* parent = parentWidget())
{
QPoint globalPos = parent->mapToGlobal(m_relativeOffset);
move(globalPos);
}
}
3.1.2 Tab Drag-and-Drop Implementation
Having implemented drag-and-drop functionality before, I approached this systematically. Qt provides native drag-and-drop APIs, but they offer limited customization. For a polished visual appearance and rich interactive features, a custom implementation was necessary.
Core Strategy: Filter and handle three mouse events to simulate drag behavior: mouse press, mouse move, and mouse release.
To intercept these events across all tab buttons, I utilized their parent container (DragTabWidget). Qt's event propagation model allows intercepting child events through event filters installed on parent widgets. The implementation follows this two-step process:
- Install the event filter on each tab button, targeting the parent widget
- Override the parent's
eventFiltermethod to handle intercepted events
// Install filter on each tab button
tabButton->installEventFilter(this);
// Override eventFilter in parent class
bool DragTabWidget::eventFilter(QObject* watched, QEvent* event)
{
TabButton* button = dynamic_cast<TabButton*>(watched);
if (button && m_buttonRegistry.contains(button->GetID()))
{
// Handle filtered events
}
return QObject::eventFilter(watched, event);
}
Mouse Press Handling
When a mouse button is pressed on a tab, record the cursor position for subsequent drag calculations:
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
m_initialGlobalPos = mouseEvent->globalPos();
m_initialButtonPos = button->pos();
}
Mouse Move Handling
Mouse movement during a drag operation involves two scenarios:
Scenario A: Moving within the tab bar
Relatively straightforward—update positions of both the dragged tab and a "placeholder" indicator that shows where the tab will land when released. A visual optimization involves rendering a screenshot of the tab button following the cursor, which reduces complexity and minimizes potential bugs.
Scenario B: Dragging outside the tab bar
When the cursor exits the tab bar while dragging, the tab should detach from its parent component:
- Hide the placeholder indicator
- Remove the corresponding content panel
- Make the panel follow the cursor, positioned below the dragged tab
if (event->type() == QEvent::MouseMove)
{
if (IsDraggingWithinTabBar())
{
// Update positions within tab bar
UpdateDragPosition(button);
UpdatePlaceholderPosition(button);
}
else
{
// Detach from current layout and follow cursor
DetachFromLayout(button);
}
}
Mouse Release Handling
Releasing the mouse after dragging results in one of three outcomes:
- Reordering within the original tab bar
- Creating a new componentized window
- Moving to an existing componentized window
if (event->type() == QEvent::MouseButtonRelease)
{
if (IsDraggingWithinTabBar())
{
// Snap to placeholder position
FinalizeDragPlacement();
}
else
{
// Create new independent window
emit TabDraggedOut(button->GetID());
// Disable close button if only one tab remains
if (m_buttonRegistry.size() == 1)
{
m_buttonRegistry.values().first()->SetCloseEnabled(false);
}
}
}
The initial implementation supported only the first two scenarios. Since all button events were processed by their respective DragTabWidget instances, cross-widget tab dragging wasn't possible. This limitation prompted a significant refactor.
Phase 1 Limitations and Refactoring Motivation
The Phase 1 implementation couldn't support dragging tabs between different tab bars because each DragTabWidget processed events independently through its own event loop. This architectural constraint necessitated a centralized event handling approach.`
3.2 Phase 2: Multi-Window Drag-and-Drop Support
To enable tab dragging between different component windows, a third-party manager class (TabMoveManager) was introduced. This singleton handles all tab-related mouse events uniformly, regardless of which DragTabWidget owns the tab.`
bool TabMoveManager::eventFilter(QObject* watched, QEvent* event)
{
TabButton* button = dynamic_cast<TabButton*>(watched);
if (button && m_tabRegistry.contains(button->GetID()))
{
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
m_startGlobalPos = mouseEvent->globalPos();
m_startButtonPos = button->mapToParent(QPoint(0, 0));
// Initialize drag state for the selected tab
InitializeDragOperation(button);
}
else if (event->type() == QEvent::MouseMove)
{
if (IsDragActive())
{
// Move within current tab bar
UpdateDragPosition(button);
UpdatePlaceholder(button);
}
else
{
// Dragging outside current layout
DetachFromLayout(button);
}
}
else if (event->type() == QEvent::MouseButtonRelease)
{
if (IsDragActive())
{
// Snap to placeholder position
CompleteDragPlacement();
}
else
{
// Released in empty space - create new TemplateLayout
emit RequestNewLayout(button->GetID());
}
// Clean up source layout if different from target
if (m_sourceLayout != m_targetLayout && m_targetLayout != nullptr)
{
m_sourceLayout->RemoveTab(button->GetID());
}
// Disable close button if only one tab remains
if (m_sourceTabWidget->GetTabCount() == 1)
{
m_sourceTabWidget->GetCloseButton()->SetEnabled(false);
}
// ... additional cleanup
}
}
return QObject::eventFilter(watched, event);
}
Beyond event handling, TabMoveManager serves as a registry for all content panels. Other system components can query panels by their registered identifiers:
void RegisterPanel(const QString& panelId, ContentPanel* panel);
void UnregisterPanel(const QString& panelId);
ContentPanel* GetPanel(const QString& panelId) const;
3.3 Phase 3: Code Refactoring
With core functionality operational, Phase 3 focused on improving code organization and clarifying component responsibilities. The final architecture includes the following classes:
- ContentPanel: Central content container holding multiple sub-panels, each corresponding to a tab. Manages memory for window references but doesn't handle window lifecycle operations.
- DragTabWidget: Container for tab buttons, supports moving the entire componentized window.
- DragToolBar: Toolbar containing the toolbox launcher button and DragTabWidget.
- MiniWidget: Floating widget window supporting various mini-tool pages.
- TabButton: Individual tab representation.
- TabMoveManager: Singleton responsible for coordinating all tab drag events.
- TemplateLayout: Main componentized window container.
- ToolboxDialog: Tool palette dialog.
- Constants: Header file containing shared definitions and constants.
This modular architecture separates concerns effectively, making the system extensible and maintainable. Each component has clearly defined responsibilities, facilitating future enhancements and bug fixes.
The code examples provided illustrate the core implementation patterns. Due to the demo's scope and ongoing development, not all source files are included here. The fundamental algorithms and design patterns, however, are fully documented above.