QSkinny UI Framework

repository·master·Indexed 23 days ago

https://github.com/uwerat/qskinny

A high-performance, lightweight C++ UI framework built on the Qt scene graph. Designed for resource-constrained environments like automotive GUIs, QSkinny provides lightweight controls for C++ and QML applications. It focuses on high startup performance and a low memory footprint by using scene graph nodes directly. It supports Qt 5.15, Qt 6.8.x LTS, and current Qt versions, offering advanced layout control, C++ native tooling integration, and flexible styling.

Tokens
10.3K
Snippets
35
Records
46
Agent score
82%

What's inside QSkinny

  1. Overview of the QSkinny framework

    master

    QSkinny is a lightweight framework built on top of the Qt scene graph and a minimal set of core Qt/Quick classes. It provides a set of lightweight controls accessible via C++ and/or QML.

    Key characteristics include:

    • Performance: Designed for high startup performance and low memory footprint, making it suitable for automotive GUIs.
    • Architecture: Uses scene graph nodes directly rather than stacking heavy objects like QObject or QQuickItem, resulting in a 'skinny' implementation.
    • Separation of Concerns: The design separates the control API/logic, the styling, and the delegated rendering.
    • Platform Support: Intended to run on all platforms supported by Qt/Quick.

    Supported Qt Versions:

    • Qt 5.15
    • Current Long Term Support (LTS) version (e.g., Qt 6.8.x)
    • Current version of Qt
  2. Advantages of using QSkinny over QML

    master

    QSkinny is a C++-based framework for building UIs that offers several advantages over QML-based development:

    • C++ Native: Uses standard C++ syntax and paradigms, making it familiar to QtWidgets developers and allowing the use of standard C++ features like inheritance and overloading.
    • Tooling Integration: Since the UI is written in C++, you can use the full suite of C++ development tools (gdb, valgrind, address sanitizers, static analysis, code coverage, and auto-test frameworks) across your entire codebase, including the UI logic.
    • IDE Flexibility: Unlike QML, which often requires QtCreator for proper language support, QSkinny works with any C++ IDE (Visual Studio, Eclipse, CLion, etc.) and integrates easily with various build systems.
    • Simplified Data Binding: Avoids the heavy boilerplate of creating QAbstractListModel adapters or manually defining Q_PROPERTY, Q_INVOKABLE, and Q_SIGNAL for every model used in a QML UI. Data can be connected to the frontend using standard C++ functionality.
    • Advanced Layout Control: Provides fine-grained layout management using concepts like size hints, size policies, and stretch factors. This is particularly useful for UIs that must adapt to different screen sizes, language changes, or window resizing (e.g., when a virtual keyboard appears).
    • C++ Custom Controls: Unlike Qt Quick Controls 2, which limits custom type definitions to QML, QSkinny allows you to implement and style both built-in components (push buttons, sliders, etc.) and custom types directly in C++.
  3. Manage visual hierarchy using the Item Tree

    master

    The Item Tree controls how items are displayed and how they inherit visual properties. You can manage this hierarchy in two ways:

    1. Via Constructor: Passing a parent to the constructor (if the class inherits from QQuickItem) sets both the QObject parent and the parent item.
    2. Via addItem(): Calling addItem() on a container sets the parent item. If the item currently has no QObject parent, addItem() will also set the QObject parent.

    Visual Inheritance: Child items inherit properties like opacity and visibility from their parent item. For example, setting topBar->setOpacity(0.2) will make all children of topBar 20% opaque.

    // Explicitly adding an item to a parent item
    auto* topLabel1 = new QskTextLabel( "top bar label 1" );
    topBar->addItem( topLabel1 );
    auto* topLabel1 = new QskTextLabel( "top bar label 1" );
    topBar->addItem( topLabel1 );
  4. Maintain aspect ratio for graphics using QskSizePolicy

    master

    When using QskGraphicLabel, the scaling behavior is determined by the setSizePolicy. To maintain the correct aspect ratio of an SVG:

    • Scale based on height: Set the horizontal policy to QskSizePolicy::ConstrainedPreferred. The layout will then use QskGraphic::widthForHeight() to calculate the width.
    • Scale based on width: Set the vertical policy to QskSizePolicy::ConstrainedPreferred. The layout will then use QskGraphic::heightForWidth() to calculate the height.

    If you use non-scalable formats like PNG or JPG, the QskGraphic will still work, but they will not benefit from vector-based scaling.

  5. Understand Size Hints in QSkinny

    master

    Size hints inform the layout engine about the dimensions of UI elements and how they should respond to resizing.

    There are two ways to define a size hint:

    1. Implicit: Deduced automatically from the element's content (e.g., text width, font, padding, and margins).
    2. Explicit: Set manually by the developer using setExplicitSizeHint(). Explicit hints always take precedence over implicit ones.

    There are three types of size hints:

    • Minimum: The smallest size an element can be.
    • Preferred: The natural/ideal size of an element when space is sufficient.
    • Maximum: The largest size an element can be.

    Note: For atomic controls like QskPushButton, it is often better to use Size Policies rather than setting minimum/maximum size hints directly.

    // Using implicit size (default)
    auto* label1 = new QskTextLabel( "control 1" );
    label1->setMargins( 10 );
    
    // Setting an explicit preferred size
    label1->setExplicitSizeHint( Qt::PreferredSize, { 150, 60 } );
  6. How skins, skin hints, and skinlets work together

    master

    The QSkinny styling architecture relies on three main components:

    1. Skins (QskSkin): Define the global look and feel. They act as containers for Skin Hints.
    2. Skin Hints: The actual property values (colors, margins, fonts, etc.) stored in the skin. They are categorized by the primitives they affect:
      • Text: Alignment, Color, TextColor, StyleColor, LinkColor, Style, FontRole.
      • Graphic: Alignment, GraphicRole.
      • Box: Margin + Metric, Border + Color, Border + Color + Metric, Shape.
    3. Skinlets: The drawing logic (similar to QML Delegates). A skinlet queries both the control (for unique data like text) and the skin (for shared hints like background color) to render the UI.

    This separation allows you to change the entire visual representation of an application by simply swapping the skin, without changing the application logic or the control instances themselves.

  7. Understand scene graph representations of controls

    master

    In QSkinny, every UI control is composed of one or more scene graph nodes. These nodes represent basic shapes (like rectangles) or provide properties like positioning (transform nodes), opacity, or clipping.

    When you create a control, QSkinny builds a hierarchy of nodes. For example, a simple QskPushButton typically consists of:

    • A root button node
    • A grouping node for children
    • A panel node (geometry node) for the background
    • A transform node for text positioning
    • A text node (geometry node) for the text display

    As you nest controls within layouts (like QskBox), the scene graph reflects this hierarchy: the layout's node becomes a parent to the control's node. QSkinny is optimized to minimize the number of nodes created and maximize node reuse to maintain performance in complex UIs.

    auto* button = new QskPushButton( "button" );
    
    QskWindow window;
    window.addItem( button );
    window.show();
  8. Manage object lifetime using the Object Tree

    master

    To ensure proper memory management, pass the QObject parent to the constructor of your controls. This establishes a parent-child relationship in the Object Tree, meaning the child will be automatically deleted when the parent is destroyed.

    Example of setting a parent via constructor:

    // topBar is the QObject parent of topLabel1
    auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar );
    auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar );
  9. Understand the three hierarchies in QSkinny

    master

    When building applications with QSkinny, you are managing three distinct but related hierarchies. Understanding the difference between them is critical for managing memory and visual behavior:

    1. The Object Tree (QObject): Manages the lifetime of objects. If a parent QObject is deleted, all its children are automatically deleted. This is used for memory management.
    2. The Item Tree (QQuickItem): Manages the visual hierarchy. It determines how items are rendered, positioned, and how they inherit visual properties like visibility and opacity. An item's position and visual state depend on its parent item.
    3. The Scene Graph: A low-level representation of graphic primitives (rectangles, textures, text) used by the backend (like OpenGL) for efficient rendering.

    Note: While the Object Tree and Item Tree are often identical, they can diverge. If they diverge, deleting an object in the Object Tree might unexpectedly delete a visual item that has been moved elsewhere in the Item Tree.

  10. Make custom classes skinnable via subcontrol overriding

    master

    To allow a subclass to be styled differently by different skins, you should avoid hardcoding values in the constructor. Instead, define custom subcontrols using the QSK_SUBCONTROLS macro and override the effectiveSubcontrol() method. This tells the skinning engine to use your custom subcontrol identifier instead of the base class's identifier when styling your specific class.

    class TextLabel : public QskTextLabel
    {
        QSK_SUBCONTROLS( Panel )
    
    TextLabel( const QString& text, QQuickItem* parent = nullptr ) : QskTextLabel( text, parent )
        {
        }
    
    QskAspect::Subcontrol effectiveSubcontrol( QskAspect::Subcontrol subControl ) const override final
        {
            if ( subControl == QskTextLabel::Panel )
                return TextLabel::Panel;
    
    return subControl;
        }
        ...
    }
  11. Write a completely new control with a skinlet

    master

    For entirely new visual components that cannot be achieved via subclassing or composition, you must implement both a control class (derived from QskControl) and a corresponding QskSkinlet.

    1. Define the Control Class

    Use QSK_SUBCONTROLS to define the logical parts of your control that need styling.

    class CustomShape : public QskControl
    {
        Q_OBJECT
    
    public:
        QSK_SUBCONTROLS( Panel, InnerShape )
    
    CustomShape( QQuickItem* parent = nullptr ) : QskControl( parent )
        {
        }
    };

    2. Implement the Skinlet

    A skinlet handles the actual rendering. It requires three main components:

    • Node Roles: Define an enum of roles that correspond to your subcontrols. These are passed to setNodeRoles() in the skinlet constructor. The constructor must be Q_INVOKABLE.
    • Subcontrol Rectangles: Override subControlRect() to define the bounding box for each subcontrol. You can use contentsRect or apply custom margins.
    • Drawing Logic: Override updateSubNode() to perform the actual rendering for each node role. This is where you manipulate QSGNode objects (like QskBoxNode).
    class CustomShapeSkinlet : public QskSkinlet
    {
        Q_GADGET
    
    public:
        enum NodeRole
        {
            PanelRole, InnerShapeRole
        };
    
    Q_INVOKABLE CustomShapeSkinlet( QskSkin* skin = nullptr ) : QskSkinlet( skin )
        {
            setNodeRoles( { PanelRole, InnerShapeRole } );
        }
    
        QRectF subControlRect( const QskSkinnable* skinnable, const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const override
        {
            const auto* customShape = static_cast< const CustomShape* >( skinnable );
    
            if ( subControl == CustomShape::Panel )
            {
                return contentsRect;
            }
            else if ( subControl == CustomShape::InnerShape )
            {
                const auto margins = customShape->marginsHint( CustomShape::InnerShape );
                return contentsRect.marginsRemoved( margins );
            }
    
            return QskSkinlet::subControlRect( skinnable, contentsRect, subControl );
        }
    
    protected:
        QSGNode* updateSubNode( const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const override
        {
            const auto* customShape = static_cast< const CustomShape* >( skinnable );
    
            switch ( nodeRole )
            {
                case PanelRole:
                {
                    auto panelNode = static_cast< QskBoxNode* >( node );
                    const auto panelRect = subControlRect( customShape, customShape->contentsRect(), CustomShape::Panel );
                    const qreal radius = panelRect.width() / 2;
                    panelNode->setBoxData( panelRect, shapeMetrics, borderMetrics, borderColors, gradient );
                    return panelNode;
                }
                case InnerShapeRole:
                {
                    auto innerNode = static_cast< QskBoxNode* >( node );
                    const auto innerRect = subControlRect( customShape, customShape->contentsRect(), CustomShape::InnerShape );
                    const qreal radius = innerRect.width() / 2;
                    innerNode->setBoxData( innerRect, shapeMetrics, borderMetrics, borderColors, gradient );
                    return innerNode;
                }
            }
    
            return QskSkinlet::updateSubNode( skinnable, nodeRole, node );
        }
    };

    3. Connect the Control and Skinlet in a Skin

    Use declareSkinlet<ControlClass, SkinletClass>() within your QskSkin to link them, then apply styles using the control's subcontrol identifiers.

    class MySkin : public QskSkin
    {
    
    public:
        MySkin( QObject* parent = nullptr ) : QskSkin( parent )
        {
           declareSkinlet< CustomShape, CustomShapeSkinlet >();
    
    setGradient( CustomShape::Panel, Qt::blue );
           setMargins( CustomShape::InnerShape, 20 );
           setGradient( CustomShape::InnerShape, Qt::magenta );
        }
    };