Guia didàctica: Rock, Paper, Scissors amb Configuració

Projecte: A08.04.3_configB
Objectiu: Aprendre gestió moderna de recursos en Qt6 i persistència de dades amb Settings


📚 Índex

  1. Introducció
  2. Què afegeix aquesta versió
  3. Gestió moderna de recursos
  4. Singleton ThemeManager
  5. Persistència amb Settings
  6. Exercicis pràctics

Introducció

Aquest projecte és una evolució del joc Pedra, Paper, Tisores bàsic que ja heu desenvolupat. Afegeix dues funcionalitats importants:

  1. Pantalla de configuració amb tema clar/fosc
  2. Persistència de preferències que es guarden entre sessions

Però el més important: utilitza la gestió moderna de recursos de Qt6, sense fitxers .qrc.


Què afegeix aquesta versió

Versió anterior (A08.04.3)

Versió actual (A08.04.3_configB)


Gestió moderna de recursos

Per què no usar fitxers .qrc?

El fitxer .qrc és un fitxer XML que Qt5 utilitzava per gestionar recursos:

<!-- qml.qrc - Mètode ANTIC (Qt5) -->
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>img/rock.png</file>
        <file>img/paper.png</file>
    </qresource>
</RCC>

Problemes:

Mètode modern: CMakeLists.txt

En Qt6, declarem els recursos directament al CMakeLists.txt:

qt_add_qml_module(rps-config
    URI rpsconfig              # Nom del mòdul
    VERSION 1.0                # Versió
    QML_FILES                  # Fitxers QML
        main.qml
        ThemeManager.qml
        BotoRPS.qml
        PaginaJoc.qml
        ConfigPage.qml
        ControlButtons.qml
        VistaJugada.qml
    RESOURCES                  # Recursos (imatges, fonts, etc.)
        img/rock.png
        img/paper.png
        img/scissors.png
        img/settings.png
        img/save.png
)

Avantatges:

On acaben els recursos?

Quan compiles, Qt crea aquesta estructura:

build/
└── rpsconfig/              # Nom del mòdul (URI)
    ├── main.qml
    ├── ThemeManager.qml
    ├── BotoRPS.qml
    ├── img/
    │   ├── rock.png
    │   └── paper.png
    └── qmldir            # Generat automàticament

Els recursos s'encasten dins de l'executable i estan disponibles amb paths com:

Paths relatius vs absoluts

❌ MALAMENT: Path sense qrc:/ (no funciona en executable)

Image {
    source: "rock.png"  // Busca al sistema de fitxers
}

Problema: Funciona durant el desenvolupament però NO en l'executable distribuït.

✅ BÉ: Path relatiu dins del mòdul

Image {
    source: "img/rock.png"  // Path relatiu al mòdul
}

Avantatge: Curt, net, funciona sempre.

✅ També BÉ: Path absolut amb qrc:/

Image {
    source: "qrc:/qt/qml/rpsconfig/img/rock.png"  // Path complet
}

Avantatge: Explícit, funciona des de qualsevol lloc.
Desavantatge: Llarg i menys llegible.

Comparació pràctica

Mètode Path al QML Funciona en executable? Recomanat
Sense qrc:/ "rock.png" ❌ NO Mai
Relatiu (mòdul) "img/rock.png" ✅ SÍ
Absolut "qrc:/qt/qml/rpsconfig/img/rock.png" ✅ SÍ Si cal

Exemple complet

CMakeLists.txt:

qt_add_qml_module(rps-config
    URI rpsconfig
    QML_FILES BotoRPS.qml
    RESOURCES img/rock.png
)

BotoRPS.qml:

import QtQuick

Item {
    property string font  // Rebem el path de la imatge
    
    Image {
        source: font  // "img/rock.png" (path relatiu)
        anchors.centerIn: parent
        fillMode: Image.PreserveAspectFit
    }
}

ControlButtons.qml:

Repeater {
    model: [
        {tipus: "R", font: "img/rock.png"},      // Path relatiu
        {tipus: "P", font: "img/paper.png"},
        {tipus: "S", font: "img/scissors.png"}
    ]
    
    BotoRPS {
        text: modelData.tipus
        font: modelData.font  // Passa el path al component
    }
}

Singleton ThemeManager

Què és un singleton?

Un singleton és un objecte que:

