Guia A08 - Desenvolupament Joc Pedra, Paper, Tisora

Índex

  1. Configuració de QtCreator i selecció de kits
  2. Introducció i requisits
  3. Configuració inicial del projecte
  4. A08.04.1 - Versió amb lletres
  5. A08.04.2 - Versió amb imatges
  6. A08.04.3 - Pantalla de configuració
  7. Mode dia/nit
  8. Proves i verificació
  9. Millores opcionals
  10. Codi de base de referència

0. Configuració de QtCreator i selecció de kits

Abans de començar el projecte, cal configurar correctament QtCreator amb els kits necessaris per compilar per a les diferents plataformes objectiu.

Què és un kit?

Un kit a QtCreator és un conjunt de configuracions que especifica:

Instal·lació de Qt

  1. Descarrega Qt Online Installer des de: https://www.qt.io/download-qt-installer
  2. Executa l'instal·lador i inicia sessió (o crea un compte gratuït)
  3. Selecciona la versió de Qt (recomanat: Qt 6.5 o superior)
  4. Marca les següents opcions:
    • ✅ Qt for Desktop (Linux/Windows/macOS segons el teu SO)
    • ✅ Qt for Android
    • ✅ Qt for WebAssembly
    • ✅ Qt Creator
    • ✅ CMake
    • ✅ Ninja

Configuració del kit Desktop

El kit de Desktop normalment es configura automàticament durant la instal·lació.

Verificació:

  1. Obre QtCreator
  2. Ves a EditPreferences (o ToolsOptions a Windows)
  3. Selecciona Kits
  4. Hauries de veure un kit anomenat similar a:
    • Desktop Qt 6.5.3 GCC 64bit (Linux)
    • Desktop Qt 6.5.3 MinGW 64bit (Windows)
    • Desktop Qt 6.5.3 clang 64bit (macOS)

Si no apareix:

Configuració del kit Android

Per compilar aplicacions per Android, necessites configurar l'SDK d'Android.

Pas 1: Instal·la Android Studio

Pas 2: Configura SDK Tools

A Android Studio:

  1. Obre ToolsSDK Manager
  2. A la pestanya SDK Platforms, marca:
    • Android 13.0 (API Level 33) o superior
  3. A la pestanya SDK Tools, marca:
    • Android SDK Build-Tools
    • Android SDK Command-line Tools
    • Android SDK Platform-Tools
    • NDK (Side by side) - versió 25.x o superior
  4. Clica Apply i espera la descàrrega

Pas 3: Configura QtCreator per Android

A QtCreator:

  1. Ves a EditPreferencesDevicesAndroid
  2. Configura les rutes:
    • JDK Location: Normalment ~/.jdks/ o dins Android Studio
    • Android SDK Location: Ruta on està instal·lat l'SDK (ex: ~/Android/Sdk)
    • Android NDK Location: Dins de l'SDK (~/Android/Sdk/ndk/25.x.x)
  3. Clica Apply

Pas 4: Crea el kit Android

  1. Ves a EditPreferencesKits
  2. Hauries de veure kits d'Android automàticament detectats
  3. Si no, clica Add i configura:
    • Name: Android Qt 6.5.3 Clang
    • Device type: Android Device
    • Compiler: Android Clang (ARM64)
    • Qt version: Selecciona la versió d'Android

Configuració del kit WebAssembly

WebAssembly (Wasm) permet executar aplicacions Qt al navegador.

Pas 1: Instal·la Emscripten

# Linux/macOS
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

# Windows
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
emsdk install latest
emsdk activate latest
emsdk_env.bat

Pas 2: Configura QtCreator per WebAssembly

  1. Ves a EditPreferencesKits
  2. Busca el kit Qt 6.5.3 for WebAssembly (hauria d'aparèixer automàticament)
  3. Si no apareix:
    • Verifica que has instal·lat Qt for WebAssembly des del Qt Installer
    • Afegeix manualment el kit apuntant al compilador d'Emscripten

Pas 3: Verifica la configuració

# Comprova que Emscripten està disponible
emcc --version

Crear un projecte amb múltiples kits

Quan creïs un projecte nou a QtCreator:

  1. FileNew Project
  2. Application (Qt)Qt Quick Application
  3. Tria ubicació i nom del projecte
  4. Build system: Tria CMake
  5. Kit Selection: Marca TOTS els kits que vulguis emprar:
    • ✅ Desktop Qt 6.5.3 (per desenvolupament i proves)
    • ✅ Android Qt 6.5.3 (per generar APK)
    • ✅ WebAssembly Qt 6.5.3 (per executar al navegador)

Això crearà configuracions de compilació per a cada plataforma.

Canviar de kit durant el desenvolupament

  1. A la barra lateral esquerra de QtCreator, clica la icona de l'ordinador (Kit Selector)
  2. Tria el kit desitjat:
    • Desktop: Per desenvolupament ràpid i proves
    • Android: Per generar APK i provar en dispositiu/emulador
    • WebAssembly: Per generar fitxers HTML executables al navegador

Consells importants


1. Introducció i requisits

Objectiu del projecte

Desenvolupar un joc de pedra, paper, tisora amb Qt Quick que inclogui:

Requisits tècnics

Software necessari:

Components de Qt:

Opcional (per altres plataformes):

Plataformes suportades

Aquest projecte funcionarà a:

Coneixements previs necessaris

Per seguir aquesta guia, hauries de tenir coneixements bàsics de:

Si necessites reforçar conceptes, consulta teoriaA08.md abans de continuar.

Estructura final del projecte

rps-game/
├── CMakeLists.txt
├── main.cpp
├── qmldir
├── resources.qrc
├── main.qml
├── GamePage.qml
├── ConfigPage.qml
├── ThemeManager.qml
├── android/
│   ├── AndroidManifest.xml
│   └── res/
│       └── drawable/
│           └── icon.png
└── img/
    ├── rock.png
    ├── paper.png
    ├── scissors.png
    └── settings.png

Temps estimat


2. Configuració inicial del projecte

Pas 1: Crea el projecte a QtCreator

  1. Obre QtCreator
  2. FileNew Project
  3. Selecciona Application (Qt)Qt Quick Application
  4. Clica Choose...
  5. Project name: rps-game
  6. Location: Tria una ubicació adequada
  7. Build system: Selecciona CMake
  8. Kit Selection: Marca els kits desitjats (Desktop, Android, WebAssembly)
  9. Qt Quick Controls Style: Deixa Default o tria Material
  10. Clica Finish

QtCreator crearà l'estructura bàsica del projecte.

Pas 2: Crea l'estructura de directoris

Al terminal o des de QtCreator, crea el directori d'imatges:

cd rps-game
mkdir img

Pas 3: Revisa el CMakeLists.txt generat

QtCreator hauria de generar un CMakeLists.txt similar a aquest:

cmake_minimum_required(VERSION 3.16)

project(rps-game VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Quick)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Quick)

