Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Componentized Trading UI Framework - Part 1: Tab Drag-and-Drop, Addition/Deletion, and Widget Support

Tech May 10 4

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
  1. 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.

  1. 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 TemplateLayout instance 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 SubWindow instance
  • SubWindow instances 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 TemplateLayout instances
  • Widget windows maintain a one-to-one relationship with their parent TemplateLayout and support synchronized movement—moving the main window repositiones all associated widgets
  1. 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:

  1. Install the event filter on each tab button, targeting the parent widget
  2. Override the parent's eventFilter method 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:

  1. Hide the placeholder indicator
  2. Remove the corresponding content panel
  3. 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.

Tags: qt

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.