Exemple d'ús: Gestionar el tema (clar/fosc) de tota l'aplicació des d'un sol lloc.

Per què usar un singleton per al tema?

Imagina que vols canviar el color de fons de l'aplicació:

❌ MALAMENT: Sense singleton

// PaginaJoc.qml
Rectangle {
    color: root.isDark ? "#1E1E1E" : "#FFFFFF"  // Duplicat!
}

// ConfigPage.qml
Rectangle {
    color: root.isDark ? "#1E1E1E" : "#FFFFFF"  // Duplicat!
}

// BotoRPS.qml
Rectangle {
    color: root.isDark ? "#2D2D2D" : "#F5F5F5"  // Més duplicació!
}

Problemes:

✅ BÉ: Amb singleton

// PaginaJoc.qml
Rectangle {
    color: ThemeManager.backgroundColor  // Centralitzat!
}

// ConfigPage.qml
Rectangle {
    color: ThemeManager.backgroundColor  // Mateix codi!
}

// Canviar el tema des de qualsevol lloc:
Button {
    onClicked: ThemeManager.toggleTheme()
}

Com crear un singleton

1. Crear el fitxer QML amb pragma Singleton

ThemeManager.qml:

pragma Singleton  // ← IMPORTANT: Declara que és singleton
import QtQuick

QtObject {
    id: themeManager
    
    // Propietat principal
    property bool darkMode: false
    
    // Colors mode clar
    property color lightBackground: "#FFFFFF"
    property color lightText: "#000000"
    
    // Colors mode fosc
    property color darkBackground: "#1E1E1E"
    property color darkText: "#FFFFFF"
    
    // Colors actius (calculats segons darkMode)
    property color backgroundColor: darkMode ? darkBackground : lightBackground
    property color textColor: darkMode ? darkText : lightText
    
    // Funció per canviar el tema
    function toggleTheme() {
        darkMode = !darkMode
    }
}

2. Declarar-lo al fitxer qmldir

qmldir:

module rpsconfig
singleton ThemeManager 1.0 ThemeManager.qml

Sintaxi:

3. Usar-lo als fitxers QML

import rpsconfig  // Importa el mòdul

Rectangle {
    color: ThemeManager.backgroundColor  // Accés directe
    
    Text {
        color: ThemeManager.textColor
        text: "Hola món"
    }
    
    Button {
        text: "Canviar tema"
        onClicked: ThemeManager.toggleTheme()
    }
}

Com funciona internament?

  1. Compilació: CMake i Qt generen codi C++ que registra el singleton
  2. Execució: Qt crea una única instància de ThemeManager
  3. Accés: Tots els fitxers QML comparteixen la mateixa instància
┌─────────────────┐
│  ThemeManager   │  ← Una sola instància
│  (Singleton)    │
└────────┬────────┘
         │
    ┌────┴────┬────────┬────────┐
    │         │        │        │
PaginaJoc  ConfigPage  Botons  etc.
    │         │        │        │
 Accedeixen a la mateixa instància

Propietats reactives

Quan canvies darkMode, tots els colors s'actualitzen automàticament:

property color backgroundColor: darkMode ? darkBackground : lightBackground
//                              ↑ Quan darkMode canvia, aquesta expressió
//                                es recalcula automàticament

Això s'anomena binding en Qt:


Persistència amb Settings

Què és Qt.labs.settings?

Qt.labs.settings és un component QML que:

On es guarden les dades?

Plataforma Ubicació
Linux ~/.config/ExempleOrg/RPSConfig.conf
macOS ~/Library/Preferences/cat.exemple.RPSConfig.plist
Windows Registre: HKEY_CURRENT_USER\Software\ExempleOrg\RPSConfig
Android SharedPreferences intern

Configurar l'organització i aplicació

main.cpp:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // IMPORTANT: Defineix organització i aplicació
    QCoreApplication::setOrganizationName("ExempleOrg");
    QCoreApplication::setOrganizationDomain("exemple.cat");
    QCoreApplication::setApplicationName("RPSConfig");
    
    // Sense això, Settings no sap on guardar les dades!

    QQmlApplicationEngine engine;
    engine.loadFromModule("rpsconfig", "Main");

    return app.exec();
}

Usar Settings al QML

main.qml:

import QtQuick
import Qt.labs.settings 1.0  // Importa Settings