set(PROJECT_SOURCES
        main.cpp
        qml.qrc
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(rps-game
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
else()
    add_executable(rps-game ${PROJECT_SOURCES})
endif()

target_link_libraries(rps-game
  PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick)

# Configuració per Android
if(ANDROID)
    set_property(TARGET rps-game APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
endif()

# Configuració general
set_target_properties(rps-game PROPERTIES
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_import_qml_plugins(rps-game)
    qt_finalize_executable(rps-game)
endif()

Punts clau:

Pas 4: Configura el fitxer de recursos (qml.qrc)

El fitxer qml.qrc hauria d'incloure tots els fitxers QML i imatges.

Modifica qml.qrc (o crea'l si no existeix):

<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>GamePage.qml</file>
        <file>ConfigPage.qml</file>
        <file>ThemeManager.qml</file>
        <file>qmldir</file>
        <file>img/rock.png</file>
        <file>img/paper.png</file>
        <file>img/scissors.png</file>
        <file>img/settings.png</file>
    </qresource>
</RCC>

Nota: Afegeix fitxers a mesura que els creïs. No cal que tots existeixin des del principi.

Pas 5: Verifica main.cpp

El fitxer main.cpp ha de carregar el fitxer QML principal i configurar els identificadors de l'aplicació per a Settings:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

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

    // ✅ IMPORTANT: Configurar identificadors per Settings
    QCoreApplication::setOrganizationName("ExempleOrg");
    QCoreApplication::setOrganizationDomain("exemple.cat");
    QCoreApplication::setApplicationName("RPSGame");

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/main.qml"_qs);
    
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection
    );
    
    engine.load(url);

    return app.exec();
}

⚠️ Nota crítica sobre Settings:
Els identificadors (organizationName, organizationDomain, applicationName) són obligatoris per a que Settings funcioni correctament. Sense aquests, obtindràs errors:

QML Settings: Failed to initialize QSettings instance. Status code is: 1
The following application identifiers have not been set: QList("organizationName", "organizationDomain")

Aquests identificadors determinen on es desen les dades al sistema:

Pas 6: Configuració per Android

Si vols compilar per Android, crea el directori android/:

mkdir android
mkdir android/res
mkdir android/res/drawable

Crea android/AndroidManifest.xml:

<?xml version="1.0"?>
<manifest package="cat.example.rpsgame" 
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:versionName="1.0" 
          android:versionCode="1" 
          android:installLocation="auto">
    
    <application android:name="org.qtproject.qt.android.bindings.QtApplication"
                 android:label="RPS Game"
                 android:icon="@drawable/icon">
        
        <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation"
                  android:name="org.qtproject.qt.android.bindings.QtActivity"
                  android:label="RPS Game"
                  android:screenOrientation="unspecified"
                  android:launchMode="singleTop"
                  android:exported="true">
            
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
    
    <uses-sdk android:minSdkVersion="26" 
              android:targetSdkVersion="33"/>
</manifest>

Punts importants:

Afegeix una icona:

Col·loca una imatge PNG (512x512px recomanat) a:

android/res/drawable/icon.png

Pas 7: Compila i prova

  1. Selecciona el kit Desktop a QtCreator

  2. Clica el botó Build (martell) o prem Ctrl+B

  3. Si hi ha errors, revisa que:

    • Tots els fitxers al .qrc existeixen
    • CMakeLists.txt està ben format
    • Has instal·lat tots els components de Qt necessaris
  4. Executa l'aplicació: Clica el botó Run (triangle verd) o prem Ctrl+R

Hauries de veure una finestra buida o amb el contingut per defecte de main.qml.

Notes sobre WebAssembly

Per compilar per WebAssembly:

  1. Selecciona el kit WebAssembly a QtCreator
  2. Compila el projecte
  3. Els fitxers generats estaran a la carpeta build-*-WebAssembly-*/
  4. Necessitaràs un servidor web per executar-ho:
cd build-*-WebAssembly-*/
python3 -m http.server 8000
  1. Obre el navegador a http://localhost:8000/rps-game.html

3. A08.04.1 - Versió amb lletres

Aquesta secció desenvolupa la versió inicial del joc emprant només text (R, P, S) sense imatges.

Objectiu

Crear una interfície funcional amb:

Pas 1: Crea la finestra principal (main.qml)

Modifica main.qml:

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 6.5

Window {
    id: arrel
    width: 640
    height: 480
    visible: true
    title: "Pedra, Paper, Tisora"
    
    Rectangle {
        id: fons
        anchors.fill: parent
        color: "#AAAAAA"
    }
}

Compila i executa. Hauries de veure una finestra grisa.

Pas 2: Afegeix tres botons amb lletres

Afegeix els botons dins del Rectangle:

Rectangle {
    id: fons
    anchors.fill: parent
    color: "#AAAAAA"
    
    Row {
        id: controls
        anchors.centerIn: parent
        spacing: 20
        
        Button {
            text: "R"
            width: 80
            height: 80
            font.pixelSize: 32
            font.bold: true
            
            onClicked: {
                console.log("Premut R")
            }
        }
        
        Button {
            text: "P"
            width: 80
            height: 80
            font.pixelSize: 32
            font.bold: true
            
            onClicked: {
                console.log("Premut P")
            }
        }
        
        Button {
            text: "S"
            width: 80
            height: 80
            font.pixelSize: 32
            font.bold: true
            
            onClicked: {
                console.log("Premut S")
            }
        }
    }
}

Compila i prova. Els botons haurien de mostrar "R", "P", "S".

Pas 3: Implementa la lògica del joc

Afegeix dues funcions a l'arrel de Window:

Window {
    id: arrel
    // ...
    
    // Genera jugada aleatòria de l'aplicació
    function jugadaAleatoria() {
        var opcions = ["R", "P", "S"]
        return opcions[Math.floor(Math.random() * opcions.length)]
    }
    
    // Determina el resultat
    function resultatJugada(jugador, aplicacio) {
        if (jugador === aplicacio) return "empat"
        
        if (jugador === "R") {
            if (aplicacio === "S") return "guanya"
            if (aplicacio === "P") return "perd"
        }
        if (jugador === "P") {
            if (aplicacio === "R") return "guanya"
            if (aplicacio === "S") return "perd"
        }
        if (jugador === "S") {
            if (aplicacio === "P") return "guanya"
            if (aplicacio === "R") return "perd"
        }
        
        return "empat"
    }
}

Pas 4: Mostra les jugades i el resultat

Afegeix elements per mostrar les jugades:

Rectangle {
    id: fons
    anchors.fill: parent
    color: "#AAAAAA"
    
    Column {
        anchors.top: parent.top
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.margins: 20
        spacing: 10
        
        Label {
            text: "Tu: " + jugadaUsuari
            font.pixelSize: 24
            anchors.horizontalCenter: parent.horizontalCenter
        }
        
        Label {
            text: "App: " + jugadaApp
            font.pixelSize: 24
            anchors.horizontalCenter: parent.horizontalCenter
        }
        
        Label {
            id: resultat
            text: missatgeResultat
            font.pixelSize: 28
            font.bold: true
            anchors.horizontalCenter: parent.horizontalCenter
        }
    }
    
    // Propietats per desar l'estat
    property string jugadaUsuari: ""
    property string jugadaApp: ""
    property string missatgeResultat: "Tria una opció"
    
    // ... (botons aquí)
}

Pas 5: Connecta els botons amb la lògica

Modifica els botons per cridar la lògica del joc:

Button {
    text: "R"
    width: 80
    height: 80
    font.pixelSize: 32
    font.bold: true
    
    onClicked: {
        fons.jugadaUsuari = "R"
        fons.jugadaApp = arrel.jugadaAleatoria()
        
        var res = arrel.resultatJugada(fons.jugadaUsuari, fons.jugadaApp)
        
        if (res === "guanya") {
            fons.missatgeResultat = "Has guanyat!"
        } else if (res === "perd") {
            fons.missatgeResultat = "Has perdut!"
        } else {
            fons.missatgeResultat = "Empat!"
        }
    }
}

Repeteix per als botons "P" i "S" canviant "R" per "P" i "S" respectivament.

Pas 6: Canvia el color de fons segons el resultat

Afegeix estats al Rectangle de fons:

Rectangle {
    id: fons
    anchors.fill: parent
    color: "#AAAAAA"
    
    // ... (contingut aquí)
    
    states: [
        State {
            name: "guanya"
            PropertyChanges {
                target: fons
                color: "lime"
            }
        },
        State {
            name: "perd"
            PropertyChanges {
                target: fons
                color: "lightcoral"
            }
        },
        State {
            name: "empat"
            PropertyChanges {
                target: fons
                color: "cyan"
            }
        }
    ]
    
    transitions: [
        Transition {
            from: "*"
            to: "*"
            ColorAnimation {
                duration: 500
            }
        }
    ]
}

Modifica els botons per canviar l'estat:

onClicked: {
    fons.jugadaUsuari = "R"
    fons.jugadaApp = arrel.jugadaAleatoria()
    
    var res = arrel.resultatJugada(fons.jugadaUsuari, fons.jugadaApp)
    fons.state = res  // Canvia l'estat
    
    if (res === "guanya") {
        fons.missatgeResultat = "Has guanyat!"
    } else if (res === "perd") {
        fons.missatgeResultat = "Has perdut!"
    } else {
        fons.missatgeResultat = "Empat!"
    }
}

Resultat esperat

Ara hauries de tenir:

Compila i prova totes les combinacions!


4. A08.04.2 - Versió amb imatges

Ara substituirem els botons de text per imatges gràfiques del joc.

Objectiu

Pas 1: Obt obtain/crea les imatges

Necessites quatre imatges:

Opcions:

  1. Crea les teves pròpies amb eines com Inkscape, GIMP, etc.
  2. Descarrega imatges lliures de:
  3. Empra les imatges del projecte adjunt (si en tens accés)

Requisits:

Desa les imatges al directori img/ del projecte.

Pas 2: Afegeix les imatges al fitxer de recursos

Modifica qml.qrc:

<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>img/rock.png</file>
        <file>img/paper.png</file>
        <file>img/scissors.png</file>
        <file>img/settings.png</file>
    </qresource>
</RCC>

Pas 3: Crea component d'imatge clicable

Crea un nou fitxer QML: BotoImatge.qml

// BotoImatge.qml
import QtQuick 2.15

Item {
    id: botoImg
    
    // Propietats públiques
    property string opcio: "R"
    property string imatgeSource: ""
    
    // Signal quan es clica
    signal clicat(string opcio)
    
    width: 100
    height: 100
    
    Rectangle {
        id: contenidor
        anchors.fill: parent
        color: "orange"
        radius: 50
        border.color: "black"
        border.width: 2
        
        Image {
            source: botoImg.imatgeSource
            anchors.fill: parent
            anchors.margins: 10
            fillMode: Image.PreserveAspectFit
            smooth: true
        }
        
        MouseArea {
            anchors.fill: parent
            onClicked: {
                botoImg.clicat(botoImg.opcio)
            }
        }
    }
}

Afegeix BotoImatge.qml al fitxer qml.qrc.

Pas 4: Substitueix els botons per imatges

Modifica la Row de controls a main.qml:

Row {
    id: controls
    anchors.centerIn: parent
    spacing: 20
    
    BotoImatge {
        opcio: "R"
        imatgeSource: "qrc:/img/rock.png"
        
        onClicat: function(opc) {
            processarJugada(opc)
        }
    }
    
    BotoImatge {
        opcio: "P"
        imatgeSource: "qrc:/img/paper.png"
        
        onClicat: function(opc) {
            processarJugada(opc)
        }
    }
    
    BotoImatge {
        opcio: "S"
        imatgeSource: "qrc:/img/scissors.png"
        
        onClicat: function(opc) {
            processarJugada(opc)
        }
    }
}

Pas 5: Afegeix funció per processar jugada

A la Window, afegeix:

Window {
    id: arrel
    // ...
    
    function processarJugada(opcioUsuari) {
        fons.jugadaUsuari = opcioUsuari
        fons.jugadaApp = jugadaAleatoria()
        
        // Converteix opcions a imatges
        fons.imatgeUsuari = imatgePerOpcio(opcioUsuari)
        fons.imatgeApp = imatgePerOpcio(fons.jugadaApp)
        
        var res = resultatJugada(opcioUsuari, fons.jugadaApp)
        fons.state = res
        
        if (res === "guanya") {
            fons.missatgeResultat = "Has guanyat!"
        } else if (res === "perd") {
            fons.missatgeResultat = "Has perdut!"
        } else {
            fons.missatgeResultat = "Empat!"
        }
    }
    
    function imatgePerOpcio(opcio) {
        if (opcio === "R") return "qrc:/img/rock.png"
        if (opcio === "P") return "qrc:/img/paper.png"
        if (opcio === "S") return "qrc:/img/scissors.png"
        return ""
    }
}

Pas 6: Mostra imatges de les jugades

Modifica la Column superior per mostrar imatges:

Column {
    anchors.top: parent.top
    anchors.horizontalCenter: parent.horizontalCenter
    anchors.margins: 30
    spacing: 20
    
    // Jugada usuari
    Column {
        anchors.horizontalCenter: parent.horizontalCenter
        spacing: 5
        
        Label {
            text: "Tu"
            font.pixelSize: 20
            anchors.horizontalCenter: parent.horizontalCenter
        }
        
        Image {
            source: fons.imatgeUsuari
            width: 80
            height: 80
            fillMode: Image.PreserveAspectFit
        }
    }
    
    // Jugada aplicació
    Column {
        anchors.horizontalCenter: parent.horizontalCenter
        spacing: 5
        
        Label {
            text: "Aplicació"
            font.pixelSize: 20
            anchors.horizontalCenter: parent.horizontalCenter
        }
        
        Image {
            source: fons.imatgeApp
            width: 80
            height: 80
            fillMode: Image.PreserveAspectFit
        }
    }
    
    Label {
        text: missatgeResultat
        font.pixelSize: 28
        font.bold: true
        anchors.horizontalCenter: parent.horizontalCenter
    }
}

// Afegeix propietats al Rectangle fons
property string imatgeUsuari: ""
property string imatgeApp: ""

Resultat esperat

Ara hauries de tenir:


5. A08.04.3 - Pantalla de configuració

Ara afegirem una segona pantalla amb estadístiques i configuració.

Objectiu

Pas 1: Implementa SwipeView i PageIndicator

Modifica main.qml per emprar SwipeView:

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 6.5
import Qt.labs.settings 1.0

Window {
    id: arrel
    width: 640
    height: 480
    visible: true
    title: "Pedra, Paper, Tisora"
    
    // Settings per persistència
    Settings {
        id: config
        category: "estadistiques"
        property int guanyades: 0
        property int perdudes: 0
    }
    
    SwipeView {
        id: vista
        anchors.fill: parent
        currentIndex: 0
        
        // Pàgina 1: Joc
        Page {
            id: paginaJoc
            
            // Contingut del joc aquí (moure tot el codi anterior)
        }
        
        // Pàgina 2: Configuració
        Page {
            id: paginaConfig
            background: Rectangle {
                color: "#FE6158"
            }
            
            Label {
                text: "Configuració"
                font.pixelSize: 36
                font.bold: true
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.top: parent.top
                anchors.margins: 20
            }
        }
    }
    
    PageIndicator {
        count: vista.count
        currentIndex: vista.currentIndex
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
    }
    
    // Funcions del joc (mantenir aquí)
    function jugadaAleatoria() { /* ... */ }
    function resultatJugada(jugador, aplicacio) { /* ... */ }
    function imatgePerOpcio(opcio) { /* ... */ }
    function processarJugada(opcioUsuari) { /* ... */ }
}

Pas 2: Separa la pàgina de joc en component

Crea GamePage.qml:

// GamePage.qml
import QtQuick 2.15
import QtQuick.Controls 6.5

Item {
    id: joc
    
    signal partidaGuanyada()
    signal partidaPerduda()
    
    Rectangle {
        id: fons
        anchors.fill: parent
        color: "#AAAAAA"
        
        property string jugadaUsuari: ""
        property string jugadaApp: ""
        property string imatgeUsuari: ""
        property string imatgeApp: ""
        property string missatgeResultat: "Tria una opció"
        
        // Column amb imatges de jugades
        Column {
            anchors.top: parent.top
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.margins: 30
            spacing: 20
            
            // (Codi de les imatges aquí)
        }
        
        // Row amb botons
        Row {
            id: controls
            anchors.centerIn: parent
            spacing: 20
            
            // (BotoImatge aquí)
        }
        
        // Estats i transicions
        states: [ /* ... */ ]
        transitions: [ /* ... */ ]
    }
    
    function processarJugada(opcioUsuari, opcioApp) {
        fons.jugadaUsuari = opcioUsuari
        fons.jugadaApp = opcioApp
        
        fons.imatgeUsuari = imatgePerOpcio(opcioUsuari)
        fons.imatgeApp = imatgePerOpcio(opcioApp)
        
        var res = determinarResultat(opcioUsuari, opcioApp)
        fons.state = res
        
        if (res === "guanya") {
            fons.missatgeResultat = "Has guanyat!"
            partidaGuanyada()
        } else if (res === "perd") {
            fons.missatgeResultat = "Has perdut!"
            partidaPerduda()
        } else {
            fons.missatgeResultat = "Empat!"
        }
    }
    
    function determinarResultat(j, a) {
        if (j === a) return "empat"
        if (j === "R" && a === "S") return "guanya"
        if (j === "R" && a === "P") return "perd"
        if (j === "P" && a === "R") return "guanya"
        if (j === "P" && a === "S") return "perd"
        if (j === "S" && a === "P") return "guanya"
        if (j === "S" && a === "R") return "perd"
        return "empat"
    }
    
    function imatgePerOpcio(opcio) {
        if (opcio === "R") return "qrc:/img/rock.png"
        if (opcio === "P") return "qrc:/img/paper.png"
        if (opcio === "S") return "qrc:/img/scissors.png"
        return ""
    }
}

Nota: Recorda afegir GamePage.qml a qml.qrc.

Pas 3: Empra GamePage a main.qml

Page {
    id: paginaJoc
    
    GamePage {
        id: gameContent
        anchors.fill: parent
        
        Connections {
            function onPartidaGuanyada() {
                config.guanyades += 1
            }
            function onPartidaPerduda() {
                config.perdudes += 1
            }
        }
    }
}

Pas 4: Afegeix botó de configuració al joc

A GamePage.qml, afegeix el botó de settings:

Rectangle {
    id: fons
    // ...
    
    Rectangle {
        id: btSettings
        anchors.left: parent.left
        anchors.top: parent.top
        anchors.margins: 10
        width: 50
        height: 50
        radius: 10
        color: "white"
        border.color: "black"
        border.width: 1
        
        Image {
            anchors.centerIn: parent
            source: "qrc:/img/settings.png"
            width: 30
            height: 30
            fillMode: Image.PreserveAspectFit
        }
        
        MouseArea {
            anchors.fill: parent
            onClicked: {
                // Aquesta funció es connectarà des de main.qml
            }
        }
    }
}

Modifica main.qml per connectar el botó:

GamePage {
    id: gameContent
    anchors.fill: parent
    
    // Accedeix al MouseArea del botó settings
    Component.onCompleted: {
        var btSettings = gameContent.children[0].children.find(
            child => child.objectName === "btSettings"
        )
        // Millor: empra un signal des de GamePage
    }
}

Millor solució: Afegeix un signal a GamePage.qml:

// GamePage.qml
Item {
    id: joc
    
    signal partidaGuanyada()
    signal partidaPerduda()
    signal configuracioClicada()  // Nou signal
    
    // ...
    
    MouseArea {
        anchors.fill: parent
        onClicked: {
            configuracioClicada()
        }
    }
}

I connecta a main.qml:

GamePage {
    id: gameContent
    
    Connections {
        function onConfiguracioClicada() {
            vista.currentIndex = 1
        }
        function onPartidaGuanyada() {
            config.guanyades += 1
        }
        function onPartidaPerduda() {
            config.perdudes += 1
        }
    }
}

Pas 5: Crea ConfigPage.qml

// ConfigPage.qml
import QtQuick 2.15
import QtQuick.Controls 6.5

Item {
    id: configPage
    
    property int guanyades: 0
    property int perdudes: 0
    
    signal tornarAlJoc()
    
    Rectangle {
        anchors.fill: parent
        color: "#FE6158"
        
        // Botó de tornar
        Rectangle {
            id: btTornar
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.margins: 10
            width: 50
            height: 50
            radius: 10
            color: "white"
            border.color: "black"
            border.width: 1
            
            Label {
                text: "<<"
                anchors.centerIn: parent
                font.pixelSize: 20
                font.bold: true
            }
            
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    tornarAlJoc()
                }
            }
        }
        
        // Títol
        Label {
            id: titol
            text: "Configuració"
            font.pixelSize: 36
            font.bold: true
            color: "darkblue"
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.top: parent.top
            anchors.margins: 20
        }
        
        // Estadístiques
        Column {
            anchors.centerIn: parent
            spacing: 20
            
            Label {
                text: "Partides guanyades: " + guanyades
                font.pixelSize: 24
                anchors.horizontalCenter: parent.horizontalCenter
            }
            
            Label {
                text: "Partides perdudes: " + perdudes
                font.pixelSize: 24
                anchors.horizontalCenter: parent.horizontalCenter
            }
            
            Button {
                text: "Reiniciar estadístiques"
                anchors.horizontalCenter: parent.horizontalCenter
                
                onClicked: {
                    // Aquest signal es connectarà des de main.qml
                }
            }
        }
    }
}

Pas 6: Integra ConfigPage a main.qml

Page {
    id: paginaConfig
    
    ConfigPage {
        id: configContent
        anchors.fill: parent
        guanyades: config.guanyades
        perdudes: config.perdudes
        
        Connections {
            function onTornarAlJoc() {
                vista.currentIndex = 0
            }
        }
    }
}

Pas 7: Implementa botó de reset

A ConfigPage.qml, afegeix signal:

Item {
    id: configPage
    
    signal tornarAlJoc()
    signal reiniciarEstadistiques()  // Nou
    
    // ...
    
    Button {
        text: "Reiniciar estadístiques"
        onClicked: {
            reiniciarEstadistiques()
        }
    }
}

Connecta a main.qml:

ConfigPage {
    id: configContent
    
    Connections {
        function onTornarAlJoc() {
            vista.currentIndex = 0
        }
        function onReiniciarEstadistiques() {
            config.guanyades = 0
            config.perdudes = 0
        }
    }
}

Resultat esperat

Ara hauries de tenir:


6. Mode dia/nit

Implementarem un sistema de temes que permeti canviar entre mode clar i fosc.

Objectiu

Pas 1: Crea ThemeManager.qml (singleton)

// ThemeManager.qml
pragma Singleton
import QtQuick 2.15

QtObject {
    id: themeManager
    
    property bool darkMode: false
    
    // Colors mode clar
    property color lightBackground: "#FFFFFF"
    property color lightText: "#000000"
    property color lightAccent: "#0066CC"
    property color lightSecondary: "#F5F5F5"
    property color lightButton: "#E0E0E0"
    
    // Colors mode fosc
    property color darkBackground: "#1E1E1E"
    property color darkText: "#FFFFFF"
    property color darkAccent: "#3DAEE9"
    property color darkSecondary: "#2D2D2D"
    property color darkButton: "#404040"
    
    // Colors actius
    property color backgroundColor: darkMode ? darkBackground : lightBackground
    property color textColor: darkMode ? darkText : lightText
    property color accentColor: darkMode ? darkAccent : lightAccent
    property color secondaryColor: darkMode ? darkSecondary : lightSecondary
    property color buttonColor: darkMode ? darkButton : lightButton
    
    function toggleTheme() {
        darkMode = !darkMode
    }
}

Pas 2: Crea fitxer qmldir

Al directori arrel del projecte, crea qmldir:

singleton ThemeManager 1.0 ThemeManager.qml

Afegeix ambdós fitxers a qml.qrc:

<file>qmldir</file>
<file>ThemeManager.qml</file>

Pas 3: Aplica ThemeManager a GamePage

Modifica GamePage.qml:

import QtQuick 2.15
import QtQuick.Controls 6.5
import "."  // Importa el directori actual

Item {
    id: joc
    
    Rectangle {
        id: fons
        anchors.fill: parent
        color: ThemeManager.backgroundColor
        
        // Anima el canvi de color
        Behavior on color {
            ColorAnimation {
                duration: 300
                easing.type: Easing.InOutQuad
            }
        }
        
        // Textos amb color del tema
        Label {
            text: "Tu"
            color: ThemeManager.textColor
            // ...
        }
        
        Label {
            text: "Aplicació"
            color: ThemeManager.textColor
            // ...
        }
        
        Label {
            text: missatgeResultat
            color: ThemeManager.textColor
            // ...
        }
    }
    
    // Mantén els estats per guanyar/perdre
    // però canvia els colors base
    states: [
        State {
            name: "guanya"
            PropertyChanges {
                target: fons
                color: "lime"
            }
        },
        State {
            name: "perd"
            PropertyChanges {
                target: fons
                color: "lightcoral"
            }
        },
        State {
            name: "empat"
            PropertyChanges {
                target: fons
                color: "cyan"
            }
        },
        State {
            name: ""  // Estat per defecte
            PropertyChanges {
                target: fons
                color: ThemeManager.backgroundColor
            }
        }
    ]
}

Pas 4: Aplica ThemeManager a ConfigPage

import QtQuick 2.15
import QtQuick.Controls 6.5
import "."

Item {
    id: configPage
    
    Rectangle {
        anchors.fill: parent
        color: ThemeManager.secondaryColor
        
        Behavior on color {
            ColorAnimation { duration: 300 }
        }
        
        Label {
            id: titol
            text: "Configuració"
            color: ThemeManager.textColor
            // ...
        }
        
        Label {
            text: "Partides guanyades: " + guanyades
            color: ThemeManager.textColor
            // ...
        }
        
        Label {
            text: "Partides perdudes: " + perdudes
            color: ThemeManager.textColor
            // ...
        }
    }
}

Pas 5: Afegeix Switch per canviar tema

A ConfigPage.qml, afegeix:

Column {
    anchors.centerIn: parent
    spacing: 20
    
    // Switch de mode fosc
    Row {
        spacing: 10
        anchors.horizontalCenter: parent.horizontalCenter
        
        Label {
            text: "Mode fosc"
            color: ThemeManager.textColor
            font.pixelSize: 20
            anchors.verticalCenter: parent.verticalCenter
        }
        
        Switch {
            id: switchTema
            checked: ThemeManager.darkMode
            
            onToggled: {
                ThemeManager.toggleTheme()
            }
        }
    }
    
    // Estadístiques
    Label {
        text: "Partides guanyades: " + guanyades
        color: ThemeManager.textColor
        font.pixelSize: 24
        anchors.horizontalCenter: parent.horizontalCenter
    }
    
    // ... resta d'elements
}

Pas 6: Desa preferència de tema amb Settings

⚠️ Important: No es pot emprar alias per enllaçar Settings amb un singleton. Els alias només funcionen amb objectes locals que tenen id. Cal sincronitzar manualment.

A main.qml, modifica Settings:

Settings {
    id: config
    category: "estadistiques"
    property int guanyades: 0
    property int perdudes: 0
}

Settings {
    id: configVisual
    category: "visual"
    property bool darkMode: false  // NO és alias
}

// Sincronització manual amb ThemeManager
Component.onCompleted: {
    // Carrega el valor desat en iniciar
    ThemeManager.darkMode = configVisual.darkMode
}

Connections {
    target: ThemeManager
    
    function onDarkModeChanged() {
        // Desa quan ThemeManager canvia
        configVisual.darkMode = ThemeManager.darkMode
    }
}

Importa ThemeManager a main.qml:

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 6.5
import Qt.labs.settings 1.0
import "."  // Importa ThemeManager

Window {
    // ...
}

Per què no funciona l'alias?

// ❌ Això NO funciona:
property alias darkMode: ThemeManager.darkMode

// Error: "Invalid alias reference. Unable to find id 'ThemeManager'"

Els alias en QML només poden referenciar objectes locals amb id, no singletons globals. Per enllaçar Settings amb un singleton, cal sincronització bidireccional manual amb Connections.

Pas 7: Actualitza imports de Settings (Qt 6.7+)

⚠️ Advertència de deprecació: Si empres Qt 6.7 o superior, Qt.labs.settings està deprecated.

Si tens Qt 6.5 o 6.6 (mantenir):

import Qt.labs.settings 1.0

Si tens Qt 6.7 o superior (actualitzar):

// Abans:
import Qt.labs.settings 1.0

// Després:
import QtCore

// El codi de Settings queda exactament igual
Settings {
    id: config
    category: "estadistiques"
    property int guanyades: 0
    property int perdudes: 0
}

Missatge de warning si no actualitzes:

QML Settings: The Settings type from Qt.labs.settings is deprecated 
and will be removed in a future release. Please use the one from QtCore instead.

L'API és idèntica, només canvia l'import. No cal modificar cap codi de Settings.

Resultat esperat

Ara hauries de tenir:


7. Proves i verificació

Abans de donar el projecte per finalitzat, verifica que tot funcioni correctament.

Proves funcionals

Joc bàsic

Navegació

Persistència

Tema

Proves per plataforma

Desktop (Linux/Windows/macOS)

  1. Selecciona el kit Desktop a QtCreator
  2. Compila el projecte (Ctrl+B)
  3. Executa l'aplicació (Ctrl+R)
  4. Verifica totes les funcionalitats
  5. Prova redimensionar la finestra (hauria d'adaptar-se)
  6. Tanca i torna a obrir (verifica persistència)

WebAssembly

  1. Selecciona el kit WebAssembly a QtCreator
  2. Compila el projecte (pot trigar més)
  3. Els fitxers generats estaran a build-*-WebAssembly-*/
  4. Executa un servidor web:
    cd build-rps-game-WebAssembly-Release/
    python3 -m http.server 8000
    
  5. Obre el navegador a http://localhost:8000/rps-game.html
  6. Verifica que Settings funciona (empra localStorage del navegador)
  7. Prova en diferents navegadors (Chrome, Firefox, Safari)

Notes WebAssembly:

Android

  1. Connecta un dispositiu Android o inicia un emulador
  2. Selecciona el kit Android a QtCreator
  3. Compila el projecte (generarà APK)
  4. Instal·la i executa en el dispositiu
  5. Verifica:
    • Touch funciona correctament
    • SwipeView llisca suaument
    • Orientació (retrat/paisatge)
    • Estadístiques es desen localment
    • No hi ha crashes

Solució de problemes Android:

Tests de qualitat

Accessibilitat

Rendiment

Errors comuns a evitar

Solució de problemes comuns

⚠️ Warning: "depends on non-bindable properties: transitions"

Problema:

QQmlExpression: Expression depends on non-bindable properties:
    QQuickImage::transitions

Apareix quan es referencia transitions[0] en un Connections.

Causa:
Accedir a transitions per índex no és "bindable" (no pot notificar canvis).

Solució:
Dona un id a la Transition i referencia-la directament:

// ❌ Abans (causa warning):
transitions: [
    Transition {
        from: "res"
        to: "novaRecepcio"
        NumberAnimation { /* ... */ }
    }
]

Connections {
    target: darreraImatge.transitions[0]  // ❌ No bindable
    
    function onRunningChanged() {
        if (darreraImatge.transitions[0].running === false) {
            // ...
        }
    }
}

// ✅ Després (correcte):
transitions: [
    Transition {
        id: transicioImatge  // ✅ Afegir id
        from: "res"
        to: "novaRecepcio"
        NumberAnimation { /* ... */ }
    }
]

Connections {
    target: transicioImatge  // ✅ Emprar l'id
    
    function onRunningChanged() {
        if (!transicioImatge.running) {  // ✅ També aquí
            // ...
        }
    }
}

🔴 Error: "Failed to initialize QSettings instance"

Problema:

QML Settings: Failed to initialize QSettings instance. Status code is: 1
The following application identifiers have not been set: 
QList("organizationName", "organizationDomain")

Causa:
Settings necessita identificadors d'aplicació per saber on desar les dades.

Solució:
Afegeix a main.cpp abans de crear el QQmlApplicationEngine:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    
    // ✅ Afegir aquestes línies
    QCoreApplication::setOrganizationName("ExempleOrg");
    QCoreApplication::setOrganizationDomain("exemple.cat");
    QCoreApplication::setApplicationName("RPSGame");
    
    QQmlApplicationEngine engine;
    // ...
}

⚠️ Warning: "Settings type from Qt.labs.settings is deprecated"

Problema:

QML Settings: The Settings type from Qt.labs.settings is deprecated 
and will be removed in a future release. 
Please use the one from QtCore instead.

Causa:
A Qt 6.7+, Qt.labs.settings està deprecated.

Solució:

// Abans (Qt 6.5/6.6):
import Qt.labs.settings 1.0

// Després (Qt 6.7+):
import QtCore

// El codi de Settings NO canvia
Settings {
    id: config
    // ...
}

🔴 Error: "Invalid alias reference. Unable to find id 'ThemeManager'"

Problema:

property alias darkMode: ThemeManager.darkMode
// Error: Invalid alias reference. Unable to find id "ThemeManager"

Causa:
Els alias només poden referenciar objectes locals amb id, no singletons.

Solució:
Empra sincronització manual (vegeu secció 6, Pas 6).

Checklist final


8. Millores opcionals

Un cop completat el projecte bàsic, pots afegir funcionalitats extra per aprendre més i millorar l'aplicació.

8.1 Animacions avançades

Descripció: Afegeix efectes visuals més sofisticats.

Implementació:

// Animació de rotació en revelar jugada
Image {
    id: imatgeJugada
    
    property int angleRotacio: 0
    
    transform: Rotation {
        origin.x: imatgeJugada.width / 2
        origin.y: imatgeJugada.height / 2
        axis { x: 0; y: 1; z: 0 }
        angle: imatgeJugada.angleRotacio
    }
    
    SequentialAnimation {
        id: animacioRevelar
        NumberAnimation {
            target: imatgeJugada
            property: "angleRotacio"
            from: 0
            to: 180
            duration: 500
        }
    }
}

// Shake animation quan es perd
SequentialAnimation {
    id: animacioShake
    loops: 3
    NumberAnimation {
        target: fons
        property: "x"
        from: 0; to: -10
        duration: 50
    }
    NumberAnimation {
        target: fons
        property: "x"
        from: -10; to: 10
        duration: 50
    }
    NumberAnimation {
        target: fons
        property: "x"
        from: 10; to: 0
        duration: 50
    }
}

Temps estimat: 1-2 hores

8.2 Efectes de so

Descripció: Afegeix feedback auditiu.

Implementació:

import QtMultimedia 5.15

Item {
    SoundEffect {
        id: soClick
        source: "qrc:/sounds/click.wav"
    }
    
    SoundEffect {
        id: soGuanyar
        source: "qrc:/sounds/win.wav"
    }
    
    SoundEffect {
        id: soPerdre
        source: "qrc:/sounds/lose.wav"
    }
    
    Button {
        onClicked: {
            soClick.play()
            // ...
        }
    }
}

Recursos:

Temps estimat: 1 hora

8.3 Històric de partides

Descripció: Mostra les últimes 10 partides jugades.

Implementació:

Settings {
    id: config
    property string historicJSON: "[]"
}

function afegirPartida(usuari, app, resultat) {
    var historic = JSON.parse(config.historicJSON)
    
    historic.unshift({
        usuari: usuari,
        app: app,
        resultat: resultat,
        data: new Date().toLocaleString()
    })
    
    if (historic.length > 10) {
        historic = historic.slice(0, 10)
    }
    
    config.historicJSON = JSON.stringify(historic)
}

ListView {
    model: JSON.parse(config.historicJSON)
    
    delegate: Row {
        Label { text: modelData.usuari }
        Label { text: "vs" }
        Label { text: modelData.app }
        Label { text: modelData.resultat }
    }
}

Temps estimat: 2 hores

8.4 Multijugador local

Descripció: Mode per 2 jugadors humans.

Implementació:

Switch {
    text: "Mode 2 jugadors"
    checked: modeDosJugadors
}

property bool modeDosJugadors: false
property int torn: 1  // 1 o 2

function processarJugada(opcio) {
    if (modeDosJugadors) {
        if (torn === 1) {
            jugador1 = opcio
            torn = 2
            missatge = "Torn del Jugador 2"
        } else {
            jugador2 = opcio
            determinarGuanyador(jugador1, jugador2)
            torn = 1
        }
    } else {
        // Mode normal vs CPU
    }
}

Temps estimat: 2-3 hores

8.5 Millor de N partides

Descripció: Juga fins guanyar 3, 5 o 7 partides.

Implementació:

SpinBox {
    id: partidesPerGuanyar
    from: 3
    to: 7
    stepSize: 2
    value: 3
}

property int victoriesJugador: 0
property int victoriesApp: 0

function verificarFinal() {
    if (victoriesJugador >= partidesPerGuanyar.value) {
        mostrarDialogVictoria("Jugador")
        reiniciarSerie()
    } else if (victoriesApp >= partidesPerGuanyar.value) {
        mostrarDialogVictoria("Aplicació")
        reiniciarSerie()
    }
}

Temps estimat: 1-2 hores

8.6 Estadístiques avançades

Descripció: Gràfics i més dades.

Implementació:

Column {
    Label {
        text: "Percentatge victòries: " + 
              (guanyades / (guanyades + perdudes) * 100).toFixed(1) + "%"
    }
    
    Label {
        text: "Ratxa actual: " + ratxaActual
    }
    
    Label {
        text: "Opció més jugada: " + opcioMesJugada
    }
}

// Amb Qt Charts (cal afegir al CMakeLists.txt)
import QtCharts 2.15

ChartView {
    PieSeries {
        PieSlice { label: "Guanyades"; value: guanyades }
        PieSlice { label: "Perdudes"; value: perdudes }
        PieSlice { label: "Empats"; value: empats }
    }
}

Temps estimat: 3-4 hores

8.7 Vibració en Android

Descripció: Feedback hàptic al dispositiu.

Implementació:

// main.cpp
#include <QVibrateEffect>  // Qt 6.2+

// O amb JNI (més compatible):
#ifdef Q_OS_ANDROID
#include <QtAndroid>

void vibrar(int milliseconds) {
    QAndroidJniObject vibratorString = QAndroidJniObject::fromString("vibrator");
    QAndroidJniObject activity = QtAndroid::androidActivity();
    QAndroidJniObject vibrator = activity.callObjectMethod(
        "getSystemService",
        "(Ljava/lang/String;)Ljava/lang/Object;",
        vibratorString.object<jstring>()
    );
    
    if (vibrator.isValid()) {
        vibrator.callMethod<void>("vibrate", "(J)V", jlong(milliseconds));
    }
}
#endif

AndroidManifest.xml:

<uses-permission android:name="android.permission.VIBRATE"/>

Temps estimat: 2-3 hores

8.8 Internacionalització

Descripció: Suport multi-idioma.

Implementació:

// Empra qsTr() per tots els textos
Label {
    text: qsTr("Has guanyat!")
}

Button {
    text: qsTr("Reiniciar")
}

Generar fitxers de traducció:

lupdate rps-game.pro -ts translations/ca.ts
lupdate rps-game.pro -ts translations/es.ts
lupdate rps-game.pro -ts translations/en.ts

Qt Linguist per traduir.

Temps estimat: 3-4 hores

8.9 IA millorada

Descripció: CPU que aprèn dels patrons del jugador.

Implementació:

property var historialJugador: []

function jugadaIA() {
    // Analitza patrons
    if (historialJugador.length >= 3) {
        var ultimaTres = historialJugador.slice(-3)
        
        // Si el jugador repeteix patró
        if (ultimaTres[0] === ultimaTres[1] && 
            ultimaTres[1] === ultimaTres[2]) {
            return contraOpcio(ultimaTres[2])
        }
    }
    
    // Sinó, aleatori
    return jugadaAleatoria()
}

function contraOpcio(opcio) {
    if (opcio === "R") return "P"  // Paper guanya pedra
    if (opcio === "P") return "S"  // Tisora guanya paper
    if (opcio === "S") return "R"  // Pedra guanya tisora
}

Temps estimat: 2-3 hores

8.10 Material Design o estil iOS

Descripció: Empra els temes nadius de Qt.

Implementació:

import QtQuick.Controls.Material 2.15

ApplicationWindow {
    Material.theme: Material.Dark
    Material.accent: Material.Purple
}

O per iOS:

import QtQuick.Controls.iOS 2.15

CMakeLists.txt:

find_package(Qt6 COMPONENTS QuickControls2)
target_link_libraries(rps-game Qt6::QuickControls2)

Temps estimat: 1-2 hores


9. Codi de base de referència

Aquesta secció proporciona el codi mínim necessari per començar el projecte. NO és la solució completa de l'exercici, sinó una base sobre la qual construir.

Estructura mínima del projecte

rps-game/
├── CMakeLists.txt          # Configuració del projecte
├── main.cpp                # Punt d'entrada
├── main.qml                # Finestra principal (buida)
├── qmldir                  # Registre de singletons
├── resources.qrc           # Fitxer de recursos (buit)
└── img/                    # Directori per imatges (buit)

main.cpp (codi complet)

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    
    // Configura organització i nom (per Settings)
    QCoreApplication::setOrganizationName("ExempleOrg");
    QCoreApplication::setApplicationName("RPSGame");

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/main.qml"_qs);
    
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection
    );
    
    engine.load(url);

    return app.exec();
}

