diff --git a/doc/tutorials/01-What-is-QSkinny.asciidoc b/doc/tutorials/01-What-is-QSkinny.asciidoc new file mode 100644 index 00000000..f4980c21 --- /dev/null +++ b/doc/tutorials/01-What-is-QSkinny.asciidoc @@ -0,0 +1,21 @@ +--- +title: 1. What is QSkinny? +layout: docs +--- + +:doctitle: 1. What is QSkinny? +:notitle: + +QSkinny is a UI framework based on the Qt graphic stack and written in +{cpp}. It allows users to write their UIs in {cpp} and/or QML. + +.The Fendt Tractor GUI +image::https://camo.githubusercontent.com/3eea80daf41ce6a86f08c73353d05000363c4df0/68747470733a2f2f7777772e66656e64742e636f6d2f696e742f67656e6576612d6173736574732f7769646765742f32383239312f6e6577732d332d6c6f772e6a7067[Fendt Tractor GUI] + +It is currently being used in the Fendt Tractor GUI project, see the +picture above. For the Fendt Tractor GUI there is no QML used at all; +the whole codebase is written in {cpp}. An overview of how QSkinny fits +into the Qt architecture is depicted below: + +.QSkinny sits on top of QtQuick, while QML is optional +image::../images/architecture-simple.png[QSkinny architecture] diff --git a/doc/tutorials/02-Why-QSkinny.asciidoc b/doc/tutorials/02-Why-QSkinny.asciidoc new file mode 100644 index 00000000..f743b871 --- /dev/null +++ b/doc/tutorials/02-Why-QSkinny.asciidoc @@ -0,0 +1,130 @@ +--- +title: 2. Why QSkinny? +layout: docs +--- + +:doctitle: 2. Why QSkinny? +:notitle: + +The typical questions about QSkinny are: Why was QSkinny created? And why would +somebody use QSkinny and not QML? + +Which technology to use always depends on the specific use case. However, +QSkinny does have some advantages: + +== 1. It's {cpp} + +QSkinny is written in {cpp}, so there is no new syntax or programming paradigm +to learn as is the case with QML. Of course QSkinny has concepts that +new programmers need to become familiar with, but they should be understandable +for people who know {cpp}. Especially programmers experienced with +QtWidgets should feel comfortable with QSkinny right away. + +=== 1.1 Integration with other build systems / IDEs + +While QtCreator is the natural choice of *IDE* for Qt programmers, +some people prefer other IDEs, e.g. Visual +Studio (Code), Eclipse, CLion etc. Such IDEs usually don't have language support +for QML like type completion and other features. So when using QML you are +either bound to using QtCreator, or use another IDE and live with the fact that +the IDE will not understand QML. + +When it comes to *build systems*, some QML tools might be hard to integrate: +For instance in Visual Studio projects it is difficult to invoke the QML +compiler through the build system. + +With QSkinny being written completely in {cpp}, it can be used with any IDE and +should integrate nicely with other build systems. QSkinny is using Qt-specific +concepts like signals and slots and invokable methods though. + +=== 1.2 Use {cpp} tooling for your whole codebase + +{cpp} has extensive tooling that assists with writing code, for instance: + +- gdb and other debuggers +- valgrind +- address sanitizer and other sanitizers +- static code analysis tools +- code coverage tools (e.g. gcov) +- auto test frameworks +- (a lot more, e.g. clang tools) + +E.g. QtCreator will let you know about potential problems in your code while +you type, e.g. "unused variable", "calling a virtual method from the constructor +of a class" etc., and it might even suggest an automatic fix for it. + +QML does have some tooling, but its feature set is nowhere near the support of +{cpp}. + +When writing your whole codebase in {cpp} with QSkinny, the tooling can be used +for the whole codebase, so also UI code can be debugged, auto tested for a +CI system, and so on. + +In addition, {cpp} has concepts that QML as a declarative language doesn't, +like inheritance and overloading. This makes it easier to implement concepts +like event handling, see <> below. + + +== 2. Easy data binding + +When displaying data from a backend in a QML UI, that data needs to be in a +certain format: It needs to be made readable by Qt's Meta Object system via +`Q_PROPERTY`, `Q_INVOKABLE`, `Q_SIGNAL` and others. + +Also, for each model that is used in QML there typically needs to be one +subclass of `QAbstractListModel`, which serves as an adapter class. The process +of subclassing and implementing virtual methods can be cumbersome, and lead to +lots of boilerplate code. + +QSkinny doesn't need any adaptation layer per se, the data just needs to be +connected to the frontend with standard {cpp} functionality. Of course classes +like the aforementioned `QAbstractListModel` can be used when it makes sense, +but this is up to the user. + + +== 3. Layouts + +Whe it comes to *layouts*, QSkinny has a complete concept of laying out the UI, +or in other words: The user can determine in a fine-grained way what happens +when there is too little or too much space available. +Concepts like size hints, size policies, stretch factors and others are concepts +that were already available in QtWidgets and Qt's Graphics View Framework, and +are now supported in QSkinny. + +Why are layouts important? QML was created under the premise that in contrast to +desktop UIs, embedded UIs run as fullscreen window on an embedded board and +thus size changes will rarely happen. + +This is true for many cases, however layout code gets important when one of the +following events happen: + +- The UI needs to run on two or more screen sizes +- Language or style changes need to be supported +- The window is resized, e.g. when the Android virtual keyboard pops up + +QSkinny allows the user to take the above use cases into account, but doesn't +force the developer to write overly complex code: A UI written with QSkinny can +be coded with fixed sizes for UI elements, as it is typically done in QML. + + +== [[Styling]] 4. Styling / Adding custom controls + +Qt Quick Controls 2 support different styles, and it even comes with several +built-in styles like a Google Material style and a Microsoft Universal style. + +One drawback with Qt Quick Controls 2 is that application developers can only +add custom types in QML, not in {cpp}. This makes it cumbersome for concepts +like event handling, as is noted in the Qt documentation: +https://doc.qt.io/qt-5/qtquickcontrols2-differences.html[Differences with Qt Quick Controls 1,role=external,window=_blank]. + +So an application developer who wants to add own types, as is common for medium +to large-scale projects, will have to implement these custom types in QML. +Since being able to use {cpp} for application logic of components seems to have been +one reason to create Qt Quick Controls 2 (another reason being performance +issues with Qt Quick Controls 1, see +https://www.qt.io/blog/2015/03/31/qt-quick-controls-for-embedded[Qt Quick Controls for Embedded,role=external,window=_blank]), allowing the user to write controls in {cpp} gives the user more flexibility. + +QSkinny allows for implementing custom types in {cpp}; also both built-in +components like push buttons, sliders etc. as well as custom types can be easily +styled from {cpp}. The latter can be achieved by simply adding style +descriptions in user code. diff --git a/doc/tutorials/03-writing-your-first-application.asciidoc b/doc/tutorials/03-writing-your-first-application.asciidoc new file mode 100644 index 00000000..0c982e3e --- /dev/null +++ b/doc/tutorials/03-writing-your-first-application.asciidoc @@ -0,0 +1,126 @@ +--- +title: 3. Writing your first application +layout: docs +--- + +:doctitle: 3. Writing your first application +:notitle: + +== Writing your first application + +=== Building the QSkinny repository + +In this chapter we will write a simple QSkinny application on Linux from scratch. As a prerequisite, a recent Qt version (>= 5.6) should be available and the directory of its `qmake` binary in the current `$PATH`. The we can build the QSkinny repository with the following commands: + +[source,xml] +.... +cd /home/user/dev/ +git clone https://github.com/uwerat/qskinny.git +cd qskinny +qmake +make +.... + +This will produce the libraries `libqskinny.so` and others in the `qskinny/lib` directory. Optionally we could install the libraries to `/usr/local` via `make install`; for now we will use the ones from the local build at `/home/user/dev/qskinny/lib`. If you checked out the repository in another directory, you will have to adapt the include and library paths used below. + +=== Compiling our first app + +As a next step, we need to write our app. Let's start with a simple `main.cpp` file in a directory `myapp`: + +.main.cpp +[source] +.... +#include +#include + +#include + +int main( int argc, char* argv[] ) +{ + QGuiApplication app( argc, argv ); + + SkinnyFont::init( &app ); + + QskWindow window; + window.show(); + + return app.exec(); +} +.... + +For now this will just create an empty window (the `QskWindow`) without any controls. Next, we need to create a `myapp.pro` file in our `myapp` directory: + +.myapp.pro +[source,xml] +.... +TEMPLATE = app +TARGET = myapp + +INCLUDEPATH += /home/user/dev/qskinny/support \ + /home/user/dev/qskinny/src/common \ + /home/user/dev/qskinny/src/controls \ + /home/peter/temp/qskinny/src/layouts + +LIBS += -L/home/user/dev/qskinny/lib -lqskinny -lqsktestsupport + +SOURCES += \ + main.cpp +.... + +Now we can compile our app: + +[source,xml] +.... +cd myapp +qmake +make +.... + +When running the app we will have to supply the `LD_LIBRARY_PATH`: + +[source,xml] +.... +LD_LIBRARY_PATH=/home/user/dev/qskinny/lib ./myapp +.... + +This should show just an empty window. + +=== Adding UI controls + +Now that we have our app running, we can add some UI controls to it by extending the `main.cpp` file we created earlier. We will add some additional include directives, and then create a horizontal layout containing two push buttons. The layout with the two buttons will be shown in the window. Below is the complete updated source file: + +.main.cpp +[source] +.... +#include +#include +#include +#include + +#include + +int main( int argc, char* argv[] ) +{ + QGuiApplication app( argc, argv ); + + SkinnyFont::init( &app ); + + auto* horizontalBox = new QskLinearBox( Qt::Horizontal ); + auto* button1 = new QskPushButton( "button 1", horizontalBox ); + auto* button2 = new QskPushButton( "button 2", horizontalBox ); + + QskWindow window; + window.addItem( horizontalBox ); + window.show(); + + return app.exec(); +} +.... + +Now the app is displaying the two buttons: + +image::../images/writing-first-application.png[An app showing two buttons] + +That's it; you just created a QSkinny application from scratch. + +For information on how the controls and layouts above behave, see the next chapters. diff --git a/doc/tutorials/04-Layouts.asciidoc b/doc/tutorials/04-Layouts.asciidoc new file mode 100644 index 00000000..6ecc84fd --- /dev/null +++ b/doc/tutorials/04-Layouts.asciidoc @@ -0,0 +1,436 @@ +--- +title: 4. Layouts +layout: docs +--- + +:doctitle: 4. Layouts +:notitle: + +== Layouts + +Layouts manage the position of UI elements on the screen, and how the +elements react to size changes (e.g. window resize). + +=== Size hints + +Size hints let the layouting code know how big UI elements are, and to +which size they may shrink or grow. + +Size hints can be explicit or implicit. Explicit sizes are set by the +user via an API call through `setExplicitSizeHint()` ("This element is +of that size''), while implicit sizes are deduced from the elements +themselves. Explicit size hints always take precedence over implicit +ones. + +For instance, the implicit size of a button is calculated from the +text width (which itself depends on the font) and possibly padding and +margins: + +.implicit horizontal size hint of a button +image::../images/size-hints-calculation.png[implicit horizontal size hint of a button] + +The implicit width of a composited UI element containing a +graphic on the left and a text on the right would be the sum of the elements’ +width, again with padding and margins. + +Layouts, i.e. classes deriving from `QskBox`, are also controls +(i.e. `QskControl` instances), so they also have size hints. A layout +typically calculates its implicit size hint by summing up the size of +its children. For instace a horizontal layout containing three buttons +next to each other will calculate its implicit width by summing up the +widths of the buttons (spacing and margins again come on top). + +There are three types of size hints: *Minimum*, *Preferred* and +*Maximum*. + +* The *minimum size hint* of a UI element is used by layouting code to +determine how small an element can be. +* The *preferred size hint* is the natural size of an element, and will +be used in an ideal case, meaning there is enough space available. +* The *maximum size hint* is used by layouting code to determine how big +an element can be. + +Minimum and maximum size hints of atomic controls like `QskPushButton` +or `QskTextLabel` are typically not used, instead size policies are used +to express how small or big a component can be (see next topic). +Minimum and maximum sizes, i.e. the methods `minimumSize()` and +`maximumSize()`, are typically used for layouts though. + +So in total, a control can have up to 6 size hints: the three types +described above, and each one can have an implicit and an explicit hint. + +==== Example + +Below is an image with an implicit size hint with a width of 91 pixels +and a height of 39 pixels (91x39). The hint is determined by the size of +the text (71x19 pixels) plus margins (10 pixels each for top, right, +bottom, left). We don’t need to set a size hint explicitly, the control +will be rendered correctly with the implicit size hint: + +[source] +.... +auto* label1 = new QskTextLabel( "control 1" ); +label1->setMargins( 10 ); +label1->setBackgroundColor( Qt::magenta ); +.... + +.control without explicit size hint +image::../images/size-hints-1.png[Image without explicit size hint] + +If we set an explicit size hint of 150x60 pixels ourselves for the +preferred size, the control will be rendered differently: + +.... +label1->setExplicitSizeHint( Qt::PreferredSize, { 150, 60 } ); +.... + +.control with explicit size hint +image::../images/size-hints-2.png[Image with explicit size hint] + +When dealing with standard controls or layouts, the size hints don’t +need to be specified explicitly, as it can be deduced from its standard +values, as seen in the example above. + +The actual size of a UI element also depends on its size policy, see the +next topic. + +=== Size policies + +Size policies define the way UI elements can change their size depending +on the available space. Imagine a UI with a top bar and a main content +area: When a status bar at the bottom is to be faded in, the top bar and +main content have less space to display. One way to deal with this would +be to leave the top bar at the same size and shrink the main area. This +can be achieved with size policies: The top bar would have a vertical +size policy of `Fixed`, while the main area would be `Preferred`, +meaning it can grow and shrink. + +The size policies of QSkinny correspond to the +*https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum[size policies from +QtWidgets]*: + +[width="100%",cols="50%,50%",options="header",] +|======================================================================= +|`QskSizePolicy::Policy` |description +|`Fixed` |The control has a fixed size and can neither grow nor shrink. + +|`Minimum` |The control cannot shrink beyond its minimum size, but it +can grow if needed. + +|`Maximum` |The control cannot grow beyond its maximum size, but it can +shrink if needed. + +|`Preferred` |The control can grow and shrink, but it should be of the +size given by `sizeHint()`. + +|`MinimumExpanding` |The control cannot shrink beyond its minimum size, +but it can grow and should get as much space as possible. + +|`Expanding` |The control can shrink and grow, and it should get as much +space as possible. + +|`Ignored` |The `sizeHint()` is ignored, and the control will get as +much space as possible. + +|`Constrained` |The size of the control depends on a constraint, +i.e. the width is depending on the height or vice versa. For this policy +and the other `Constrained*` ones below, `QskControl::widthForHeight()` +or `QskControl::heightForWidth()` will be queried. + +|`ConstrainedMinimum` |The size of the control depends on a constraint, +but it can grow if needed. + +|`ConstrainedMaximum` |The size of the control depends on a constraint, +but it can shrink if needed. + +|`ConstrainedPreferred` |The size of the control depends on a +constraint, but it can grow and srhink if needed. + +|`ConstrainedMinimumExpanding` |The size of the control depends on a +constraint, but it can grow and should get as much space as possible. + +|`ConstrainedExpanding` |The size of the control depends on a +constraint, and it should get as much space as possible. +|======================================================================= + +All the `Constrained*` policies correspond to Qt’s +https://doc.qt.io/qt-5/qsizepolicy.html#hasHeightForWidth[QSizePolicy::hasHeightForWidth()] +or +https://doc.qt.io/qt-5/qsizepolicy.html#hasWidthForHeight[QSizePolicy::hasWidthForHeight()] +flag. E.g. if a control has a horizontal size policy of `Constrained` +and a vertical size policy of `Fixed`, it will call `widthForHeight()` +to determine the width that corresponds to the height. + +==== Example + +Below is an example of two buttons with different size policies. In this +case only the horizontal size policies are considered; the vertical size +policies behave correspondingly. + +[source] +.... +auto horizontalBox = new QskLinearBox( Qt::Horizontal ); + +auto* label1 = new QskTextLabel( "size policy: fixed" ); +label1->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); +horizontalBox->addItem( label1 ); + +auto* label2 = new QskTextLabel( "size policy: minimum" ); +label2->setSizePolicy( Qt::Horizontal, QskSizePolicy::Minimum ); +horizontalBox->addItem( label2 ); +... +.... + +By default the width of the buttons is determined by its text plus its +margins: + +.Size policies with preferred size +image::../images/size-policies-horizontal-minimum-1.png[Fixed vs. Minimum size policy] + +After growing the window horizontally, the button with the Fixed +horizontal size policy keeps its width, while the button with the +Minimum policy will grow: + +.Size policies when increasing window width +image::../images/size-policies-horizontal-minimum-2.png[Fixed vs. Minimum size policy] + +When shrinking the window below its original size, both buttons stay +with their width: The one on the left because of its `Fixed` size policy, +and the one on the right because it won’t shrink below its original size +due to the `Minimum` size policy. + +.Size policies when shrinking window width +image::../images/size-policies-horizontal-minimum-3.png[Fixed vs. Minimum size policy] + +If we change the policy of the right button to `Preferred`, it will shrink +below its original size (even though the text is too wide now): + +.... +label2->setSizePolicy( Qt::Horizontal, QskSizePolicy::Preferred ); +label2->setText( "size policy: preferred" ); +.... + +.Size policies when changing to preferred size policy +image::../images/size-policies-horizontal-minimum-4.png[Fixed vs. Minimum size policy] + +=== Types of layouts + +There are different types of layouts that can group UI elements +together. Internally, layouts use the `layoutRect()` method to determine +the available space to place its children. + +==== Linear layouts (QskLinearBox) + +A linear layout can group elements either horizontally or vertically, as +in the images below. + +[source] +.... +auto horizontalBox = new QskLinearBox( Qt::Horizontal ); + +auto* label1 = new QskTextLabel( "control 1" ); +horizontalBox->addItem( label1 ); + +auto* label2 = new QskTextLabel( "control 2" ); +horizontalBox->addItem( label2 ); + +auto* label3 = new QskTextLabel( "control 3" ); +horizontalBox->addItem( label3 ); +... +.... + +.Horizontal layout +image::../images/layout-horizontal.png[Horizontal layout] + +[source] +.... +auto verticalBox = new QskLinearBox( Qt::Vertical ); + +auto* label1 = new QskTextLabel( "control 1" ); +verticalBox->addItem( label1 ); + +auto* label2 = new QskTextLabel( "control 2" ); +verticalBox->addItem( label2 ); + +auto* label3 = new QskTextLabel( "control 3" ); +verticalBox->addItem( label3 ); +... +.... + +.Vertical layout +image::../images/layout-vertical.png[Vertical layout] + +==== Grid layouts (QskGridBox) + +Grid layouts are like linear layouts, but 2 dimensional, and support +laying out UI controls in a grid, including spanning columns and rows. + +[source] +.... +auto* gridBox = new QskGridBox; + +auto* label1 = new QskTextLabel( "control 1" ); +gridBox->addItem( label1, 0, 0 ); // last two arguments are row and column + +auto* label2 = new QskTextLabel( "control 2" ); +gridBox->addItem( label2, 0, 1 ); + +auto* label3 = new QskTextLabel( "control 3" ); +gridBox->addItem( label3, 0, 2 ); + +auto* label4 = new QskTextLabel( "control 4" ); +gridBox->addItem( label4, 1, 0, 1, 2 ); // additional arguments are rowSpan and columnSpan + +auto* label5 = new QskTextLabel( "control 5" ); +gridBox->addItem( label5, 1, 2 ); + +auto* label6 = new QskTextLabel( "control 6" ); +gridBox->addItem( label6, 2, 0 ); + +auto* label7 = new QskTextLabel( "control 7" ); +gridBox->addItem( label7, 2, 1, 1, 2 ); +.... + +.Grid layout +image::../images/layout-grid.png[Grid layout] + +==== Stack layouts (QskStackBox) + +Stack layouts allow for items to be arranged on top of each other. +Usually there is one current (visible) item, while the rest of the items +are hidden below the current one: + +[source] +.... +auto* stackBox = new QskStackBox; + +auto* label1 = new QskTextLabel( "control 1" ); +label1->setBackgroundColor( Qt::blue ); +stackBox->addItem( label1 ); + +auto* label2 = new QskTextLabel( "control 2" ); +label2->setBackgroundColor( Qt::cyan ); +stackBox->addItem( label2 ); + +auto* label3 = new QskTextLabel( "control 3" ); +label3->setBackgroundColor( Qt::magenta ); +stackBox->addItem( label3 ); + +stackBox->setCurrentIndex( 2 ); +... +.... + +.Stack layout (symbolized) +image::../images/layout-stack.png[Stack layout] + +In this example, "control 3" is stacked on top of the blue and the +cyan control. Controls in a stacked layout can be of different sizes. + +NOTE: The image above is just for illustrating purposes. In practice +the topmost control ("control 3" here) is completely covering the ones +below it. + +==== QskControl::autoLayoutChildren() + +When the `QskControl::autoLayoutChildren()` flag is set, the control will +recalculate the geometry of its children whenever the item is updating +its layout. + +=== Stretch factors + +Stretch factors allow layouts to keep a size ratio for their elements. +Let’s say a horizontal layout contains two elements, and when filling up +additional space, the second element should always have twice the width +of the first element. Then the first element should have a stretch +factor of 1 and the second element a factor of 2. + +Stretch factors are set on the layout rather than on the controls +itself: + +[source] +.... +auto horizontalBox = new QskLinearBox( Qt::Horizontal ); + +auto* label1 = new QskTextLabel( "stretch factor 1" ); +horizontalBox->addItem( label1 ); +horizontalBox->setStretchFactor( label1, 1 ); + +auto* label2 = new QskTextLabel( "stretch factor 2" ); +horizontalBox->addItem( label2 ); +horizontalBox->setStretchFactor( label2, 2 ); + +... +.... + +When the layout has all the space it needs (but not more), both elements +are rendered with their preferred size: + +.Stretch factors with preferred size +image::../images/stretch-factors-1.png[Stretch factors preferred size] + +When the layout gets more width, the stretch factors come into play: + +.A stretch factor of 1:2 +image::../images/stretch-factors-2.png[Stretch factors increasing width] + +No matter how wide the layout is, the aspect ratio of 1:2 will always be +kept, meaning that the label on the left will get 33% of the space, and +the label on the right 67%: + +.A stretch factor of 1:2 with different widths +image::../images/stretch-factors-3.png[Stretch factors even more width] + +Stretch factors in QSkinny are the same as in the Qt Graphics View +Framework, see +https://doc.qt.io/qt-5/qgraphicslinearlayout.html#stretch-factor-in-qgraphicslinearlayout[Stretch +Factor in QGraphicsLinearLayout]. + +=== Nesting layouts + +In a real-world application it is typical to nest several layouts in +each other. The example below depicts a UI with a top bar and menu items +on the left: + +.A UI with nested layouts +image::../images/nesting-layouts.png[Nested layouts] + +The code to produce the above UI could look like this (setting colors +etc. omitted for brevity): + +[source] +.... +auto* outerBox = new QskLinearBox( Qt::Vertical ); + +auto* topBar = new QskLinearBox( Qt::Horizontal, outerBox ); + +auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar ); +auto* topLabel2 = new QskTextLabel( "top bar label 2", topBar ); +auto* topLabel3 = new QskTextLabel( "top bar label 3", topBar ); + +auto* mainBox = new QskLinearBox( Qt::Horizontal, outerBox ); + +auto* menuBox = new QskLinearBox( Qt::Vertical, mainBox ); + +auto* menuLabel1 = new QskTextLabel( "menu 1", menuBox ); +auto* menuLabel2 = new QskTextLabel( "menu 2", menuBox ); +auto* menuLabel3 = new QskTextLabel( "menu 3", menuBox ); + +auto* mainText = new QskTextLabel( "here main area", mainBox ); +... +.... + +Here we have an outer vertical layout which divides the content into a +top bar and a main box. The top bar itself consists of a horizontal +layout with 3 buttons, while the main area is split into a left part +with menu buttons and a right part for the main area. That left part +with the menu buttons is again a vertical layout. + +The following diagram makes the layouts visible: + +.The layout structure of the UI +image::../images/nesting-layouts-architecture.png[Nested layouts architecture] + +=== Anchoring in QSkinny + +TODO + diff --git a/doc/tutorials/05-Skins.asciidoc b/doc/tutorials/05-Skins.asciidoc new file mode 100644 index 00000000..acde4d05 --- /dev/null +++ b/doc/tutorials/05-Skins.asciidoc @@ -0,0 +1,305 @@ +--- +title: 5. Skins +layout: docs +--- + +:doctitle: 5. Skins +:notitle: + +== Skins, Skin hints and Skinlets + +Skins, Skin hints and Skinlets allow the user to define how specific +controls looke like. Controls are drawn on the screen by the +skinlet, and therefore it will read information from both the control +itself as well as read the skin hints from the skin: + +.Skinlets query the control and the skin +image::../images/skins-1.png[Styling controls] + +For instance, a button skinlet will read the margins from the skin and +the text to render from the button. + +=== Skins + +Skins are a way to define a look and feel for a whole set of UI +controls, e.g. a night time vs. day time skin, skins for different +brands or an Android Material skin. They contain all kinds of properties +(i.e. skin hints) like colors, margins, fonts and more. + +[source] +.... +class MySkin : public QskSkin +{ + +public: + MySkin( QObject* parent = nullptr ) : QskSkin( parent ) + { + // here define the skin with skin hints + } +}; +.... + +The example below shows different implementations for a push button: One +has a traditional desktop skin, the other is a flat button with a skin +often found in mobile devices. + +.desktop style button +image::../images/skinlets-button-1.png[desktop style button] + +.flat button +image::../images/skinlets-button-2.png[flat button] + +=== Skin hints + +Each instance of a button will have unique properties like its text or +icon file name, but all buttons will have common properties like the +(default) background color and font size. These common properties are +called skin hints, and are defined in a skin. Skin hints are either +colors, e.g. the background color of a button, metrics (e.g. padding) or +flags (e.g. text alignment). + +Skin hints being part of a skin means that each skin can have different +skin hints: + +All buttons in a day time-like skin would have a light background color +and dark text color, while a night time skin would have a dark +background color and light text color by default. + +Extending the `MySkin` example from above, here is an example of some +skin hints for a push button, setting the padding to 10 pixels, the +background color to magenta and the text color to black: + +[source] +.... +class MySkin : public QskSkin +{ + +public: + MySkin( QObject* parent = nullptr ) : QskSkin( parent ) + { + setGradient( QskPushButton::Panel, Qt::magenta ); + setMargins( QskPushButton::Panel | QskAspect::Padding, 10 ); + setColor( QskPushButton::Text, Qt::black ); + } +}; +.... + +.A button styled with skin hints +image::../images/skin-hints.png[Button with skin hints] + +When writing a new skin, a developer needs to know which hints to set +for which control. This usually depends on the control itself; however, +since usually controls are broken down into the three primitives box, +text and graphic, the methods for rendering each of them will take the +following skin hints into account: + +[cols=",",options="header",] +|======================================================================= +|Primitive |Skin hint from QskAspect +|Text |`Alignment` + +`Color` + +`TextColor` + +`StyleColor` + +`LinkColor` + +`Style` + +`FontRole` + +|Graphic |`Alignment` + +`GraphicRole` + +|Box | `Margin` + +`Metric` \| `Border` + +`Color` \| `Border` + +`Color` + +`Metric` \| `Shape` +|======================================================================= + +Some special cases exist where elements other than the primitives above +are used. + +==== States and animations + +Skin hints can also depend on the state a control is in: Buttons for +instance can be in a `Pressed` or `Hovered` state. For such cases, skin +hints cannot only be set on a subcontrol, but also be made dependent on +a specific state. In the example below we define the background color of +the button to be magenta in the default state and cyan in the `Hovered` +state. + +When dealing with states, QSkinny allows for animations between those (and other entities +like skins). The example below adds a different color for the `Hovered` +state and an animation when transitioning between the background colors. +The duration is set to be one second (1000 milliseconds in the +`setAnimation()` call below). Now when a user will hover over the +button, there will be a smooth animation from magenta to cyan +interpolating between the colors. Without the `setAnimation()` call, the +button would just switch to magenta when hovered right away. + +[source] +.... +class MySkin : public QskSkin +{ + +public: + MySkin( QObject* parent = nullptr ) : QskSkin( parent ) + { + setGradient( QskPushButton::Panel, Qt::magenta ); + setMargins( QskPushButton::Panel | QskAspect::Padding, 10 ); + setColor( QskPushButton::Text, Qt::black ); + + setGradient( QskPushButton::Panel | QskPushButton::Hovered, Qt::cyan ); + setAnimation( QskPushButton::Panel | QskAspect::Color, 1000 ); + } +}; +.... + +.button in normal state +image::../images/skin-hints-states-1.png[button in normal state] + +.button in hovered state +image::../images/skin-hints-states-2.png[button in hovered state] + +==== Local skin hints + +It is possible to set local skin hints on specific controls to override +skin-wide settings: + +[source] +.... +auto* label1 = new QskTextLabel( "control 1" ); +label1->setMargins( 20 ); +label1->setBackgroundColor( Qt::blue ); +.... + +In general it is recommended to set the skin hints in the skin rather +than on the control locally, in order to separate the style from the +implementation, and to allow switching between skins. How to write +controls that are themable is explained in the section about +link:Writing-own-controls.html[writing own controls]. + +Taking animations and local skin hints into account, the architecture +diagram now looks like this: + +.Skinlets can also read from local skinlets and animators +image::../images/skins-2.png[Animators and local skin hints] + +=== Skinlets + +A skinlet is in charge of drawing a control on the screen, similar to a +Delegate in QML. It will read all the hints it needs from either the +control itself or the skin, then it will draw the subcontrols that +represent the control: In the sample case of a button, the skinlet will +first draw the background panel, potentially consisting of a rectangle +with a fill color. Then it will draw the text of the button, and last it +will draw an icon, in case the button has one set. + +Each skin can have a different skinlet to draw a control. Often the +skinlet is the same across different skins and the skins only differ in +skin hints, e.g. buttons having different fonts. However, it is also +possible to have completely different skinlets per skin. This ensures a +separation of application code instantiating the controls itself from +the visual representation of the controls. + +QSkinny already contains implementations of many common controls like +text labels, buttons and so on. However, some custom controls might +need to be written from scratch, including the skinlet; for an +explanation on how to do this, see the example of +link:Writing-own-controls.html[writing own controls]. + +For a closer look at how the skinlet draws the controls in the scene +graph, see link:scene-graph.html[scene graph representations of controls]. + +Of course each app has different controls and therefore there are also +different skinlets, so a more complete version of the architecture +diagram looks like this: + +.There is one skinlet for each atomic control +image::../images/skins-3.png[Animators and local skin hints] + +=== Skin factories and switching between skins + +Skins are usually not created by the user directly, but by a skin +factory. Such a factory keeps track of the skins registered in the +system, and handles creating a new skin when the user switches them +during application lifetime. + +When having two skins called `MySkin` and `OtherSkin` in an app, the +corresponding skin factory might look like this: + +[source] +.... +class MySkinFactory : public QskSkinFactory +{ + + Q_OBJECT + +public: + QStringList skinNames() const override + { + return { "MySkin", "OtherSkin" }; + } + + QskSkin* createSkin( const QString& skinName ) override + { + if ( skinName == "MySkin" ) + return new MySkin; + + if ( skinName == "OtherSkin" ) + return new OtherSkin; + + return nullptr; + } +}; +.... + +That skin factory has to be registered during app start; it is also a +good idea to set a default skin right away: + +[source] +.... +int main( int argc, char* argv[] ) +{ + auto* skinFactory = new MySkinFactory; + qskSkinManager->registerFactory( "MySkinFactory", skinFactory ); + + QGuiApplication app( argc, argv ); + + qskSetup->setSkin( "MySkin" ); + + ... + QskWindow window; + window.show(); + + return app.exec(); +} +.... + +Now we can define the `OtherSkin` and define different skin hints for +e.g. push buttons. Here we define the background color and padding to be +different; also we configure buttons to have a blue border: + +[source] +.... +class OtherSkin : public QskSkin +{ + +public: + OtherSkin( QObject* parent = nullptr ) : QskSkin( parent ) + { + setGradient( QskPushButton::Panel, Qt::cyan ); + setMargins( QskPushButton::Panel | QskAspect::Padding, 15 ); + setBoxBorderColors( QskPushButton::Panel, Qt::blue ); + setBoxBorderMetrics( QskPushButton::Panel, 1 ); + } +}; +.... + +Switching between skins will change the look of `QskPushButton` +instances: + +.button in `MySkin` (as above) +image::../images/skin-hints-states-1.png[button in normal state] + +.button in `OtherSkin` +image::../images/skin-factory.png[Styling controls] + diff --git a/doc/tutorials/06-scalable-graphics.asciidoc b/doc/tutorials/06-scalable-graphics.asciidoc new file mode 100644 index 00000000..2975d83b --- /dev/null +++ b/doc/tutorials/06-scalable-graphics.asciidoc @@ -0,0 +1,62 @@ +--- +title: 6. (Scalable) graphics +layout: docs +--- + +:doctitle: 6. (Scalable) graphics +:notitle: + +== (Scalable) graphics + +QSkinny offers support for scalable graphics, i.e. rendering SVGs that +adapt to a specific size. This means that when a graphic is embedded in +a layout, it can change its size when the layout is growing or +shrinking, while still maintaining a correct aspect ratio. + +Imagine the following code, which produces the image depicted below: + +[source] +.... +auto horizontalBox = new QskLinearBox( Qt::Horizontal ); +horizontalBox->setPreferredSize( { 200, 75 } ); + +QImage image1( ":/images/cloud.svg" ); +QskGraphic graphic1 = QskGraphic::fromImage( image1 ); +auto* label1 = new QskGraphicLabel( graphic1, horizontalBox ); +label1->setSizePolicy( QskSizePolicy::ConstrainedPreferred, QskSizePolicy::Expanding ); + +QImage image2( ":/images/train.svg" ); +QskGraphic graphic2 = QskGraphic::fromImage( image2 ); +auto* label2 = new QskGraphicLabel( graphic2, horizontalBox ); +label2->setSizePolicy( QskSizePolicy::ConstrainedPreferred, QskSizePolicy::Expanding ); +... +.... + +.graphics with preferred size +image::../images/scalable-graphics-1.png[Scalable graphics default] + +When resizing the window, the graphics will scale according to the size +available in the layout: + +.graphics bounded by width +image::../images/scalable-graphics-2.png[Scalable graphics bounded by width] + +.graphics bounded by height +image::../images/scalable-graphics-3.png[Scalable graphics bounded by height] + +Since we set the horizontal size policy of the graphics to +`ConstrainedPreferred`, the scaling is done through QskGraphic’s +`widthForHeight()` methods to maintain the correct aspect ratio. If we +had set the vertical policy to `ConstrainedPreferred` and the horizontal +one to e.g. `Expanding`, the layout would have queried the +`heightForWidth()` method instead. + +Of course non-scalable graphics like PNGs and JPGs are also supported: + +[source] +.... +QImage image( "background.jpg" ); +QskGraphic graphic = QskGraphic::fromImage( image ); +... +.... + diff --git a/doc/tutorials/07-parents-and-parent-items.asciidoc b/doc/tutorials/07-parents-and-parent-items.asciidoc new file mode 100644 index 00000000..9b074116 --- /dev/null +++ b/doc/tutorials/07-parents-and-parent-items.asciidoc @@ -0,0 +1,191 @@ +--- +title: 7. Parents and parent items +layout: docs +--- + +:doctitle: 7. Parents and parent items +:notitle: + +== Parents and parent items + +Creating an app with QSkinny consists of creating controls, putting them +into layouts and nesting layouts and controls inside each other. The +nesting already creates some sort of a hierarchy in the app, see the +"Nesting layouts" section in the link:Layouts.html[layouts page]. In +more general terms, all controls are part of several hierarchies: + +* The *object tree*. This is a tree of `QObject` instances which manages +lifetime: Objects created with a parent will get deleted whenever their +parent is deleted. For more information, see the Qt documentation on +https://doc.qt.io/qt-5/objecttrees.html[Object Trees & Ownership]. +* The *item tree*. This is a tree of items displayed on the screen, +i.e. `QQuickItem` instances. Qt will traverse the item tree when +rendering items on the screen. The positioning of an item depends on its +parent item, e.g. layouts will position their child items according to +certain policies. In addition, visual items will inherit properties from +its parent item like visibility or opacity. The item tree is often +similar to the object tree, but not necessarily: Instances of +`QQuickItem` can have a parent item set, but have another parent, or no +parent at all. See also the Qt documentation on +https://doc.qt.io/qt-5/qtquick-visualcanvas-visualparent.html[Concepts - +Visual Parent in Qt Quick]. +* The *scene graph*. The scene graph contains a representation of +graphic primitives like rectangles, textures (i.e. images) and text, to +allow efficient rendering on the screen with OpenGL or other backends. +This is described in more details in link:scene-graph.html[scene graph +representations of controls]. + +=== Example + +Let’s look at the "Nesting layouts" example from the +link:Layouts.html[layouts documentation]. The UI looks like this: + +.UI with nested layouts +image::../images/nesting-layouts.png[Nested layouts] + +The code for this UI is below: + +[source] +.... +auto* outerBox = new QskLinearBox( Qt::Vertical ); + +auto* topBar = new QskLinearBox( Qt::Horizontal, outerBox ); + +auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar ); +auto* topLabel2 = new QskTextLabel( "top bar label 2", topBar ); +auto* topLabel3 = new QskTextLabel( "top bar label 3", topBar ); + +auto* mainBox = new QskLinearBox( Qt::Horizontal, outerBox ); + +auto* menuBox = new QskLinearBox( Qt::Vertical, mainBox ); + +auto* menuLabel1 = new QskTextLabel( "menu 1", menuBox ); +auto* menuLabel2 = new QskTextLabel( "menu 2", menuBox ); +auto* menuLabel3 = new QskTextLabel( "menu 3", menuBox ); + +auto* mainText = new QskTextLabel( "here main area", mainBox ); + +QskWindow window; +window.addItem( outerBox ); +window.show(); +.... + +==== Object tree + +In the example above, when we created a new element, we always passed +the `QObject` parent as an argument to the constructor, which is good +practice. We do that for instance in this line: + +[source] +.... +auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar ); +.... + +This makes sure `topBar` is a parent of `topLabel1`. It means that when +`topBar` is deleted, it will automatically delete `topLabel1`, because +the latter is a child of the `topBar`. + +Below is an image of the object tree, i.e. the `QObject` parent-child +relationship. The `QskWindow` is hereby the parent of the +`QQuickRootItem`, which itself is the parent of the `outer box`, and so +on. For information on how to obtain this tree, see +https://doc.qt.io/qt-5/qobject.html#dumpObjectTree[QObject::dumpObjectTree()]. + +.QObject tree (and item tree) of the nested layouts UI +image::../images/object-hierarchy.png[QObject hierarchy] + +==== Item tree + +The Item tree for the example above is identical to the object tree. As +described, we always pass the parent object in the constructor: + +[source] +.... +auto* topLabel1 = new QskTextLabel( "top bar label 1", topBar ); +.... + +The line above will (in addition to the setting the parent) also ensure +that `topBar` will be a *parent item* of `topLabel1`; this is done by +the `QQuickItem` constructor. + +Even if we had not passed the parent in the constructor, we could still +add the label to the `topBar` via an explicit call: + +[source] +.... +auto* topLabel1 = new QskTextLabel( "top bar label 1" ); +topBar->addItem( topLabel1 ); +.... + +The call to `addItem()` above sets the parent item of `topLabel1` to +`topBar` and thus the latter will display it as one of its children. In +this case it would also set the parent, because the `topLabel1` does not +have one yet. In other words, setting a parent item will also set the +parent *if* the parent is null. + +So since the `topBar` is a parent item of `topLabel1`, it means that +`topLabel1` will inherit settings like visibility and opacity from +`topBar`. For instance, if we set the the visibility of the `topBar` to +false, all its child items will be invisible as well (which in this case +would be all top bar labels). If we set the opacity to 0.2, all its +child items will be almost transparent: + +[source] +.... +topBar->setOpacity( 0.2 ); +.... + +.Changing opacity of an item will affect all its child items +image::../images/nesting-layouts-item-tree-1.png[Changing the item tree] + +==== Difference in object trees and item trees + +As an example for when the object tree and item tree differ, let’s +decide to add a bottom bar to our UI and move our `topLabel1` from the +top bar to the bottom bar. This is easy: + +[source] +.... +auto* bottomBar = new QskLinearBox( Qt::Horizontal, outerBox ); +topLabel1->setParentItem( bottomBar ); +.... + +.Moving a label from the top bar to the bottom bar +image::../images/nesting-layouts-item-tree-2.png[Moving a label to the bottom bar] + +Now we decide to get rid of our top bar altogether: + +[source] +.... +topBar->deleteLater(); +.... + +This will also delete our label from the bottom bar: + +.Deleting the top bar will delete all its children +image::../images/nesting-layouts-item-tree-3.png[Deleting the top bar] + +The reason why the label from the bottom bar was also deleted is that +with the call to `setParentItem()` above we set a new parent item; the +parent, however, was still `topBar` (the call to `setParentItem()` did +not change the parent, because it was not null). So when the `topBar` +was deleted, it deleted all of its children, including the moved label +`topLabel1`. + +After we moved the label to the bottom bar, the object tree was +different from the item tree, hence we got a surprising result when +deleting the top bar. It is a good idea to try to keep the trees the +same, and be aware of the existence of both of them. + +If we reparent our label to the bottom bar before deleting the top bar, +we get the desired effect: + +[source] +.... +topLabel1->setParent( bottomBar ); +topLabel1->setParentItem( bottomBar ); +topBar->deleteLater(); +.... + +.Reparenting the label will keep it alive when deleting the top bar +image::../images/nesting-layouts-item-tree-4.png[Reparenting the item] diff --git a/doc/tutorials/08-qskinny-and-qml.asciidoc b/doc/tutorials/08-qskinny-and-qml.asciidoc new file mode 100644 index 00000000..ef521bc4 --- /dev/null +++ b/doc/tutorials/08-qskinny-and-qml.asciidoc @@ -0,0 +1,53 @@ +--- +title: 8. Using QSkinny and QML +layout: docs +--- + +:doctitle: 8. Using QSkinny and QML +:notitle: + +== QSkinny - Using QSkinny and QML + +Combining QSkinny and QML is possible: Since both QML elements and +QSkinny controls derive from `QQuickItem`, they can be combined and +arranged in a common app. The +https://github.com/uwerat/qskinny/tree/master/examples/buttons[QSkinny +buttons example] shows how QSkinny controls can be used from QML. + +When using a QSkinny control, all the methods exposed as either properties, +slots or invokables can be used in QML. For example, the QSkinny control +`QskLinearBox` defines the following properties: + +[source] +.... +class QSK_EXPORT QskLinearBox : public QskIndexedLayoutBox +{ + Q_PROPERTY( Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged FINAL ) + Q_PROPERTY( qreal spacing READ spacing WRITE setSpacing RESET resetSpacing NOTIFY spacingChanged FINAL ) + ... +}; +.... + +The `QskLinearBox` class is registered to QML as `Qsk.LinearBox` via +Qt’s `qmlRegisterType`, so the exposed properties `orientation` and +`spacing` can be used like this: + +[source] +.... +Qsk.LinearBox +{ + orientation: Qt.Horizontal + spacing: 10 + + // here define elements inside the box + ... +} +.... + +The full Buttons example is depicted below. + +.The buttons example shows how to mix QSkinny and QML +image::../images/buttons-example.png[Buttons example] + +For more information on using C++ classes from QML, see the article about exposing attributes of {cpp} types to QML in the +https://doc.qt.io/qt-5/qtqml-cppintegration-exposecppattributes.html[Qt documentation]. diff --git a/doc/tutorials/09-writing-own-controls.asciidoc b/doc/tutorials/09-writing-own-controls.asciidoc new file mode 100644 index 00000000..ea20bb5d --- /dev/null +++ b/doc/tutorials/09-writing-own-controls.asciidoc @@ -0,0 +1,314 @@ +--- +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::../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::../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::../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::../images/control-with-skinlet.png[Control with skinlet] diff --git a/doc/tutorials/10-scene-graph.asciidoc b/doc/tutorials/10-scene-graph.asciidoc new file mode 100644 index 00000000..964bacec --- /dev/null +++ b/doc/tutorials/10-scene-graph.asciidoc @@ -0,0 +1,74 @@ +--- +title: 9. Scene graph representations of controls +layout: docs +--- + +:doctitle: 9. Scene graph representations of controls +:notitle: + +== QSkinny - Scene graph representations of controls + +Each control that is displayed on the screen consists of one or more +scene graph nodes. Those nodes can be either basic shapes like +rectangles, or they can contain other information like positioning (used +with transform nodes), opacity or clipping. + +The source code below shows a minimal example displaying a button: + +[source] +.... +auto* button = new QskPushButton( "button" ); + +QskWindow window; +window.addItem( button ); +window.show(); +.... + +For this example, the scene graph will contain the following nodes: + +.Scene graph representation of a button +image::../images/skins-sg-1.png[Scene graph nodes for a button] + +The top two nodes (root and Quick root item) are created for every +QtQuick application. The button itself consists of 5 nodes in our case: +One root note (`button node`), one node just to group its children (just +labeled `node`), one geometry node for drawing the background (`panel +node`), one transform node for setting the position of the text and +another geometry node for displaying the text (`text node`). + +For an explanation of the different scene graph node types, see the Qt +documentation of +https://doc.qt.io/qt-5/qsgnode.html#NodeType-enum[QSGNode::NodeType]. + +The example above is the simplest form of a button, in practice there +might be more nodes per control, for instance an opacity node or a clip +node. + +Now we add more elements to the UI by putting the button inside a layout +(`QskBox`): + +[source] +.... +auto* box = new QskBox; +auto* button = new QskPushButton( "button", box ); + +QskWindow window; +window.addItem( box ); +window.show(); +.... + +Then the scene graph has the following structure: + +.Scene graph representation of a button inside a box +image::../images/skins-sg-2.png[Scene graph nodes for a button in a box] + +Here we can see that since the box is a parent of the button, the `box +node` is also a parent of the `button node` in the scene graph. Also, the +box has two child nodes: The button, which is the same as in the earlier +example, and a node for the panel of the box, in case the panel itself has a +background color. + +In a more complicated UI with multiple elements and more advanced +layouts, the number of scene graph nodes can be quite high. This is why +QSkinny tries to create as little nodes as possible and reuse as many as +it can. diff --git a/doc/tutorials/images/architecture-simple.png b/doc/tutorials/images/architecture-simple.png new file mode 100644 index 00000000..02769868 Binary files /dev/null and b/doc/tutorials/images/architecture-simple.png differ diff --git a/doc/tutorials/images/architecture.png b/doc/tutorials/images/architecture.png new file mode 100644 index 00000000..97499a7f Binary files /dev/null and b/doc/tutorials/images/architecture.png differ diff --git a/doc/tutorials/images/buttons-example.png b/doc/tutorials/images/buttons-example.png new file mode 100644 index 00000000..535c2323 Binary files /dev/null and b/doc/tutorials/images/buttons-example.png differ diff --git a/doc/tutorials/images/compositing-controls.png b/doc/tutorials/images/compositing-controls.png new file mode 100644 index 00000000..2813298a Binary files /dev/null and b/doc/tutorials/images/compositing-controls.png differ diff --git a/doc/tutorials/images/control-with-skinlet.png b/doc/tutorials/images/control-with-skinlet.png new file mode 100644 index 00000000..e7d587e6 Binary files /dev/null and b/doc/tutorials/images/control-with-skinlet.png differ diff --git a/doc/tutorials/images/horizontal-layout.gif b/doc/tutorials/images/horizontal-layout.gif new file mode 100644 index 00000000..64a94e78 Binary files /dev/null and b/doc/tutorials/images/horizontal-layout.gif differ diff --git a/doc/tutorials/images/layout-grid.png b/doc/tutorials/images/layout-grid.png new file mode 100644 index 00000000..0f0f9ea1 Binary files /dev/null and b/doc/tutorials/images/layout-grid.png differ diff --git a/doc/tutorials/images/layout-horizontal.png b/doc/tutorials/images/layout-horizontal.png new file mode 100644 index 00000000..a725a7b3 Binary files /dev/null and b/doc/tutorials/images/layout-horizontal.png differ diff --git a/doc/tutorials/images/layout-stack.png b/doc/tutorials/images/layout-stack.png new file mode 100644 index 00000000..14827908 Binary files /dev/null and b/doc/tutorials/images/layout-stack.png differ diff --git a/doc/tutorials/images/layout-vertical.png b/doc/tutorials/images/layout-vertical.png new file mode 100644 index 00000000..c7ad2f4c Binary files /dev/null and b/doc/tutorials/images/layout-vertical.png differ diff --git a/doc/tutorials/images/nesting-layouts-architecture.png b/doc/tutorials/images/nesting-layouts-architecture.png new file mode 100644 index 00000000..2bde6a54 Binary files /dev/null and b/doc/tutorials/images/nesting-layouts-architecture.png differ diff --git a/doc/tutorials/images/nesting-layouts-item-tree-1.png b/doc/tutorials/images/nesting-layouts-item-tree-1.png new file mode 100644 index 00000000..440cf60a Binary files /dev/null and b/doc/tutorials/images/nesting-layouts-item-tree-1.png differ diff --git a/doc/tutorials/images/nesting-layouts-item-tree-2.png b/doc/tutorials/images/nesting-layouts-item-tree-2.png new file mode 100644 index 00000000..5a519344 Binary files /dev/null and b/doc/tutorials/images/nesting-layouts-item-tree-2.png differ diff --git a/doc/tutorials/images/nesting-layouts-item-tree-3.png b/doc/tutorials/images/nesting-layouts-item-tree-3.png new file mode 100644 index 00000000..6298b6f4 Binary files /dev/null and b/doc/tutorials/images/nesting-layouts-item-tree-3.png differ diff --git a/doc/tutorials/images/nesting-layouts-item-tree-4.png b/doc/tutorials/images/nesting-layouts-item-tree-4.png new file mode 100644 index 00000000..f10528d0 Binary files /dev/null and b/doc/tutorials/images/nesting-layouts-item-tree-4.png differ diff --git a/doc/tutorials/images/nesting-layouts.png b/doc/tutorials/images/nesting-layouts.png new file mode 100644 index 00000000..2bd76046 Binary files /dev/null and b/doc/tutorials/images/nesting-layouts.png differ diff --git a/doc/tutorials/images/object-hierarchy.png b/doc/tutorials/images/object-hierarchy.png new file mode 100644 index 00000000..f1be6856 Binary files /dev/null and b/doc/tutorials/images/object-hierarchy.png differ diff --git a/doc/tutorials/images/scalable-graphics-1.png b/doc/tutorials/images/scalable-graphics-1.png new file mode 100644 index 00000000..8ba6764e Binary files /dev/null and b/doc/tutorials/images/scalable-graphics-1.png differ diff --git a/doc/tutorials/images/scalable-graphics-2.png b/doc/tutorials/images/scalable-graphics-2.png new file mode 100644 index 00000000..a03db2c9 Binary files /dev/null and b/doc/tutorials/images/scalable-graphics-2.png differ diff --git a/doc/tutorials/images/scalable-graphics-3.png b/doc/tutorials/images/scalable-graphics-3.png new file mode 100644 index 00000000..c716c460 Binary files /dev/null and b/doc/tutorials/images/scalable-graphics-3.png differ diff --git a/doc/tutorials/images/size-hints-1.png b/doc/tutorials/images/size-hints-1.png new file mode 100644 index 00000000..ac0db10c Binary files /dev/null and b/doc/tutorials/images/size-hints-1.png differ diff --git a/doc/tutorials/images/size-hints-2.png b/doc/tutorials/images/size-hints-2.png new file mode 100644 index 00000000..e1d60e62 Binary files /dev/null and b/doc/tutorials/images/size-hints-2.png differ diff --git a/doc/tutorials/images/size-hints-calculation.png b/doc/tutorials/images/size-hints-calculation.png new file mode 100644 index 00000000..332c6515 Binary files /dev/null and b/doc/tutorials/images/size-hints-calculation.png differ diff --git a/doc/tutorials/images/size-policies-horizontal-minimum-1.png b/doc/tutorials/images/size-policies-horizontal-minimum-1.png new file mode 100644 index 00000000..6c045963 Binary files /dev/null and b/doc/tutorials/images/size-policies-horizontal-minimum-1.png differ diff --git a/doc/tutorials/images/size-policies-horizontal-minimum-2.png b/doc/tutorials/images/size-policies-horizontal-minimum-2.png new file mode 100644 index 00000000..d2b16d32 Binary files /dev/null and b/doc/tutorials/images/size-policies-horizontal-minimum-2.png differ diff --git a/doc/tutorials/images/size-policies-horizontal-minimum-3.png b/doc/tutorials/images/size-policies-horizontal-minimum-3.png new file mode 100644 index 00000000..b8b57ad4 Binary files /dev/null and b/doc/tutorials/images/size-policies-horizontal-minimum-3.png differ diff --git a/doc/tutorials/images/size-policies-horizontal-minimum-4.png b/doc/tutorials/images/size-policies-horizontal-minimum-4.png new file mode 100644 index 00000000..7918dd6b Binary files /dev/null and b/doc/tutorials/images/size-policies-horizontal-minimum-4.png differ diff --git a/doc/tutorials/images/skin-factory.png b/doc/tutorials/images/skin-factory.png new file mode 100644 index 00000000..f56c4957 Binary files /dev/null and b/doc/tutorials/images/skin-factory.png differ diff --git a/doc/tutorials/images/skin-hints-states-1.png b/doc/tutorials/images/skin-hints-states-1.png new file mode 100644 index 00000000..8de3ad96 Binary files /dev/null and b/doc/tutorials/images/skin-hints-states-1.png differ diff --git a/doc/tutorials/images/skin-hints-states-2.png b/doc/tutorials/images/skin-hints-states-2.png new file mode 100644 index 00000000..0d6e91f9 Binary files /dev/null and b/doc/tutorials/images/skin-hints-states-2.png differ diff --git a/doc/tutorials/images/skin-hints.png b/doc/tutorials/images/skin-hints.png new file mode 100644 index 00000000..1b939a70 Binary files /dev/null and b/doc/tutorials/images/skin-hints.png differ diff --git a/doc/tutorials/images/skinlets-button-1.png b/doc/tutorials/images/skinlets-button-1.png new file mode 100644 index 00000000..38e3591f Binary files /dev/null and b/doc/tutorials/images/skinlets-button-1.png differ diff --git a/doc/tutorials/images/skinlets-button-2.png b/doc/tutorials/images/skinlets-button-2.png new file mode 100644 index 00000000..4568d02b Binary files /dev/null and b/doc/tutorials/images/skinlets-button-2.png differ diff --git a/doc/tutorials/images/skins-1.png b/doc/tutorials/images/skins-1.png new file mode 100644 index 00000000..cbcf4035 Binary files /dev/null and b/doc/tutorials/images/skins-1.png differ diff --git a/doc/tutorials/images/skins-2.png b/doc/tutorials/images/skins-2.png new file mode 100644 index 00000000..084b94a4 Binary files /dev/null and b/doc/tutorials/images/skins-2.png differ diff --git a/doc/tutorials/images/skins-3.png b/doc/tutorials/images/skins-3.png new file mode 100644 index 00000000..be3df56c Binary files /dev/null and b/doc/tutorials/images/skins-3.png differ diff --git a/doc/tutorials/images/skins-sg-1.png b/doc/tutorials/images/skins-sg-1.png new file mode 100644 index 00000000..9c1c7e46 Binary files /dev/null and b/doc/tutorials/images/skins-sg-1.png differ diff --git a/doc/tutorials/images/skins-sg-2.png b/doc/tutorials/images/skins-sg-2.png new file mode 100644 index 00000000..1f2923e7 Binary files /dev/null and b/doc/tutorials/images/skins-sg-2.png differ diff --git a/doc/tutorials/images/stretch-factors-1.png b/doc/tutorials/images/stretch-factors-1.png new file mode 100644 index 00000000..653bdfe8 Binary files /dev/null and b/doc/tutorials/images/stretch-factors-1.png differ diff --git a/doc/tutorials/images/stretch-factors-2.png b/doc/tutorials/images/stretch-factors-2.png new file mode 100644 index 00000000..4095e9d2 Binary files /dev/null and b/doc/tutorials/images/stretch-factors-2.png differ diff --git a/doc/tutorials/images/stretch-factors-3.png b/doc/tutorials/images/stretch-factors-3.png new file mode 100644 index 00000000..b12b2f21 Binary files /dev/null and b/doc/tutorials/images/stretch-factors-3.png differ diff --git a/doc/tutorials/images/subclassing-existing-controls.png b/doc/tutorials/images/subclassing-existing-controls.png new file mode 100644 index 00000000..8a127b46 Binary files /dev/null and b/doc/tutorials/images/subclassing-existing-controls.png differ diff --git a/doc/tutorials/images/writing-first-application.png b/doc/tutorials/images/writing-first-application.png new file mode 100644 index 00000000..409a42f7 Binary files /dev/null and b/doc/tutorials/images/writing-first-application.png differ diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md new file mode 100644 index 00000000..e98636ee --- /dev/null +++ b/doc/tutorials/index.md @@ -0,0 +1,6 @@ +--- +title: Tutorials +excerpt: In this section you'll find the QSkinny tutorials. +layout: docs +--- +