ApplicationWindow {
    id: root
    
    // Dades locals de l'aplicació
    property bool soundEnabled: true
    property int gamesPlayed: 0
    
    // Settings: guarda automàticament
    Settings {
        // property alias NOM_SETTING: objecte.propietat
        property alias darkMode: ThemeManager.darkMode
        property alias sound: root.soundEnabled
        property alias games: root.gamesPlayed
    }
    
    // Quan darkMode canvia → es guarda automàticament
    // Quan l'app s'inicia → es carrega automàticament
}

Com funciona Settings?

Inici de l'aplicació:
1. Qt carrega Settings des del disc
2. Assigna valors a les propietats (darkMode, soundEnabled, etc.)
3. L'app continua amb els valors restaurats

Durant l'execució:
1. L'usuari canvia darkMode = true
2. Settings detecta el canvi
3. Guarda automàticament al disc

Tancament de l'aplicació:
1. Settings guarda tots els canvis pendents
2. L'app es tanca

Exemple complet

ConfigPage.qml:

import QtQuick
import QtQuick.Controls
import rpsconfig

Page {
    Column {
        spacing: 20
        
        // Switch per al tema
        Switch {
            text: "Mode fosc"
            checked: ThemeManager.darkMode
            
            onToggled: {
                ThemeManager.toggleTheme()
                // Settings guarda automàticament!
            }
        }
        
        // Switch per al so
        Switch {
            text: "So activat"
            checked: root.soundEnabled
            
            onToggled: {
                root.soundEnabled = checked
                // Settings guarda automàticament!
            }
        }
        
        Text {
            text: "Partides jugades: " + root.gamesPlayed
            color: ThemeManager.textColor
        }
    }
}

Verificar que funciona

  1. Executa l'aplicació
  2. Canvia el tema a fosc
  3. Tanca l'aplicació
  4. Torna a obrir-la
  5. ✅ El tema hauria de seguir sent fosc!

Veure el fitxer de configuració (Linux)

# Mostra el contingut del fitxer de configuració
cat ~/.config/ExempleOrg/RPSConfig.conf

Veuràs alguna cosa com:

[General]
darkMode=true
sound=true
games=5

Consells pràctics

  1. Usa alias sempre que sigui possible

    Settings {
        property alias darkMode: ThemeManager.darkMode  // Millor
        // En comptes de:
        // property bool darkMode: ThemeManager.darkMode  // Pitjor
    }
    
  2. Agrupa settings relacionades

    Settings {
        category: "Appearance"  // Grup al fitxer de configuració
        property alias darkMode: ThemeManager.darkMode
        property alias fontSize: root.fontSize
    }
    
    Settings {
        category: "Game"
        property alias difficulty: root.difficulty
        property alias gamesPlayed: root.gamesPlayed
    }
    
  3. Proporciona valors per defecte

    property bool soundEnabled: true  // Valor per defecte si no hi ha Settings
    
    Settings {
        property alias sound: root.soundEnabled
    }
    

Recursos addicionals

Documentació oficial Qt6

Bones pràctiques

  1. Usa singletons per a estat global (temes, configuració, etc.)
  2. Usa Settings per a persistència (preferències de l'usuari)
  3. Usa paths relatius per a recursos dins del mòdul
  4. Separa lògica de vista (components reutilitzables)
  5. Fes Clean + Rebuild després de canvis en recursos

Errors comuns

Error Causa Solució
"Cannot open: img/rock.png" Recursos no al CMakeLists.txt Afegeix-los a RESOURCES
"ThemeManager is not defined" Falta declaració singleton Afegeix al qmldir
"Settings no guarda" Falta setOrganizationName Afegeix al main.cpp
Colors [undefined] ThemeManager no és singleton Verifica pragma Singleton

Resum

Has après:

Gestió moderna de recursos sense fitxers .qrc
Paths relatius dins de mòduls QML
Singletons per a estat global compartit
Persistència automàtica amb Settings
Arquitectura modular amb components reutilitzables

Pròxims passos:

  1. Completa els exercicis pràctics
  2. Experimenta amb més funcionalitats de Settings
  3. Crea els teus propis singletons per a altres casos d'ús
  4. Aplica aquests conceptes als teus projectes

Molt bé! Ara tens tots els coneixements per crear aplicacions Qt6 modernes amb gestió de recursos i persistència de dades. 🎉