main.qml (esquelet bàsic)

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 6.5

Window {
    id: arrel
    width: 640
    height: 480
    visible: true
    title: "Pedra, Paper, Tisora"
    
    Rectangle {
        anchors.fill: parent
        color: "#AAAAAA"
        
        Label {
            text: "Començar aquí el teu joc"
            anchors.centerIn: parent
            font.pixelSize: 24
        }
    }
}

CMakeLists.txt (configuració bàsica)

cmake_minimum_required(VERSION 3.16)

project(rps-game VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Quick)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Quick)

set(PROJECT_SOURCES
    main.cpp
    resources.qrc
)

qt_add_executable(rps-game
    MANUAL_FINALIZATION
    ${PROJECT_SOURCES}
)

target_link_libraries(rps-game
    PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick
)

# Configuració per Android
if(ANDROID)
    set_property(TARGET rps-game APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
endif()

set_target_properties(rps-game PROPERTIES
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

qt_finalize_executable(rps-game)

resources.qrc (estructura inicial)

<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
    </qresource>
</RCC>

Exemples de conceptes aïllats

Exemple 1: Crear un botó clickable amb imatge

// BotoImatge.qml
import QtQuick 2.15

Rectangle {
    id: boto
    width: 80
    height: 80
    radius: 40
    color: "orange"
    
    signal clicat()
    
    Image {
        source: "ruta/imatge.png"
        anchors.fill: parent
        anchors.margins: 10
    }
    
    MouseArea {
        anchors.fill: parent
        onClicked: boto.clicat()
    }
}

Exemple 2: Emprar Settings per desar dades

import Qt.labs.settings 1.0

Settings {
    id: config
    property int comptador: 0
}

Button {
    text: "Increment (" + config.comptador + ")"
    onClicked: config.comptador += 1
}

Exemple 3: Canviar entre pàgines amb SwipeView

SwipeView {
    id: vista
    anchors.fill: parent
    
    Page {
        Label { text: "Pàgina 1" }
    }
    
    Page {
        Label { text: "Pàgina 2" }
    }
}

Button {
    text: "Anar a pàgina 2"
    onClicked: vista.currentIndex = 1
}

On trobar més exemples

Nota important sobre la solució completa

La solució completa d'aquest exercici (amb tots els apartats implementats) es publicarà després del termini de lliurament per tal de no comprometre l'aprenentatge autònom dels estudiants.

Si tens dubtes durant el desenvolupament:

  1. Consulta teoriaA08.md per conceptes específics
  2. Revisa la documentació oficial de Qt
  3. Pregunta al professor o als companys
  4. Experimenta amb el codi base proporcionat

Resum i següents passos

Has après a:

Recomanacions finals:

  1. Comença simple: Implementa primer la versió amb lletres abans d'afegir imatges
  2. Prova regularment: Compila i executa després de cada canvi important
  3. Documenta el codi: Afegeix comentaris per facilitar el manteniment
  4. Empra control de versions: Git et permetrà tornar enrere si alguna cosa falla
  5. Experimenta: Prova les millores opcionals per aprendre més
  6. Pregunta: Si et quedes encallat, consulta la documentació o pregunta

Recursos addicionals:

Entrega del projecte:

Assegura't que el teu projecte inclogui:

Bon desenvolupament i èxit amb el projecte! 🎮🚀