![Peter Hartmann](/assets/img/avatar_default.png)
... by making the references absolute. Now they should work both on github and on the homepage. Resolves #414
315 lines
9.3 KiB
Plaintext
315 lines
9.3 KiB
Plaintext
---
|
||
title: 9. Writing own controls
|
||
layout: docs
|
||
---
|
||
|
||
:doctitle: 9. Writing own controls
|
||
:notitle:
|
||
|
||
== Writing own controls
|
||
|
||
Writing own controls is either done by subclassing or compositing an
|
||
existing displayable control like `QskTextLabel`, or by writing a
|
||
completely new class including a skinlet, which is typically derived
|
||
directly from `QskControl`.
|
||
|
||
=== Subclassing existing controls
|
||
|
||
Let’s say an app is displaying a text label with a specific style at
|
||
several different places, then it makes sense to subclass `QskTextLabel`
|
||
and set the needed properties like font size etc. in the derived class:
|
||
|
||
[source]
|
||
....
|
||
class TextLabel : public QskTextLabel
|
||
{
|
||
|
||
Q_OBJECT
|
||
|
||
public:
|
||
TextLabel( const QString& text, QQuickItem* parent = nullptr ) : QskTextLabel( text, parent )
|
||
{
|
||
setMargins( 15 );
|
||
setBackgroundColor( Qt::cyan );
|
||
}
|
||
};
|
||
....
|
||
|
||
.A subclassed control with local skin hints
|
||
image::/doc/tutorials/images/subclassing-existing-controls.png[Subclassing existing controls]
|
||
|
||
Then there is no need to set the margins and background color for every
|
||
instance of the custom text label.
|
||
|
||
=== Making custom classes skinnable
|
||
|
||
To make custom classes like the `TextLabel` class above skinnable, we
|
||
need to define our own subcontrols and style them in our skin, in
|
||
contrast to setting the values directly in the class. To be able to set
|
||
specific values for our `TextLabel` class that are different from the
|
||
generic `QskTextLabel`, we need to define our own subcontrols and
|
||
substitute the generic subcontrols for them in an overriden method
|
||
`effectiveSubcontrol()`:
|
||
|
||
[source]
|
||
....
|
||
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;
|
||
}
|
||
...
|
||
}
|
||
....
|
||
|
||
When the skinlet is drawing a `TextLabel` instance, it queries it for
|
||
its subcontrols through `effectiveSubcontrol()` in order to style them
|
||
properly. Now that we substitute the `QskTextLabel::Panel` for our
|
||
`TextLabel::Panel`, we can style it accordingly in our skin, so we don’t
|
||
need to set the local skin hints in the constructor of `TextLabel`
|
||
anymore.
|
||
|
||
[source]
|
||
....
|
||
class MySkin : public QskSkin
|
||
{
|
||
|
||
public:
|
||
MySkin( QObject* parent = nullptr ) : QskSkin( parent )
|
||
{
|
||
setGradient( TextLabel::Panel, Qt::cyan );
|
||
setMargins( TextLabel::Panel | QskAspect::Padding, 15 );
|
||
}
|
||
};
|
||
....
|
||
|
||
.A subclassed control with skin hints defined in the skin
|
||
image::/doc/tutorials/images/subclassing-existing-controls.png[Subclassing existing controls]
|
||
|
||
The styling described above has the same effect as in the simpler
|
||
example, but now the `TextLabel` control can be given a different style
|
||
depending on the skin.
|
||
|
||
In our class we only set a custom skin hint for the panel, but as
|
||
`QskTextLabel` also has a `Text` subcontrol, we could of course also
|
||
define our own one for the text.
|
||
|
||
=== Compositing controls
|
||
|
||
Controls can also be composited; e.g. when writing a class with a text
|
||
label on the left and a graphic on the right side, it could look like
|
||
this:
|
||
|
||
[source]
|
||
....
|
||
class TextAndGraphic : public QskLinearBox
|
||
{
|
||
|
||
Q_OBJECT
|
||
|
||
public:
|
||
TextAndGraphic( const QString& text, const QString& graphicName, QQuickItem* parent = nullptr )
|
||
: QskLinearBox( Qt::Horizontal, parent ),
|
||
m_textLabel( new QskTextLabel( text, this ) )
|
||
{
|
||
addItem( m_textLabel );
|
||
|
||
QImage image( QString( ":/images/%1.svg" ).arg( graphicName ) );
|
||
auto graphic = QskGraphic::fromImage( image );
|
||
|
||
m_graphicLabel = new QskGraphicLabel( graphic );
|
||
m_graphicLabel->setExplicitSizeHint( Qt::PreferredSize, { 30, 30 } );
|
||
addItem( m_graphicLabel );
|
||
|
||
setAutoLayoutChildren( true );
|
||
...
|
||
}
|
||
|
||
private:
|
||
QskTextLabel* m_textLabel;
|
||
QskGraphicLabel* m_graphicLabel;
|
||
};
|
||
....
|
||
|
||
This allows for easy instantiation of the class with a text and a file
|
||
name for the graphic:
|
||
|
||
[source]
|
||
....
|
||
auto* textAndGraphic = new TextAndGraphic( "Text", "cloud" );
|
||
....
|
||
|
||
.A composited control
|
||
image::/doc/tutorials/images/compositing-controls.png[Compositing controls]
|
||
|
||
=== Writing controls with a skinlet
|
||
|
||
QSkinny already comes with controls like text labels, list views,
|
||
buttons etc. When there is a completely new control to be written that
|
||
cannot be subclassed or composited, the skinlet for the class needs to
|
||
be implemented as well.
|
||
|
||
==== Writing the class
|
||
|
||
For demo purposes we create a class called `CustomShape` which shall
|
||
display an outer circle and an inner circle, with minimal API. There are
|
||
only 2 subcontrols that will be painted in the skinlet later:
|
||
|
||
[source]
|
||
....
|
||
class CustomShape : public QskControl
|
||
{
|
||
Q_OBJECT
|
||
|
||
public:
|
||
QSK_SUBCONTROLS( Panel, InnerShape )
|
||
|
||
CustomShape( QQuickItem* parent = nullptr ) : QskControl( parent )
|
||
{
|
||
}
|
||
};
|
||
....
|
||
|
||
==== Writing the skinlet
|
||
|
||
Writing the skinlet is the hard part of the work. We need the following
|
||
things in our skinlet:
|
||
|
||
* A definition of node roles. They typically correspond to subcontrols
|
||
from the control, so since in our case we have a subcontrol `Panel` and
|
||
`InnerShape`, there will be the node roles `PanelRole` and
|
||
`InnerShapeRole`. The node roles are often set in the constructor of the
|
||
class.
|
||
|
||
IMPORTANT: The constructor of the skinlet needs to be invokable!
|
||
|
||
[source]
|
||
....
|
||
class CustomShapeSkinlet : public QskSkinlet
|
||
{
|
||
Q_GADGET
|
||
|
||
public:
|
||
enum NodeRole
|
||
{
|
||
PanelRole, InnerShapeRole
|
||
};
|
||
|
||
Q_INVOKABLE CustomShapeSkinlet( QskSkin* skin = nullptr ) : QskSkinlet( skin )
|
||
{
|
||
setNodeRoles( { PanelRole, InnerShapeRole } );
|
||
}
|
||
....
|
||
|
||
* The enclosing rectangle for each subcontrol. This can be just the
|
||
`contentsRect`, but we can define it more accurately if we want by
|
||
applying some metrics. If the code below is hard to understand, the
|
||
important thing to take away from it is that different subcontrols can
|
||
have different enclosing rectangles.
|
||
|
||
[source]
|
||
....
|
||
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 );
|
||
....
|
||
|
||
* The code to actually draw the nodes. In our case of an outer circle
|
||
and an inner circle, the code for each subcontrol / node role is quite
|
||
similar. The method `updateSubNode()`, which is reimplemented from
|
||
`QQuickItem`, is called once for each node role. The code below again
|
||
might not be straight forward to understand, the gist of it is that for
|
||
each node role we draw a circle by creating a `BoxNode`.
|
||
|
||
[source]
|
||
....
|
||
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 );
|
||
}
|
||
};
|
||
....
|
||
|
||
==== Connecting class and skinlet
|
||
|
||
In our skin, we need to declare that the skinlet above will be
|
||
responsible of drawing our control via `declareSkinlet`. Also, we can
|
||
style our control with skin hints:
|
||
|
||
[source]
|
||
....
|
||
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 );
|
||
}
|
||
};
|
||
....
|
||
|
||
SkinFactories etc. are again omitted here. Finally we can draw our
|
||
control; the effort might seem excessive, but we wrote the control with
|
||
all capabilities of styling; in addition, the control will react to size
|
||
changes properly. A simpler version with hardcoded values for margins,
|
||
colors etc. can be written with less code.
|
||
|
||
.A class with an own skinlet
|
||
image::/doc/tutorials/images/control-with-skinlet.png[Control with skinlet]
|