Diferència entre revisions de la pàgina «Ús d'arxius vectorials SVG al QML (Qt6)»
| (Hi ha 58 revisions intermèdies del mateix usuari que no es mostren) | |||
| Línia 1: | Línia 1: | ||
[[Image:AmpMeter00.png|center|thumb|Exemple d'ús d'arxius SVG]] | [[Image:AmpMeter00.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| − | + | [https://github.com/jordibinefa/Qt/tree/master/QML/svg Codis s'aquesta pàgina al GitHub] | |
| − | + | [https://binefa.com/QtQml/svg/ampMeter00_04/ ampMeter00_04] Execució via web mitjançant WebAssembly | |
| − | === Estructura | + | [https://binefa.com/QtQml/svg/ampMeter00_09 ampMeter00_09] Execució via web mitjançant WebAssembly |
| + | == Explicació de <code>main.qml</code> d'<code>ampMeter00_00</code> == | ||
| + | |||
| + | El fitxer <code>main.qml</code> de <code>ampMeter00_00</code> estableix la base d'una aplicació Qt Quick que simula un amperímetre simple [1]. | ||
| + | |||
| + | === Estructura bàsica de la finestra === | ||
* <code>import QtQuick</code>: Importa el mòdul Qt Quick necessari per als elements de la interfície d'usuari [1]. | * <code>import QtQuick</code>: Importa el mòdul Qt Quick necessari per als elements de la interfície d'usuari [1]. | ||
* <code>Window</code>: Defineix la finestra principal de l'aplicació [1]. | * <code>Window</code>: Defineix la finestra principal de l'aplicació [1]. | ||
| Línia 12: | Línia 17: | ||
** <code>title: qsTr("ampMeter00")</code>: Defineix el títol de la finestra com "ampMeter00" [1]. | ** <code>title: qsTr("ampMeter00")</code>: Defineix el títol de la finestra com "ampMeter00" [1]. | ||
| − | === Element | + | === Element arrel (Rectangle) === |
* <code>Rectangle { id: root ... }</code>: Dins la finestra, hi ha un <code>Rectangle</code> anomenat <code>root</code> que serveix com a contenidor principal [1]. | * <code>Rectangle { id: root ... }</code>: Dins la finestra, hi ha un <code>Rectangle</code> anomenat <code>root</code> que serveix com a contenidor principal [1]. | ||
* <code>anchors.fill: parent</code>: Fa que aquest rectangle ompli completament la finestra pare [1]. | * <code>anchors.fill: parent</code>: Fa que aquest rectangle ompli completament la finestra pare [1]. | ||
* <code>color: "white"</code>: Estableix el color de fons del rectangle com a blanc [1]. | * <code>color: "white"</code>: Estableix el color de fons del rectangle com a blanc [1]. | ||
| − | === Imatge de | + | === Imatge de fons de l'amperímetre === |
* <code>Image { id: fonsSvg ... }</code>: S'afegeix un element <code>Image</code> per al fons de l'amperímetre [1]. | * <code>Image { id: fonsSvg ... }</code>: S'afegeix un element <code>Image</code> per al fons de l'amperímetre [1]. | ||
* <code>source: "qrc:/material/AmpMeterFons_c.svg"</code>: Carrega la imatge de fons des dels recursos de l'aplicació [1]. | * <code>source: "qrc:/material/AmpMeterFons_c.svg"</code>: Carrega la imatge de fons des dels recursos de l'aplicació [1]. | ||
| Línia 23: | Línia 28: | ||
* <code>anchors.centerIn: parent</code>: Centra la imatge de fons dins del seu element pare (<code>root</code> rectangle) [1]. | * <code>anchors.centerIn: parent</code>: Centra la imatge de fons dins del seu element pare (<code>root</code> rectangle) [1]. | ||
| − | === Imatge de l' | + | === Imatge de l'agulla de l'amperímetre === |
* <code>Image { id: agullaSvg ... }</code>: S'afegeix un altre element <code>Image</code> per representar l'agulla de l'amperímetre [2]. | * <code>Image { id: agullaSvg ... }</code>: S'afegeix un altre element <code>Image</code> per representar l'agulla de l'amperímetre [2]. | ||
* <code>source: "qrc:/material/AmpMeterAgulla_c.svg"</code>: Carrega la imatge de l'agulla [2]. | * <code>source: "qrc:/material/AmpMeterAgulla_c.svg"</code>: Carrega la imatge de l'agulla [2]. | ||
| Línia 30: | Línia 35: | ||
* <code>rotation: 45</code>: Estableix una rotació inicial de 45 graus per a l'agulla [2]. Aquest és el '''punt clau de control''' en aquesta primera versió, ja que la rotació es defineix de manera estàtica [2]. | * <code>rotation: 45</code>: Estableix una rotació inicial de 45 graus per a l'agulla [2]. Aquest és el '''punt clau de control''' en aquesta primera versió, ja que la rotació es defineix de manera estàtica [2]. | ||
| − | En resum, <code>ampMeter00_00 | + | En resum, <code>ampMeter00_00</code> presenta una interfície d'usuari bàsica amb un fons i una agulla d'amperímetre, on l'agulla es mostra en una posició fixa de 45 graus [1, 2]. |
'''main.qml''': | '''main.qml''': | ||
| Línia 66: | Línia 71: | ||
== Canvis Progressius en el Codi == | == Canvis Progressius en el Codi == | ||
A continuació, s'expliquen els canvis introduïts en cada iteració del projecte: | A continuació, s'expliquen els canvis introduïts en cada iteració del projecte: | ||
| − | === | + | === Interactivitat i animació (d'<code>ampMeter00_00</code> a <code>ampMeter00_01</code>) === |
| − | La versió <code>ampMeter00_01 | + | La versió <code>ampMeter00_01</code> introdueix la '''interactivitat''' i l''''animació''' per a l'agulla de l'amperímetre. |
* '''Rotació Inicial i Opacitat de l'Agulla:''' | * '''Rotació Inicial i Opacitat de l'Agulla:''' | ||
| Línia 83: | Línia 88: | ||
** Cada botó és un <code>Rectangle</code> gris amb un <code>width</code> i <code>height</code> de 60 píxels i un <code>radius</code> de 30 (creant cercles). | ** Cada botó és un <code>Rectangle</code> gris amb un <code>width</code> i <code>height</code> de 60 píxels i un <code>radius</code> de 30 (creant cercles). | ||
** Cada <code>Rectangle</code> conté un <code>Text</code> blanc amb la seva etiqueta (per exemple, "-60", "0", "60"). | ** Cada <code>Rectangle</code> conté un <code>Text</code> blanc amb la seva etiqueta (per exemple, "-60", "0", "60"). | ||
| − | ** Cada botó té una <code>MouseArea</code> que detecta clics. Quan es clica, la <code>rotation</code> de l'agulla (<code>agullaSvg.rotation</code>) es defineix a un valor específic (-45, -20, 0, 20, 45). ** El <code>cursorShape</code> de la <code>MouseArea</code> es defineix com <code>Qt.PointingHandCursor</code> per indicar que és un element clicable. | + | ** Cada botó té una <code>MouseArea</code> que detecta clics. Quan es clica, la <code>rotation</code> de l'agulla (<code>agullaSvg.rotation</code>) es defineix a un valor específic (-45, -20, 0, 20, 45). |
| + | ** El <code>cursorShape</code> de la <code>MouseArea</code> es defineix com <code>Qt.PointingHandCursor</code> per indicar que és un element clicable. | ||
Aquesta versió permet a l'usuari controlar la rotació de l'agulla mitjançant botons, amb una animació suau cada vegada que l'agulla canvia de posició. | Aquesta versió permet a l'usuari controlar la rotació de l'agulla mitjançant botons, amb una animació suau cada vegada que l'agulla canvia de posició. | ||
| + | |||
| + | '''main.qml''': | ||
<pre><nowiki> | <pre><nowiki> | ||
// main.qml | // main.qml | ||
| Línia 234: | Línia 242: | ||
</nowiki></pre> | </nowiki></pre> | ||
| − | === | + | === Animació automàtica i contínua (d'<code>ampMeter00_01</code> a <code>ampMeter00_02</code>) === |
| − | La versió <code>ampMeter00_02 | + | La versió <code>ampMeter00_02</code> afegeix una '''animació automàtica i contínua''' per a l'agulla. |
* '''Canvi de Funció d'Acceleració:''' | * '''Canvi de Funció d'Acceleració:''' | ||
** La funció d'acceleració (<code>easing.type</code>) del <code>NumberAnimation</code> de l'agulla es canvia de <code>Easing.InOutBack</code> a <code>Easing.InOutQuad</code>. | ** La funció d'acceleració (<code>easing.type</code>) del <code>NumberAnimation</code> de l'agulla es canvia de <code>Easing.InOutBack</code> a <code>Easing.InOutQuad</code>. | ||
| − | * '''Animació Seqüencial:''' ** S'afegeix una <code>SequentialAnimation on rotation</code> a l'agulla (<code>agullaSvg</code>). | + | * '''Animació Seqüencial:''' |
| + | ** S'afegeix una <code>SequentialAnimation on rotation</code> a l'agulla (<code>agullaSvg</code>). | ||
** <code>loops: Animation.Infinite</code>: Aquesta animació es repetirà indefinidament. | ** <code>loops: Animation.Infinite</code>: Aquesta animació es repetirà indefinidament. | ||
** Consisteix en dues <code>RotationAnimation</code>s: | ** Consisteix en dues <code>RotationAnimation</code>s: | ||
| Línia 245: | Línia 254: | ||
** Aquesta animació fa que l'agulla es mogui contínuament entre <code>45</code> i <code>-45</code> graus quan no es controla manualment. | ** Aquesta animació fa que l'agulla es mogui contínuament entre <code>45</code> i <code>-45</code> graus quan no es controla manualment. | ||
* Els botons de control manual romanen igual, permetent a l'usuari sobrescriure l'animació automàtica. | * Els botons de control manual romanen igual, permetent a l'usuari sobrescriure l'animació automàtica. | ||
| + | |||
| + | '''main.qml''': | ||
<pre><nowiki> | <pre><nowiki> | ||
// main.qml | // main.qml | ||
| Línia 406: | Línia 417: | ||
} | } | ||
</nowiki></pre> | </nowiki></pre> | ||
| − | === | + | |
| − | La versió <code>ampMeter00_03 | + | === Model de dades ''ListModel'' i repetició ''Repeater'' (d'<code>ampMeter00_02</code> a <code>ampMeter00_03</code>) === |
| + | La versió <code>ampMeter00_03</code> refactoritza el codi dels botons utilitzant un '''model de dades (<code>ListModel</code>) i un <code>Repeater</code>''', millorant la modularitat i la facilitat de manteniment. | ||
* '''Eliminació de l'Animació Seqüencial:''' | * '''Eliminació de l'Animació Seqüencial:''' | ||
** La <code>SequentialAnimation on rotation</code> s'elimina. | ** La <code>SequentialAnimation on rotation</code> s'elimina. | ||
| Línia 501: | Línia 513: | ||
</nowiki></pre> | </nowiki></pre> | ||
| − | === | + | === Component '''Botonera'''. QML reutilitzable (d'<code>ampMeter00_03</code> a <code>ampMeter00_04</code>) === |
| − | La versió <code>ampMeter00_04 | + | La versió <code>ampMeter00_04</code> millora la modularitat extraient la lògica dels botons en un '''component QML reutilitzable''' anomenat <code>Botonera.qml</code>. |
* '''Creació del Component <code>Botonera.qml</code>:''' | * '''Creació del Component <code>Botonera.qml</code>:''' | ||
** Es crea un nou fitxer <code>Botonera.qml</code> que conté la implementació del <code>Row</code> i el <code>Repeater</code> per als botons. | ** Es crea un nou fitxer <code>Botonera.qml</code> que conté la implementació del <code>Row</code> i el <code>Repeater</code> per als botons. | ||
| Línia 613: | Línia 625: | ||
</nowiki></pre> | </nowiki></pre> | ||
| − | === | + | === Component '''AmpMeter''' (d' <code>ampMeter00_04</code> a <code>ampMeter00_05</code>) === |
| − | La versió <code>ampMeter00_05 | + | La versió <code>ampMeter00_05</code> continua amb la refactorització, encapsulant la lògica de l'amperímetre (fons i agulla) en un '''component QML propi''' anomenat <code>AmpMeter.qml</code>. |
* '''Creació del Component <code>AmpMeter.qml</code>:''' | * '''Creació del Component <code>AmpMeter.qml</code>:''' | ||
** Es crea un nou fitxer <code>AmpMeter.qml</code> que conté el fons (<code>fonsSvg</code>) i l'agulla (<code>agullaSvg</code>) de l'amperímetre. | ** Es crea un nou fitxer <code>AmpMeter.qml</code> que conté el fons (<code>fonsSvg</code>) i l'agulla (<code>agullaSvg</code>) de l'amperímetre. | ||
| Línia 741: | Línia 753: | ||
duration: 300 | duration: 300 | ||
easing.type: Easing.InOutBack | easing.type: Easing.InOutBack | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </nowiki></pre> | ||
| + | |||
| + | = Modularitat i separació de responsabilitats = | ||
| + | L'evolució dels codis, prenent com a referència inicial '''ampMeter00 _05''', mostra una progressió cap a una major modularitat i control de la interfície d'usuari a través de la introducció de nous components QML i la separació de responsabilitats. | ||
| + | |||
| + | === Punt de referència inicial per a la modularitat i separació de responsabilitats - ampMeter00 _05 === | ||
| + | En aquesta versió, el fitxer <code>main.qml </code> configura una finestra amb un <code>Rectangle </code> blanc. Inclou dos components QML personalitzats: <code>AmpMeter </code> i <code>Botonera </code>. | ||
| + | |||
| + | * El component ''' <code>AmpMeter </code>''' es centra a la pantalla i gestiona la visualització del fons i l'agulla d'un amperímetre. La rotació de l'agulla es controla mitjançant una propietat <code>rotation </code> exposada i animada amb una <code>NumberAnimation </code> que utilitza <code>Easing.InOutBack </code>. | ||
| + | * El component ''' <code>Botonera </code>''' es col·loca a la part inferior i es centra horitzontalment. S'alimenta d'un <code>ListModel </code> anomenat <code>buttonModel </code> que conté les etiquetes i els valors de rotació per als botons. Quan un botó és clicat, emet un senyal <code>buttonClicked </code> amb el seu valor, i aquest valor s'assigna a la propietat <code>rotation </code> del component <code>ampMeter </code>. | ||
| + | |||
| + | === Introducció de Múltiples AmpMeters - ampMeter00 _06 === | ||
| + | Aquesta versió es titula "ampMeter00 _06 - Triple" i incrementa l'amplada de la finestra a 960 píxels. La millora principal és la capacitat de mostrar i controlar '''tres components <code>AmpMeter </code> simultàniament'''. | ||
| + | |||
| + | [[Image:AmpMeter06.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| + | |||
| + | * Els tres <code>AmpMeter </code> ( <code>ampMeter1 </code>, <code>ampMeter2 </code>, <code>ampMeter3 </code>) estan organitzats en una <code>Row </code> (fila) centrada, amb un desplaçament vertical per deixar espai als controls. Cadascun té dimensions i escala definides de manera individual (per exemple, <code>ampMeter1 </code> i <code>ampMeter3 </code> amb <code>scale: 0.5 </code>, i <code>ampMeter2 </code> amb <code>scale: 0.8 </code>). | ||
| + | * El <code>ListModel </code> dels botons es manté, però el gestor <code>onButtonClicked </code> a <code>main.qml </code> ara actualitza la propietat <code>rotation </code> dels '''tres''' components <code>AmpMeter </code> de forma idèntica ( <code>ampMeter1.rotation = value </code>, <code>ampMeter2.rotation = value </code>, <code>ampMeter3.rotation = value </code>). | ||
| + | * Els components <code>Botonera.qml </code> i <code>AmpMeter.qml </code> conserven la seva estructura anterior. | ||
| + | |||
| + | === Incorporació d'un lliscador o ''slider'' - ampMeter00 _07 === | ||
| + | Continuant amb la configuració "Triple", aquesta versió afegeix un nou element de control: un slider (lliscador). | ||
| + | |||
| + | [[Image:AmpMeter07.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| + | |||
| + | |||
| + | * Es mantenen els tres components <code>AmpMeter </code> en fila. | ||
| + | * S'introdueix un <code>Column </code> ( <code>sliderContainer </code>) que conté la lògica del slider, situat just a sobre dels botons de control. | ||
| + | * Aquest slider consisteix en un <code>Rectangle </code> que serveix de pista i un altre <code>Rectangle </code> ( <code>sliderHandle </code>) que fa de mànec lliscant. | ||
| + | * Un <code>MouseArea </code> permet arrossegar el <code>sliderHandle </code> només en l'eix X ( <code>drag.axis: Drag.XAxis </code>). | ||
| + | * El gestor <code>onPositionChanged </code> calcula un valor ( <code>value </code>) entre -60 i +60, el converteix en un angle de rotació entre -45 i +45, i l'aplica '''exclusivament a <code>ampMeter2 </code>''' ( <code>ampMeter2.rotation = rotation </code>). | ||
| + | * Es mostren etiquetes per als valors extrems (-60 i +60) i el valor actual del slider. | ||
| + | * La <code>Botonera </code> ara només controla <code>ampMeter1 </code> i <code>ampMeter3 </code>, ja que <code>ampMeter2 </code> és controlat pel nou slider. | ||
| + | |||
| + | === Modularització del lliscador amb <code>BarraLliscant.qml </code> - ampMeter00 _08 === | ||
| + | En aquesta etapa, la lògica del lliscador es refactoritza en un component QML independent anomenat ''' <code>BarraLliscant.qml </code>'''. | ||
| + | |||
| + | [[Image:AmpMeter08.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| + | |||
| + | * A <code>main.qml </code>, el <code>sliderContainer </code> es reemplaça per una instància del component <code>BarraLliscant </code>. | ||
| + | * <code>BarraLliscant.qml </code> encapsula tota la interfície i la lògica del lliscador, incloent les etiquetes, la pista, el mànec, el càlcul del valor i la visualització del valor actual. | ||
| + | * Defineix propietats configurables com <code>minValue </code>, <code>maxValue </code>, <code>sliderWidth </code> i <code>sliderHeight </code>. | ||
| + | * Emet un senyal <code>valueChanged(real value) </code> quan el valor del lliscador canvia. | ||
| + | * A <code>main.qml </code>, el senyal <code>onValueChanged </code> de la <code>BarraLliscant </code> s'utilitza per convertir el valor del lliscador en rotació i aplicar-lo a <code>ampMeter2 </code>. | ||
| + | * El component <code>AmpMeter.qml </code> introdueix propietats <code>fondoSource </code> i <code>agullaSource </code> per personalitzar les imatges, tot i que inicialment mantenen els mateixos valors per defecte. | ||
| + | * La <code>Botonera </code> continua controlant <code>ampMeter1 </code> i <code>ampMeter3 </code>. | ||
| + | |||
| + | === Modularització completa amb <code>Vista.qml </code> i <code>Controlador.qml </code> - ampMeter00 _09 === | ||
| + | Aquesta és la versió més modularitzada, amb el títol "ampMeter00 _09 - Modular". <code>main.qml </code> se simplifica enormement delegant les responsabilitats a dos nous components de nivell superior: ''' <code>Vista </code>''' i ''' <code>Controlador </code>'''. | ||
| + | |||
| + | [[Image:AmpMeter09.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| + | |||
| + | * ''' <code>Vista.qml </code>''': Aquest component agrupa els tres components <code>AmpMeter </code> en una <code>Row </code>. | ||
| + | ** Exposa les propietats <code>ampMeter1Rotation </code>, <code>ampMeter2Rotation </code>, <code>ampMeter3Rotation </code> com àlies per controlar la rotació de cada <code>AmpMeter </code> des del seu pare. | ||
| + | ** Permet personalitzar l'escala de cada <code>AmpMeter </code> ( <code>ampMeter1Scale </code>, <code>ampMeter2Scale </code>, <code>ampMeter3Scale </code>). | ||
| + | ** Una novetat important és que cada <code>AmpMeter </code> dins de la <code>Vista </code> pot tenir un tipus d'animació d'''easing''' diferent ( <code>Easing.InOutQuad </code>, <code>Easing.InOutBack </code>, <code>Easing.InOutCubic </code>), cosa que requereix una nova propietat <code>easingType </code> a <code>AmpMeter.qml </code>. | ||
| + | * ''' <code>Controlador.qml </code>''': Aquest component agrupa la <code>BarraLliscant </code> i la <code>Botonera </code> en una <code>Column </code>. | ||
| + | ** Rep el <code>buttonModel </code> per a la <code>Botonera </code>. | ||
| + | ** Emet senyals <code>sliderValueChanged(real value) </code> i <code>buttonClicked(real value) </code> per comunicar els esdeveniments dels seus components interns al seu pare. | ||
| + | ** Connecta el senyal <code>onValueChanged </code> de la seva <code>BarraLliscant </code> interna al seu propi <code>sliderValueChanged </code>. | ||
| + | ** Connecta el senyal <code>onButtonClicked </code> de la seva <code>Botonera </code> interna al seu propi <code>buttonClicked </code>. | ||
| + | * A <code>main.qml </code>, el gestor <code>onSliderValueChanged </code> del <code>Controlador </code> ara actualitza <code>vista.ampMeter2Rotation </code>. | ||
| + | * El gestor <code>onButtonClicked </code> del <code>Controlador </code> actualitza <code>vista.ampMeter1Rotation </code> i <code>vista.ampMeter3Rotation </code>. | ||
| + | |||
| + | En resum, l'evolució del codi il·lustra una transició des d'una aplicació monolítica amb la lògica de control i vista al fitxer <code>main.qml </code>, cap a una estructura modular on components reutilitzables ( <code>AmpMeter </code>, <code>Botonera </code>, <code>BarraLliscant </code>) s'organitzen en components de més alt nivell ( <code>Vista </code>, <code>Controlador </code>), millorant la mantenibilitat i la claredat del codi. | ||
| + | = Implementació en Python = | ||
| + | [[Image:AmpMeter10.png|center|thumb|Exemple d'ús d'arxius SVG]] | ||
| + | == Instal·lació de '''pyside6''' == | ||
| + | * [https://pypi.org/project/PySide6/ Projecte PySide6] | ||
| + | * A Linux (Debian 13): | ||
| + | sudo apt install python3-pip python3-pipdeptree | ||
| + | sudo apt install python3-qtpy-pyside6 | ||
| + | * A un entorn virtual a Linux o a Windows: | ||
| + | pip install PySide6 | ||
| + | |||
| + | == Compilació de l'arxiu de recursos .qrc emprant '''pyside6-rcc''' == | ||
| + | === Modificació de l'arxiu de recursos === | ||
| + | Per a fer compatibles els arxius .qml previs, s'han afegit dues línies a l'arxiu .qrc (en negreta): | ||
| + | <RCC> | ||
| + | <qresource prefix="/"> | ||
| + | <file alias="agulla.svg">material/AmpMeterAgulla_c.svg</file> | ||
| + | <file alias="fons.svg">material/AmpMeterFons_c.svg</file> | ||
| + | '''<file>material/AmpMeterAgulla_c.svg</file>''' | ||
| + | '''<file>material/AmpMeterFons_c.svg</file>''' | ||
| + | </qresource> | ||
| + | </RCC> | ||
| + | === Compilació === | ||
| + | * Cal compilar l'arxiu de recursos qrc. En aquest exemple ''recursos.qrc'' es compilat amb pyside6-rcc (nom del compilat: ''rc_recursos.py''): | ||
| + | '''pyside6-rcc''' recursos.qrc '''-o''' rc_recursos.py | ||
| + | === Execució === | ||
| + | python main.py | ||
| + | |||
| + | == main.py == | ||
| + | <pre><nowiki> | ||
| + | #!/usr/bin/env python3 | ||
| + | import sys | ||
| + | import os | ||
| + | from PySide6.QtGui import QGuiApplication | ||
| + | from PySide6.QtQml import QQmlApplicationEngine | ||
| + | from PySide6.QtCore import QUrl | ||
| + | |||
| + | # Importar els recursos compilats | ||
| + | # pyside6-rcc recursos.qrc -o rc_recursos.py | ||
| + | import rc_recursos | ||
| + | |||
| + | def main(): | ||
| + | app = QGuiApplication(sys.argv) | ||
| + | |||
| + | engine = QQmlApplicationEngine() | ||
| + | |||
| + | # Afegir el directori actual al path de cerca de QML | ||
| + | current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| + | engine.addImportPath(current_dir) | ||
| + | |||
| + | # Carregar el fitxer QML principal des del sistema de fitxers | ||
| + | qml_file = os.path.join(current_dir, "main.qml") | ||
| + | engine.load(QUrl.fromLocalFile(qml_file)) | ||
| + | |||
| + | # Verificar que el QML s'ha carregat correctament | ||
| + | if not engine.rootObjects(): | ||
| + | print("Error: No s'ha pogut carregar main.qml") | ||
| + | return -1 | ||
| + | |||
| + | return app.exec() | ||
| + | |||
| + | if __name__ == "__main__": | ||
| + | sys.exit(main()) | ||
| + | |||
| + | </nowiki></pre> | ||
| + | == main.qml == | ||
| + | L'únic canvi a '''main.qml''' és el títol: | ||
| + | <pre><nowiki> | ||
| + | // main.qml | ||
| + | import QtQuick | ||
| + | |||
| + | Window { | ||
| + | width: 960 | ||
| + | height: 480 | ||
| + | visible: true | ||
| + | title: qsTr("ampMeter00_10 - Python") | ||
| + | |||
| + | Rectangle { | ||
| + | id: root | ||
| + | anchors.fill: parent | ||
| + | color: "white" | ||
| + | |||
| + | // Model de dades per als botons | ||
| + | ListModel { | ||
| + | id: buttonModel | ||
| + | ListElement { label: "-60"; value: -45 } | ||
| + | ListElement { label: "-30"; value: -20 } | ||
| + | ListElement { label: "0"; value: 0 } | ||
| + | ListElement { label: "30"; value: 20 } | ||
| + | ListElement { label: "60"; value: 45 } | ||
| + | } | ||
| + | |||
| + | // Component Vista amb els tres AmpMeters | ||
| + | Vista { | ||
| + | id: vista | ||
| + | anchors.centerIn: parent | ||
| + | anchors.verticalCenterOffset: -40 | ||
| + | } | ||
| + | |||
| + | // Component Controlador amb BarraLliscant i Botonera | ||
| + | Controlador { | ||
| + | id: controlador | ||
| + | anchors.bottom: parent.bottom | ||
| + | anchors.horizontalCenter: parent.horizontalCenter | ||
| + | anchors.bottomMargin: 20 | ||
| + | |||
| + | model: buttonModel | ||
| + | |||
| + | onSliderValueChanged: function(value) { | ||
| + | // Converteix el valor (-60 a +60) a rotació (-45 a +45) | ||
| + | var rotation = (value / 60) * 45 | ||
| + | vista.ampMeter2Rotation = rotation | ||
| + | } | ||
| + | |||
| + | onButtonClicked: function(value) { | ||
| + | // Controla ampMeter1 i ampMeter3 | ||
| + | vista.ampMeter1Rotation = value | ||
| + | vista.ampMeter3Rotation = value | ||
} | } | ||
} | } | ||
Revisió de 20:12, 14 set 2025
Codis s'aquesta pàgina al GitHub
ampMeter00_04 Execució via web mitjançant WebAssembly
ampMeter00_09 Execució via web mitjançant WebAssembly
Contingut
- 1 Explicació de main.qml d'ampMeter00_00
- 2 Canvis Progressius en el Codi
- 2.1 Interactivitat i animació (d'ampMeter00_00 a ampMeter00_01)
- 2.2 Animació automàtica i contínua (d'ampMeter00_01 a ampMeter00_02)
- 2.3 Model de dades ListModel i repetició Repeater (d'ampMeter00_02 a ampMeter00_03)
- 2.4 Component Botonera. QML reutilitzable (d'ampMeter00_03 a ampMeter00_04)
- 2.5 Component AmpMeter (d' ampMeter00_04 a ampMeter00_05)
- 3 Modularitat i separació de responsabilitats
- 3.1 Punt de referència inicial per a la modularitat i separació de responsabilitats - ampMeter00 _05
- 3.2 Introducció de Múltiples AmpMeters - ampMeter00 _06
- 3.3 Incorporació d'un lliscador o slider - ampMeter00 _07
- 3.4 Modularització del lliscador amb BarraLliscant.qml - ampMeter00 _08
- 3.5 Modularització completa amb Vista.qml i Controlador.qml - ampMeter00 _09
- 4 Implementació en Python
Explicació de main.qml d'ampMeter00_00
El fitxer main.qml de ampMeter00_00 estableix la base d'una aplicació Qt Quick que simula un amperímetre simple [1].
Estructura bàsica de la finestra
import QtQuick: Importa el mòdul Qt Quick necessari per als elements de la interfície d'usuari [1].Window: Defineix la finestra principal de l'aplicació [1].width: 640,height: 480: Estableix les dimensions de la finestra [1].visible: true: Fa que la finestra sigui visible quan s'inicia l'aplicació [1].title: qsTr("ampMeter00"): Defineix el títol de la finestra com "ampMeter00" [1].
Element arrel (Rectangle)
Rectangle { id: root ... }: Dins la finestra, hi ha unRectangleanomenatrootque serveix com a contenidor principal [1].anchors.fill: parent: Fa que aquest rectangle ompli completament la finestra pare [1].color: "white": Estableix el color de fons del rectangle com a blanc [1].
Imatge de fons de l'amperímetre
Image { id: fonsSvg ... }: S'afegeix un elementImageper al fons de l'amperímetre [1].source: "qrc:/material/AmpMeterFons_c.svg": Carrega la imatge de fons des dels recursos de l'aplicació [1].fillMode: Image.PreserveAspectFit: Aquesta propietat assegura que la imatge mantingui les seves proporcions originals mentre s'ajusta dins de l'espai disponible [1].anchors.centerIn: parent: Centra la imatge de fons dins del seu element pare (rootrectangle) [1].
Imatge de l'agulla de l'amperímetre
Image { id: agullaSvg ... }: S'afegeix un altre elementImageper representar l'agulla de l'amperímetre [2].source: "qrc:/material/AmpMeterAgulla_c.svg": Carrega la imatge de l'agulla [2].fillMode: Image.PreserveAspectFit: Igual que amb el fons, manté les proporcions de l'agulla [2].anchors.centerIn: parent: Centra l'agulla dins del seu pare (rootrectangle) [2].rotation: 45: Estableix una rotació inicial de 45 graus per a l'agulla [2]. Aquest és el punt clau de control en aquesta primera versió, ja que la rotació es defineix de manera estàtica [2].
En resum, ampMeter00_00 presenta una interfície d'usuari bàsica amb un fons i una agulla d'amperímetre, on l'agulla es mostra en una posició fixa de 45 graus [1, 2].
main.qml:
// main.qml
import QtQuick // [1]
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_00")
Rectangle {
id: root
anchors.fill: parent
color: "white"
Image {
id: fonsSvg
source: "qrc:/material/AmpMeterFons_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image { // [2]
id: agullaSvg
source: "qrc:/material/AmpMeterAgulla_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
rotation: 45
}
}
}
Canvis Progressius en el Codi
A continuació, s'expliquen els canvis introduïts en cada iteració del projecte:
Interactivitat i animació (d'ampMeter00_00 a ampMeter00_01)
La versió ampMeter00_01 introdueix la interactivitat i l'animació per a l'agulla de l'amperímetre.
- Rotació Inicial i Opacitat de l'Agulla:
- La
rotationinicial de l'agulla (agullaSvg) es canvia a0. - S'afegeix una propietat
opacity: 0.8a l'agulla.
- La
- Animació de Rotació (Behavior):
- S'afegeix un
Behavior on rotationa l'agulla. Això significa que cada vegada que la propietatrotationde l'agulla canvia, s'executarà una animació. NumberAnimation { duration: 300; easing.type: Easing.InOutBack }: L'animació durarà 300 mil·lisegons i utilitzarà una funció d'acceleracióEasing.InOutBackper a un efecte més dinàmic.
- S'afegeix un
- Botons de Control:
- S'afegeix una fila de botons (
Row) a la part inferior de la pantalla, anomenadacontrolButtons. - Aquests botons es centren horitzontalment (
anchors.horizontalCenter: parent.horizontalCenter) i es col·loquen a 20 píxels de la part inferior (anchors.bottomMargin: 20). - Hi ha un
spacing: 20entre els botons. - Cada botó és un
Rectanglegris amb unwidthiheightde 60 píxels i unradiusde 30 (creant cercles). - Cada
Rectangleconté unTextblanc amb la seva etiqueta (per exemple, "-60", "0", "60"). - Cada botó té una
MouseAreaque detecta clics. Quan es clica, larotationde l'agulla (agullaSvg.rotation) es defineix a un valor específic (-45, -20, 0, 20, 45). - El
cursorShapede laMouseAreaes defineix comQt.PointingHandCursorper indicar que és un element clicable.
- S'afegeix una fila de botons (
Aquesta versió permet a l'usuari controlar la rotació de l'agulla mitjançant botons, amb una animació suau cada vegada que l'agulla canvia de posició.
main.qml:
// main.qml
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_01")
Rectangle {
id: root
anchors.fill: parent
color: "white"
Image {
id: fonsSvg
source: "qrc:/material/AmpMeterFons_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image {
id: agullaSvg
source: "qrc:/material/AmpMeterAgulla_c.svg"
fillMode: Image.PreserveAspectFit
opacity: 0.8
anchors.centerIn: parent
rotation: 0
Behavior on rotation {
NumberAnimation {
duration: 300
easing.type: Easing.InOutBack
}
}
}
Row {
id: controlButtons
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
spacing: 20
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "-60"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = -45
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "-30"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = -20
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "0"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 0
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "30"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 20
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "60"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 45
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
Animació automàtica i contínua (d'ampMeter00_01 a ampMeter00_02)
La versió ampMeter00_02 afegeix una animació automàtica i contínua per a l'agulla.
- Canvi de Funció d'Acceleració:
- La funció d'acceleració (
easing.type) delNumberAnimationde l'agulla es canvia deEasing.InOutBackaEasing.InOutQuad.
- La funció d'acceleració (
- Animació Seqüencial:
- S'afegeix una
SequentialAnimation on rotationa l'agulla (agullaSvg). loops: Animation.Infinite: Aquesta animació es repetirà indefinidament.- Consisteix en dues
RotationAnimations:- Una que fa girar l'agulla fins a
45graus durant1000mil·lisegons. - Una segona que la fa girar fins a
-45graus durant500mil·lisegons.
- Una que fa girar l'agulla fins a
- Aquesta animació fa que l'agulla es mogui contínuament entre
45i-45graus quan no es controla manualment.
- S'afegeix una
- Els botons de control manual romanen igual, permetent a l'usuari sobrescriure l'animació automàtica.
main.qml:
// main.qml
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_02")
Rectangle {
id: root
anchors.fill: parent
color: "white"
Image {
id: fonsSvg
source: "qrc:/material/AmpMeterFons_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image {
id: agullaSvg
source: "qrc:/material/AmpMeterAgulla_c.svg"
fillMode: Image.PreserveAspectFit
opacity: 0.8
anchors.centerIn: parent
rotation: 0
Behavior on rotation {
NumberAnimation {
duration: 300
easing.type: Easing.InOutQuad
}
}
SequentialAnimation on rotation {
loops: Animation.Infinite
RotationAnimation {
to: 45
duration: 1000
}
RotationAnimation {
to: -45
duration: 500
}
}
}
Row {
id: controlButtons
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
spacing: 20
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "-60"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = -45
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "-30"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = -20
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "0"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 0
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "30"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 20
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: "60"
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = 45
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
Model de dades ListModel i repetició Repeater (d'ampMeter00_02 a ampMeter00_03)
La versió ampMeter00_03 refactoritza el codi dels botons utilitzant un model de dades (ListModel) i un Repeater, millorant la modularitat i la facilitat de manteniment.
- Eliminació de l'Animació Seqüencial:
- La
SequentialAnimation on rotations'elimina.
- La
- Model de Dades per a Botons (
ListModel):- Es defineix un
ListModelambid: buttonModel. - Conté
ListElements, cadascun amb unalabel(el text del botó) i unvalue(el valor de rotació corresponent). Per exemple,{ label: "-60"; value: -45 }.
- Es defineix un
- Generació de Botons amb
Repeater:- En lloc de declarar cada botó individualment, s'utilitza un
Repeaterambmodel: buttonModeldins delRowdecontrolButtons. - El
Repeatercrea unRectangleper a cada element del model. - El
Textde cada botó utilitzamodel.labeli l'esdevenimentonClickedde laMouseAreautilitzaagullaSvg.rotation = model.value.
- En lloc de declarar cada botó individualment, s'utilitza un
Aquest canvi redueix la duplicitat de codi i facilita l'addició o modificació de botons en el futur.
main.qml:
// main.qml
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_03")
Rectangle {
id: root
anchors.fill: parent
color: "white"
Image {
id: fonsSvg
source: "qrc:/material/AmpMeterFons_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image {
id: agullaSvg
source: "qrc:/material/AmpMeterAgulla_c.svg"
fillMode: Image.PreserveAspectFit
opacity: 0.8
anchors.centerIn: parent
rotation: 0
Behavior on rotation {
NumberAnimation {
duration: 300
easing.type: Easing.InOutBack
}
}
}
ListModel {
id: buttonModel
ListElement { label: "-60"; value: -45 }
ListElement { label: "-30"; value: -20 }
ListElement { label: "0"; value: 0 }
ListElement { label: "30"; value: 20 }
ListElement { label: "60"; value: 45 }
}
Row {
id: controlButtons
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
spacing: 20
Repeater {
model: buttonModel
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: model.label
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: agullaSvg.rotation = model.value
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
}
Component Botonera. QML reutilitzable (d'ampMeter00_03 a ampMeter00_04)
La versió ampMeter00_04 millora la modularitat extraient la lògica dels botons en un component QML reutilitzable anomenat Botonera.qml.
- Creació del Component
Botonera.qml:- Es crea un nou fitxer
Botonera.qmlque conté la implementació delRowi elRepeaterper als botons. - Aquest component té una propietat
property alias model: repeater.modelque permet al component pare passar-li elListModel. - Defineix un
signal buttonClicked(real value)que s'emet quan es prem un botó, enviant elvaluede rotació. - Dins del
MouseAreade cada botó,onClickedara cridabotonera.buttonClicked(model.value).
- Es crea un nou fitxer
- Ús del Component
Botoneraamain.qml:- A
main.qml, elRoworiginal de botons es reemplaça per una instància del nou componentBotonera. controlButtons(la instància deBotonera) rep elbuttonModelmitjançantmodel: buttonModel.- Captura el senyal
onButtonClickeddel componentBotonerai actualitza la rotació de l'agulla ambagullaSvg.rotation = value.
- A
Aquesta refactorització permet reutilitzar el bloc de botons en altres parts de l'aplicació o en altres projectes, fent el codi de main.qml més net i centrat en la composició.
main.qml:
// main.qml
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_04")
Rectangle {
id: root
anchors.fill: parent
color: "white"
Image {
id: fonsSvg
source: "qrc:/material/AmpMeterFons_c.svg"
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image {
id: agullaSvg
source: "qrc:/material/AmpMeterAgulla_c.svg"
fillMode: Image.PreserveAspectFit
opacity: 0.8
anchors.centerIn: parent
rotation: 0
Behavior on rotation {
NumberAnimation {
duration: 300
easing.type: Easing.InOutBack
}
}
}
ListModel {
id: buttonModel
ListElement { label: "-60"; value: -45 }
ListElement { label: "-30"; value: -20 }
ListElement { label: "0"; value: 0 }
ListElement { label: "30"; value: 20 }
ListElement { label: "60"; value: 45 }
}
Botonera {
id: controlButtons
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
model: buttonModel
onButtonClicked: function(value) {
agullaSvg.rotation = value
}
}
}
}
Botonera.qml:
// Botonera.qml
import QtQuick
Row {
id: botonera
spacing: 20
property alias model: repeater.model
signal buttonClicked(real value)
Repeater {
id: repeater
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: model.label
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: botonera.buttonClicked(model.value)
cursorShape: Qt.PointingHandCursor
}
}
}
}
Component AmpMeter (d' ampMeter00_04 a ampMeter00_05)
La versió ampMeter00_05 continua amb la refactorització, encapsulant la lògica de l'amperímetre (fons i agulla) en un component QML propi anomenat AmpMeter.qml.
- Creació del Component
AmpMeter.qml:- Es crea un nou fitxer
AmpMeter.qmlque conté el fons (fonsSvg) i l'agulla (agullaSvg) de l'amperímetre. - Exposa una propietat
property alias rotation: agullaSvg.rotationper permetre al component pare controlar directament la rotació de l'agulla interna. - També defineix propietats per personalitzar les imatges de fons i agulla (
fonsSource,agullaSource) i l'opacitat de l'agulla (agullaOpacity). - El
Behavior on rotationper a l'agulla es troba ara dins d'aquest component.
- Es crea un nou fitxer
- Ús del Component
AmpMeteramain.qml:- A
main.qml, les dues imatges originals (fonsSvgiagullaSvg) es reemplacen per una instància del nou componentAmpMeter. - L'esdeveniment
onButtonClickeddelBotoneraara actualitza la rotació de l'amperímetre ambampMeter.rotation = value, utilitzant la propietat exposada pel componentAmpMeter.
- A
Aquesta última refactorització crea un component autònom (AmpMeter) que conté tota la lògica i la presentació de l'amperímetre, fent que main.qml sigui extremadament simple i es basi en la composició de components.
main.qml:
// main.qml
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("ampMeter00_05")
Rectangle {
id: root
anchors.fill: parent
color: "white"
AmpMeter {
id: ampMeter
anchors.centerIn: parent
width: parent.width
height: parent.height
}
ListModel {
id: buttonModel
ListElement { label: "-60"; value: -45 }
ListElement { label: "-30"; value: -20 }
ListElement { label: "0"; value: 0 }
ListElement { label: "30"; value: 20 }
ListElement { label: "60"; value: 45 }
}
Botonera {
id: controlButtons
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
model: buttonModel
onButtonClicked: function(value) {
ampMeter.rotation = value
}
}
}
}
Botonera.qml:
// Botonera.qml
import QtQuick
Row {
id: botonera
spacing: 20
property alias model: repeater.model
signal buttonClicked(real value)
Repeater {
id: repeater
Rectangle {
width: 60
height: 60
radius: 30
color: "gray"
Text {
anchors.centerIn: parent
text: model.label
color: "white"
font.pixelSize: 14
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: botonera.buttonClicked(model.value)
cursorShape: Qt.PointingHandCursor
}
}
}
}
AmpMeter.qml:
// AmpMeter.qml
import QtQuick
Item {
id: ampMeter
property alias rotation: agullaSvg.rotation
property string fonsSource: "qrc:/material/AmpMeterFons_c.svg"
property string agullaSource: "qrc:/material/AmpMeterAgulla_c.svg"
property real agullaOpacity: 0.8
Image {
id: fonsSvg
source: ampMeter.fonsSource
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Image {
id: agullaSvg
source: ampMeter.agullaSource
fillMode: Image.PreserveAspectFit
opacity: ampMeter.agullaOpacity
anchors.centerIn: parent
rotation: 0
Behavior on rotation {
NumberAnimation {
duration: 300
easing.type: Easing.InOutBack
}
}
}
}
Modularitat i separació de responsabilitats
L'evolució dels codis, prenent com a referència inicial ampMeter00 _05, mostra una progressió cap a una major modularitat i control de la interfície d'usuari a través de la introducció de nous components QML i la separació de responsabilitats.
Punt de referència inicial per a la modularitat i separació de responsabilitats - ampMeter00 _05
En aquesta versió, el fitxer main.qml configura una finestra amb un Rectangle blanc. Inclou dos components QML personalitzats: AmpMeter i Botonera .
- El component
AmpMeteres centra a la pantalla i gestiona la visualització del fons i l'agulla d'un amperímetre. La rotació de l'agulla es controla mitjançant una propietatrotationexposada i animada amb unaNumberAnimationque utilitzaEasing.InOutBack. - El component
Botoneraes col·loca a la part inferior i es centra horitzontalment. S'alimenta d'unListModelanomenatbuttonModelque conté les etiquetes i els valors de rotació per als botons. Quan un botó és clicat, emet un senyalbuttonClickedamb el seu valor, i aquest valor s'assigna a la propietatrotationdel componentampMeter.
Introducció de Múltiples AmpMeters - ampMeter00 _06
Aquesta versió es titula "ampMeter00 _06 - Triple" i incrementa l'amplada de la finestra a 960 píxels. La millora principal és la capacitat de mostrar i controlar tres components AmpMeter simultàniament.
- Els tres
AmpMeter(ampMeter1,ampMeter2,ampMeter3) estan organitzats en unaRow(fila) centrada, amb un desplaçament vertical per deixar espai als controls. Cadascun té dimensions i escala definides de manera individual (per exemple,ampMeter1iampMeter3ambscale: 0.5, iampMeter2ambscale: 0.8). - El
ListModeldels botons es manté, però el gestoronButtonClickedamain.qmlara actualitza la propietatrotationdels tres componentsAmpMeterde forma idèntica (ampMeter1.rotation = value,ampMeter2.rotation = value,ampMeter3.rotation = value). - Els components
Botonera.qmliAmpMeter.qmlconserven la seva estructura anterior.
Incorporació d'un lliscador o slider - ampMeter00 _07
Continuant amb la configuració "Triple", aquesta versió afegeix un nou element de control: un slider (lliscador).
- Es mantenen els tres components
AmpMeteren fila. - S'introdueix un
Column(sliderContainer) que conté la lògica del slider, situat just a sobre dels botons de control. - Aquest slider consisteix en un
Rectangleque serveix de pista i un altreRectangle(sliderHandle) que fa de mànec lliscant. - Un
MouseAreapermet arrossegar elsliderHandlenomés en l'eix X (drag.axis: Drag.XAxis). - El gestor
onPositionChangedcalcula un valor (value) entre -60 i +60, el converteix en un angle de rotació entre -45 i +45, i l'aplica exclusivament aampMeter2(ampMeter2.rotation = rotation). - Es mostren etiquetes per als valors extrems (-60 i +60) i el valor actual del slider.
- La
Botoneraara només controlaampMeter1iampMeter3, ja queampMeter2és controlat pel nou slider.
Modularització del lliscador amb BarraLliscant.qml - ampMeter00 _08
En aquesta etapa, la lògica del lliscador es refactoritza en un component QML independent anomenat BarraLliscant.qml .
- A
main.qml, elsliderContaineres reemplaça per una instància del componentBarraLliscant. BarraLliscant.qmlencapsula tota la interfície i la lògica del lliscador, incloent les etiquetes, la pista, el mànec, el càlcul del valor i la visualització del valor actual.- Defineix propietats configurables com
minValue,maxValue,sliderWidthisliderHeight. - Emet un senyal
valueChanged(real value)quan el valor del lliscador canvia. - A
main.qml, el senyalonValueChangedde laBarraLliscants'utilitza per convertir el valor del lliscador en rotació i aplicar-lo aampMeter2. - El component
AmpMeter.qmlintrodueix propietatsfondoSourceiagullaSourceper personalitzar les imatges, tot i que inicialment mantenen els mateixos valors per defecte. - La
Botoneracontinua controlantampMeter1iampMeter3.
Modularització completa amb Vista.qml i Controlador.qml - ampMeter00 _09
Aquesta és la versió més modularitzada, amb el títol "ampMeter00 _09 - Modular". main.qml se simplifica enormement delegant les responsabilitats a dos nous components de nivell superior: Vista i Controlador .
-
Vista.qml: Aquest component agrupa els tres componentsAmpMeteren unaRow.- Exposa les propietats
ampMeter1Rotation,ampMeter2Rotation,ampMeter3Rotationcom àlies per controlar la rotació de cadaAmpMeterdes del seu pare. - Permet personalitzar l'escala de cada
AmpMeter(ampMeter1Scale,ampMeter2Scale,ampMeter3Scale). - Una novetat important és que cada
AmpMeterdins de laVistapot tenir un tipus d'animació deasing diferent (Easing.InOutQuad,Easing.InOutBack,Easing.InOutCubic), cosa que requereix una nova propietateasingTypeaAmpMeter.qml.
- Exposa les propietats
-
Controlador.qml: Aquest component agrupa laBarraLliscanti laBotoneraen unaColumn.- Rep el
buttonModelper a laBotonera. - Emet senyals
sliderValueChanged(real value)ibuttonClicked(real value)per comunicar els esdeveniments dels seus components interns al seu pare. - Connecta el senyal
onValueChangedde la sevaBarraLliscantinterna al seu propisliderValueChanged. - Connecta el senyal
onButtonClickedde la sevaBotonerainterna al seu propibuttonClicked.
- Rep el
- A
main.qml, el gestoronSliderValueChangeddelControladorara actualitzavista.ampMeter2Rotation. - El gestor
onButtonClickeddelControladoractualitzavista.ampMeter1Rotationivista.ampMeter3Rotation.
En resum, l'evolució del codi il·lustra una transició des d'una aplicació monolítica amb la lògica de control i vista al fitxer main.qml , cap a una estructura modular on components reutilitzables ( AmpMeter , Botonera , BarraLliscant ) s'organitzen en components de més alt nivell ( Vista , Controlador ), millorant la mantenibilitat i la claredat del codi.
Implementació en Python
Instal·lació de pyside6
- Projecte PySide6
- A Linux (Debian 13):
sudo apt install python3-pip python3-pipdeptree sudo apt install python3-qtpy-pyside6
- A un entorn virtual a Linux o a Windows:
pip install PySide6
Compilació de l'arxiu de recursos .qrc emprant pyside6-rcc
Modificació de l'arxiu de recursos
Per a fer compatibles els arxius .qml previs, s'han afegit dues línies a l'arxiu .qrc (en negreta):
<RCC>
<qresource prefix="/">
<file alias="agulla.svg">material/AmpMeterAgulla_c.svg</file>
<file alias="fons.svg">material/AmpMeterFons_c.svg</file>
<file>material/AmpMeterAgulla_c.svg</file>
<file>material/AmpMeterFons_c.svg</file>
</qresource>
</RCC>
Compilació
- Cal compilar l'arxiu de recursos qrc. En aquest exemple recursos.qrc es compilat amb pyside6-rcc (nom del compilat: rc_recursos.py):
pyside6-rcc recursos.qrc -o rc_recursos.py
Execució
python main.py
main.py
#!/usr/bin/env python3
import sys
import os
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QUrl
# Importar els recursos compilats
# pyside6-rcc recursos.qrc -o rc_recursos.py
import rc_recursos
def main():
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
# Afegir el directori actual al path de cerca de QML
current_dir = os.path.dirname(os.path.abspath(__file__))
engine.addImportPath(current_dir)
# Carregar el fitxer QML principal des del sistema de fitxers
qml_file = os.path.join(current_dir, "main.qml")
engine.load(QUrl.fromLocalFile(qml_file))
# Verificar que el QML s'ha carregat correctament
if not engine.rootObjects():
print("Error: No s'ha pogut carregar main.qml")
return -1
return app.exec()
if __name__ == "__main__":
sys.exit(main())
main.qml
L'únic canvi a main.qml és el títol:
// main.qml
import QtQuick
Window {
width: 960
height: 480
visible: true
title: qsTr("ampMeter00_10 - Python")
Rectangle {
id: root
anchors.fill: parent
color: "white"
// Model de dades per als botons
ListModel {
id: buttonModel
ListElement { label: "-60"; value: -45 }
ListElement { label: "-30"; value: -20 }
ListElement { label: "0"; value: 0 }
ListElement { label: "30"; value: 20 }
ListElement { label: "60"; value: 45 }
}
// Component Vista amb els tres AmpMeters
Vista {
id: vista
anchors.centerIn: parent
anchors.verticalCenterOffset: -40
}
// Component Controlador amb BarraLliscant i Botonera
Controlador {
id: controlador
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 20
model: buttonModel
onSliderValueChanged: function(value) {
// Converteix el valor (-60 a +60) a rotació (-45 a +45)
var rotation = (value / 60) * 45
vista.ampMeter2Rotation = rotation
}
onButtonClicked: function(value) {
// Controla ampMeter1 i ampMeter3
vista.ampMeter1Rotation = value
vista.ampMeter3Rotation = value
}
}
}
}