Abans de començar el projecte, cal configurar correctament QtCreator amb els kits necessaris per compilar per a les diferents plataformes objectiu.
Un kit a QtCreator és un conjunt de configuracions que especifica:
El kit de Desktop normalment es configura automàticament durant la instal·lació.
Verificació:
Edit → Preferences (o Tools → Options a Windows)KitsDesktop 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:
sudo apt install build-essentialPer compilar aplicacions per Android, necessites configurar l'SDK d'Android.
Pas 1: Instal·la Android Studio
Pas 2: Configura SDK Tools
A Android Studio:
Tools → SDK ManagerSDK Platforms, marca:SDK Tools, marca:Apply i espera la descàrregaPas 3: Configura QtCreator per Android
A QtCreator:
Edit → Preferences → Devices → Android~/.jdks/ o dins Android Studio~/Android/Sdk)~/Android/Sdk/ndk/25.x.x)ApplyPas 4: Crea el kit Android
Edit → Preferences → KitsAdd i configura:Android Qt 6.5.3 ClangAndroid DeviceAndroid Clang (ARM64)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
Edit → Preferences → KitsQt 6.5.3 for WebAssembly (hauria d'aparèixer automàticament)Qt for WebAssembly des del Qt InstallerPas 3: Verifica la configuració
# Comprova que Emscripten està disponible
emcc --version
Quan creïs un projecte nou a QtCreator:
File → New ProjectApplication (Qt) → Qt Quick ApplicationCMakeAixò crearà configuracions de compilació per a cada plataforma.
Desenvolupar un joc de pedra, paper, tisora amb Qt Quick que inclogui:
Software necessari:
Components de Qt:
Opcional (per altres plataformes):
Aquest projecte funcionarà a:
Per seguir aquesta guia, hauries de tenir coneixements bàsics de:
Si necessites reforçar conceptes, consulta teoriaA08.md abans de continuar.
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
File → New ProjectApplication (Qt) → Qt Quick ApplicationChoose...rps-gameCMakeDefault o tria MaterialFinishQtCreator crearà l'estructura bàsica del projecte.
Al terminal o des de QtCreator, crea el directori d'imatges:
cd rps-game
mkdir img
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:
CMAKE_AUTOMOC i CMAKE_AUTORCC: Processen automàticament fitxers MOC i QRCfind_package: Busca Qt 6qt_add_executable: Crea l'executableQT_ANDROID_PACKAGE_SOURCE_DIREl 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.
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:
~/.config/ExempleOrg/RPSGame.confHKEY_CURRENT_USER\Software\ExempleOrg\RPSGame~/Library/Preferences/cat.exemple.RPSGame.plistSi 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:
package: Identificador únic de l'aplicació (modifica'l)android:label: Nom visible de l'appandroid:icon: Icona de l'app (cal crear-la)minSdkVersion: Android 8.0 (API 26) mínimAfegeix una icona:
Col·loca una imatge PNG (512x512px recomanat) a:
android/res/drawable/icon.png
Selecciona el kit Desktop a QtCreator
Clica el botó Build (martell) o prem Ctrl+B
Si hi ha errors, revisa que:
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.
Per compilar per WebAssembly:
build-*-WebAssembly-*/cd build-*-WebAssembly-*/
python3 -m http.server 8000
http://localhost:8000/rps-game.htmlAquesta secció desenvolupa la versió inicial del joc emprant només text (R, P, S) sense imatges.
Crear una interfície funcional amb:
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.
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".
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"
}
}
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í)
}
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.
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!"
}
}
Ara hauries de tenir:
Compila i prova totes les combinacions!
Ara substituirem els botons de text per imatges gràfiques del joc.
Necessites quatre imatges:
rock.png - Pedrapaper.png - Paperscissors.png - Tisorasettings.png - Icona de configuració (per més endavant)Opcions:
Requisits:
Desa les imatges al directori img/ del projecte.
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>
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.
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)
}
}
}
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 ""
}
}
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: ""
Ara hauries de tenir:
Ara afegirem una segona pantalla amb estadístiques i configuració.
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) { /* ... */ }
}
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.
Page {
id: paginaJoc
GamePage {
id: gameContent
anchors.fill: parent
Connections {
function onPartidaGuanyada() {
config.guanyades += 1
}
function onPartidaPerduda() {
config.perdudes += 1
}
}
}
}
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
}
}
}
// 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
}
}
}
}
}
Page {
id: paginaConfig
ConfigPage {
id: configContent
anchors.fill: parent
guanyades: config.guanyades
perdudes: config.perdudes
Connections {
function onTornarAlJoc() {
vista.currentIndex = 0
}
}
}
}
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
}
}
}
Ara hauries de tenir:
Implementarem un sistema de temes que permeti canviar entre mode clar i fosc.
// 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
}
}
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>
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
}
}
]
}
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
// ...
}
}
}
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
}
⚠️ 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.
⚠️ 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.
Ara hauries de tenir:
Abans de donar el projecte per finalitzat, verifica que tot funcioni correctament.
Ctrl+B)Ctrl+R)build-*-WebAssembly-*/cd build-rps-game-WebAssembly-Release/
python3 -m http.server 8000
http://localhost:8000/rps-game.htmlNotes WebAssembly:
Solució de problemes Android:
adb logcatid a la TransitionProblema:
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í
// ...
}
}
}
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;
// ...
}
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ó:
import Qt.labs.settings 1.0import QtCore// 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
// ...
}
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).
Un cop completat el projecte bàsic, pots afegir funcionalitats extra per aprendre més i millorar l'aplicació.
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
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
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
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
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
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
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
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
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
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
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.
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)
#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();
}
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
}
}
}
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)
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>
// 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()
}
}
import Qt.labs.settings 1.0
Settings {
id: config
property int comptador: 0
}
Button {
text: "Increment (" + config.comptador + ")"
onClicked: config.comptador += 1
}
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
}
teoriaA08.md per més detallsLa 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:
teoriaA08.md per conceptes específicsAssegura't que el teu projecte inclogui:
Bon desenvolupament i èxit amb el projecte! 🎮🚀