Un programa Qt per dins
En C, main comença a dalt i acaba a baix. En un programa amb finestres no: main prepara les finestres i després es queda esperant que l'usuari faci alguna cosa. Això es diu programació dirigida per esdeveniments, i és el canvi de xip més gran d'aquesta unitat.
a.exec()El main.cpp de qualsevol projecte
#include "marcador.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Marcador w;
w.show();
return a.exec();
}
QApplication a(argc, argv);crea l'objecte que reparteix els esdeveniments. Ha d'existir abans de crear cap giny.Marcador w;crea la nostra finestra: aquí s'executa el constructor de la classe que has dissenyat.w.show();la fa visible.return a.exec();entra al bucle d'esdeveniments. Només en surt quan es tanca l'última finestra, i el valor que retorna és el codi de sortida del programa.
Gairebé mai tocaràs aquest fitxer: tot el que fa el programa és dins de la classe.
A06.00: el primer projecte
- Instal·la Qt Creator amb un kit Desktop de Qt 6. Al panell Projects comprova que el kit està seleccionat.
- Fitxer ▸ Nou projecte ▸ Application (Qt) ▸ Qt Widgets Application, amb CMake com a sistema de compilació.
- A la pàgina de detalls posa el nom de la classe amb majúscula inicial (
Marcador, nomarcador), deixaQWidgetcom a classe base i el Generate form marcat. - Prem ▶: ha de sortir una finestra buida. El Creator ha creat
main.cpp, la classe (.hi.cpp), el.uii elCMakeLists.txt.
El nom de la classe acaba escrit a tres llocs (ho veuràs al bloc 3); és molt més fàcil encertar-lo el primer cop que canviar-lo després. Els exemples d'aquesta pàgina funcionen amb Qt 6.4 o més nou. Quan un exercici necessiti una funció més recent, la pàgina 2 ho diu.
El projecte CMake
El CMakeLists.txt diu quins fitxers formen el programa i quines parts de Qt necessita. El Creator el genera, i en aquesta unitat només l'has de tocar quan afegeixes fitxers. Fes clic a cada línia.
El CMakeLists.txt d'un projecte del Creator, línia a línia
Tres eines que treballen soles
Abans de compilar, Qt passa el teu codi per tres generadors. Tu no els crides: s'activen des del CMakeLists.txt.
| Eina | Què fa | Fitxer que genera | Activada? |
|---|---|---|---|
moc (AUTOMOC) | Llegeix les classes amb Q_OBJECT i genera el codi dels senyals i la informació que permet connectar per nom. | moc_marcador.cpp | sí, per qt_standard_project_setup() |
uic (AUTOUIC) | Converteix cada .ui (XML) en una classe C++. | ui_marcador.h | sí, per qt_standard_project_setup() |
rcc (AUTORCC) | Converteix els recursos (.qrc, per exemple imatges) en codi C++. | qrc_ex04.cpp | no: caldrà set(CMAKE_AUTORCC ON) (pàgina 2) |
Els fitxers generats viuen a la carpeta de compilació (build/…/…_autogen/), no dins del projecte. Per això no els veuràs al Creator, i per això, quan lliures el .zip, no hi va la carpeta de compilació ni el CMakeLists.txt.user.
Designer i la classe Ui::
El .ui és un XML que descriu els ginys. No s'edita a mà: al Creator, doble clic i s'obre el Designer. El que hi dibuixes acaba convertit en codi C++. Tria un fitxer per veure què és i qui el fa.
Del .ui a ui->lbPunts
Layouts i spacers
Cada giny va dins d'un layout (vertical, horitzontal, quadrícula) i així la finestra s'adapta quan canvia de mida. Un spacer és un espai elàstic que empeny els ginys. Un layout buit, com hlCamps a l'exemple del bloc 6, és un lloc reservat que el codi ompliria més tard amb addWidget.
Els ginys d'A06.01
| Giny | Senyal que interessa | Funció per canviar-lo | Notes |
|---|---|---|---|
QPushButton | clicked() | setText(QString) | el text es posa al Designer |
QLabel | — | setNum(int), setText(QString) | només mostra |
QSlider | valueChanged(int) | setValue(int) | per defecte va de 0 a 99; orientation el fa vertical |
QLCDNumber | — | display(int) | la propietat digitCount fixa les xifres |
QSpinBox | valueChanged(int) | setValue(int), setRange(min, max) | caixa numèrica amb fletxes |
La columna de la dreta la pots veure al Designer: selecciona el giny i mira la finestra Property Editor.
Noms: tria una convenció i mantén-la
- Classes amb majúscula inicial:
Marcador,CampImport. Els fitxers del Creator van en minúscula (marcador.h), i està bé. - Ginys al Designer amb un prefix segons el tipus:
btper als botons,lbper a les etiquetes,spper a les caixes numèriques,hlivlper als layouts. - El
objectNameés el nom de la variable:ui->lbPunts. També és el nom que fa servir la connexió per nom,on_bt2_clicked: si no coincideix exactament, no es connecta.
Canviar el nom d'una classe: el nom és a tres llocs
marcador.h i marcador.cpp | la classe, els constructors i Ui::Marcador |
marcador.ui | <class>Marcador</class> i el objectName del giny arrel: d'aquí surt Ui::Marcador |
| els fitxers que la fan servir | per exemple, Marcador *m = new Marcador; |
Com fer-ho: tanca el .ui del Designer, prem Ctrl+Shift+F (cerca avançada), escriu el nom vell i el nou, marca Case sensitive i Whole words only i restringeix els fitxers a *.h,*.cpp,*.ui. Els noms dels fitxers no canvien. Rename Symbol Under Cursor (Ctrl+Shift+R) no serveix per si sol: no arriba al XML del .ui.
Si canvies Marcador per Tauler només al C++ i et deixes el .ui, el compilador es queixa, però no diu que el problema és al .ui:
marcador.cpp:7:18: error: invalid use of incomplete type 'class Ui::Tauler'
Vol dir que Ui::Tauler no existeix: uic encara ha generat Ui::Marcador, que és el nom que diu el .ui.
Senyals i slots
És la idea central de Qt. Un giny no sap qui l'escolta: només avisa que ha passat alguna cosa. Cinc idees:
- SenyalEl giny l'emet quan passa alguna cosa: «m'han clicat», «el valor ha canviat». No sap qui l'escolta.
- SlotUna funció normal que pot respondre a un senyal. També la pots cridar directament, com qualsevol funció.
- connectEls uneix. Un senyal pot anar a molts slots, un slot pot rebre molts senyals, i un senyal pot anar a un altre senyal.
- DesacoblamentL'emissor no coneix el receptor. Per això una mateixa classe serveix en projectes diferents sense canviar-la.
- OrdreL'emissió és una crida directa: els slots s'executen un darrere l'altre, en l'ordre en què es van connectar, i l'
emitno torna fins que han acabat.
Regla de compatibilitat: un slot pot ignorar arguments del final del senyal, però no pot demanar-ne més. Per exemple, valueChanged(int) pot anar a setValue(int) o a un slot sense arguments, però no a un slot amb dos.
Tres maneres de connectar
1. Per nom. No escrius cap connect. La funció setupUi crida connectSlotsByName, que busca els slots amb la forma on_<objectName>_<senyal> i els connecta sols. El slot ha d'estar sota private slots: (mira el marcador.h del visor de fitxers de dalt).
2. Al Designer (Edit Signals/Slots, tecla F4). Arrossegues des del giny emissor fins al receptor i tries senyal i slot. La connexió queda guardada dins del .ui i no escrius cap codi. És ideal quan un giny estàndard n'ha de governar un altre, com un QSlider que mou un QLCDNumber. Prova-ho aquí: dues connexions, una en cada sentit.
Cada widget només emet valueChanged quan el valor canvia de veritat. Per això, dues connexions en cercle (dial → caixa i caixa → dial) no fan voltes infinites: la segona vegada el valor ja és el mateix i no es torna a emetre. A l'últim bloc veuràs què passa quan aquesta protecció no hi és.
3. Amb connect() al codi. Quatre formes de connectar el mateix botó a un slot (compilades i provades: les tres primeres fan exactament el mateix):
// 1. Per nom: no hi ha connect(); el slot s'anomena on_bt3_clicked()
// 2. Moderna (la que recomano):
connect(bt3, &QPushButton::clicked, this, &Marcador::afegeixTres);
// 3. Amb cadenes (codi antic i enunciats):
connect(bt3, SIGNAL(clicked()), this, SLOT(afegeixTres()));
// 4. Amb una lambda (per a una línia):
connect(bt3, &QPushButton::clicked, this, [this] { suma(3); });
Llegir un connect modern
connect(/*emissor*/ bt3, /*senyal*/ &QPushButton::clicked, /*receptor*/ this, /*slot*/ &Marcador::afegeixTres);
Quatre arguments: qui emet, quin senyal, qui rep i quina funció l'atén. Els senyals i els slots s'anomenen com a funcions membre (&Classe::nom), i el compilador els comprova.
Cadenes contra funcions. Amb SIGNAL() i SLOT() Qt rep només text i el resol en executar el programa. Per això un nom mal escrit compila igual i només s'ensuma per la consola. És la forma que veuràs a l'enunciat de l'A06.02 i a codi antic; a la resta de la pàgina faig servir la moderna.
| Amb cadenes | Moderna |
|---|---|
connect(a, SIGNAL(x(int)), b, SLOT(y(int))) | connect(a, &A::x, b, &B::y) |
| el nom és text: s'ha d'escriure exacte, i els arguments sense el nom de la variable | el nom és una funció: l'editor l'autocompleta i renomenar el símbol l'actualitza |
| nom mal escrit: avís a la consola en executar | nom mal escrit: error de compilació |
Funcions sobrecarregades. Si el senyal o el slot té diverses versions, cal dir quina vols amb qOverload. Per exemple, QLCDNumber::display existeix per a int, double i QString:
connect(lliscant, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));
Sense el qOverload el compilador es queixa d'un unresolved overloaded function type. És exactament la connexió que Designer genera quan arrossegues un slider fins a un LCD.
Classes pròpies: senyals i slots
Un giny teu ha de poder dir què passa (un senyal) i deixar que li diguin què ha de fer (un slot). Aquesta parella és la seva interfície. Com a exemple, un CampImport: una caixa numèrica amb una etiqueta d'unitat.
campimport.h
#ifndef CAMPIMPORT_H
#define CAMPIMPORT_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class CampImport;
}
QT_END_NAMESPACE
class CampImport : public QWidget
{
Q_OBJECT
public:
CampImport(QWidget *parent = nullptr);
~CampImport();
void configura(const QString &etiqueta, int minim, int maxim);
public slots:
void setImport(int valor);
signals:
void importCanviat(int valor);
private:
Ui::CampImport *ui;
};
#endif // CAMPIMPORT_H
Q_OBJECTés imprescindible a la primera línia de la classe: sense ell,mocno genera el codi dels senyals i el programa no enllaça.- Un
signalnomés es declara, sotasignals:. No l'implementes: ho famoc. - Un
slotes declara sotapublic slots:(oprivate slots:) i s'implementa com una funció qualsevol. - Per emetre un senyal, l'escrius com una crida:
emit importCanviat(v);. L'emités només una marca per llegir millor (si te'l deixes, funciona igual), però s'escriu sempre.
Parelles de noms
Qt anomena la parella valueChanged / setValue: el senyal en passat («ha canviat») i el slot com una ordre («posa»). Fes-ho igual: importCanviat(int) i setImport(int). Costa poc i el codi es llegeix sol.
Reenviar un senyal
El QSpinBox de dins ja emet valueChanged(int). En lloc d'escriure un slot que rebi el valor i faci emit importCanviat(v), connectem un senyal amb l'altre al constructor:
CampImport::CampImport(QWidget *parent)
: QWidget(parent)
, ui(new Ui::CampImport)
{
ui->setupUi(this);
connect(ui->spImport, &QSpinBox::valueChanged, this, &CampImport::importCanviat);
}
Cap giny de fora no sap que hi ha un QSpinBox a dins: només veu importCanviat.
Dissenya la interfície abans d'ajuntar res
Quan construeixes dos ginys per separat i després els has d'integrar (com fas a l'A06.01), és més fàcil si des del primer dia et preguntes: què ha de dir aquest giny al món? (un senyal) i què li han de poder ordenar? (un slot). Si no, acabaràs reobrint cada classe per afegir-hi el que faltava.
Ginys dins d'altres ginys
Un Convertidor conté dos CampImport (euros i cèntims). Al Designer, el Convertidor només té un layout buit, hlCamps; el codi hi posa dins els dos camps.
El constructor del Convertidor
Convertidor::Convertidor(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Convertidor)
{
ui->setupUi(this);
euros = new CampImport;
cents = new CampImport;
euros->setObjectName("euros");
cents->setObjectName("cents");
euros->configura("euros", 0, 99);
cents->configura("cèntims", 0, 9999);
ui->hlCamps->addWidget(euros);
ui->hlCamps->addWidget(cents);
connect(euros, &CampImport::importCanviat, this, &Convertidor::deEurosACentims);
connect(cents, &CampImport::importCanviat, this, &Convertidor::deCentimsAEuros);
}
new CampImport;crea un giny sense pare. Un giny sense pare és una finestra pròpia (comprovat:isWindow()és cert).addWidgetel col·loca al layout, i el layout fa que el pare del camp sigui elConvertidor: ara és una peça de dins de la finestra i ja no és una finestra apart. Si t'oblides de l'addWidget, el camp no surt a la finestra.- Els
connectvan al final, quan els dos camps ja existeixen.
No cal cap delete: quan es destrueix un giny, destrueix tots els seus fills. Per això new sense delete és normal en ginys amb pare.
El pare fa de mediador
void Convertidor::deEurosACentims(int e)
{
cents->setImport(e * 100);
}
void Convertidor::deCentimsAEuros(int c)
{
euros->setImport(c / 100);
}
Un CampImport no sap res de l'altre. Tota la relació (aquí, la conversió ×100) viu al pare. Així el mateix CampImport serviria per a un convertidor de metres i centímetres, o de qualsevol altra cosa.
Quan els senyals fan voltes
Al convertidor, un canvi d'euros n'avisa els cèntims, i un canvi de cèntims n'avisa els euros. Si els dos avisos se seguissin sempre, es mouria en cercle. Poden passar tres coses, i la diferència és setImport.
Cas 1: el widget s'atura sol
Amb ginys estàndard (el dial i la caixa del bloc 4) no cal fer res: setValue amb el valor que ja té no emet cap senyal, i la cadena s'atura.
Cas 2: l'eco que t'espatlla el valor
Si setImport simplement fa setValue:
void CampImport::setImport(int valor)
{
ui->spImport->setValue(valor);
}
L'usuari escriu 150 als cèntims. El resultat, executat de veritat:
usuari escriu 150 a cents
Convertidor::deCentimsAEuros(150)
CampImport[euros]::setImport(1)
Convertidor::deEurosACentims(1)
CampImport[cents]::setImport(100)
Convertidor::deCentimsAEuros(100)
CampImport[euros]::setImport(1)
= euros 1 | cents 100
La cadena s'atura, però l'usuari ha escrit 150 i veu 100. La divisió entera 150 / 100 perd els 50, i l'eco ha tornat a escriure els cèntims. Amb 200 tot va bé, perquè el resultat torna a ser 200: és l'error que apareix «a vegades» i és el més difícil de trobar.
Cas 3: sense fre
Si setImport emet sempre, encara que el valor no hagi canviat:
void CampImport::setImport(int valor)
{
QSignalBlocker blocker(ui->spImport);
ui->spImport->setValue(valor);
emit importCanviat(valor);
}
Amb 1 euro, els dos camps s'avisen sense parar:
usuari escriu 1 a euros
Convertidor::deEurosACentims(1)
CampImport[cents]::setImport(100)
Convertidor::deCentimsAEuros(100)
CampImport[euros]::setImport(1)
Convertidor::deEurosACentims(1)
CampImport[cents]::setImport(100)
Convertidor::deCentimsAEuros(100)
CampImport[euros]::setImport(1)
Convertidor::deEurosACentims(1)
CampImport[cents]::setImport(100)
Convertidor::deCentimsAEuros(100)
[aturat: recursió massa profunda]
Cada crida es fa dins de l'anterior, així que la pila de crides creix fins a desbordar-se. El programa de prova el talla a l'onzè nivell perquè es pugui veure; sense aquest límit, acaba amb un Segmentation fault.
Les eines
| Problema | Eina |
|---|---|
| voltes infinites | el setter emet només si el valor canvia (if (v == n) return;): és el que fan els ginys de Qt |
| l'eco torna a escriure el valor | quan canvies l'altre giny des del codi, silencia'n els senyals amb QSignalBlocker |
| les dues coses alhora | una direcció que mana i un setter que no fa eco, com al pas a pas «Convertidor amb QSignalBlocker» |
void CampImport::setImport(int valor)
{
QSignalBlocker blocker(ui->spImport);
ui->spImport->setValue(valor);
}
QSignalBlocker bloqueja els senyals de l'objecte mentre existeix i els desbloqueja sol quan la funció acaba, encara que surtis abans amb un return. És millor que blockSignals(true) i blockSignals(false) escrits a mà, que és fàcil deixar-ne un sense fer.
Si surt un error
Els errors de Qt sovint no passen quan compiles, sinó quan executes, i aleshores només surt un missatge a la consola. Al Creator es veuen a Application Output (execució) i a Issues i Compile Output (compilació). Tots els missatges d'aquesta taula són reals: s'han provocat i copiat d'una execució de veritat.
| Què veus | Quan | Què vol dir |
|---|
El que sembla un error i no ho és
- Deixar-te l'
emitquan emets un senyal: compila i funciona igual. - No llistar el
.uiaqt_add_executable:uicel troba igualment pel#include "ui_marcador.h". Llistat, el Creator el mostra a l'arbre del projecte.
Tria un exemple i avança'l pas a pas. A l'esquerra veus el codi i l'estat; a la dreta, la finestra i la traça de crides: qui crida qui, i quins senyals s'emeten pel camí. Les traces estan copiades d'executar els programes de veritat (amb Qt 6.4), no inventades. Els camps de sobre el codi et deixen canviar els valors.
| On | Valor |
|---|
⚡ senyal (l'emet un giny) · ▸ funció (s'executa un slot o una funció) · el sagnat indica que la crida es fa dins de l'anterior.
A06.00 i A06.01
Cada targeta diu què necessites de la teoria i té dues pistes que pots obrir d'una en una. L'A06.02 es treballa a introQt_02.html.
A06.00 Instal·lació de la versió educativa del Qt Creator
Instal·la el Qt Creator amb un kit de Qt 6 per a escriptori i comprova que pots crear, compilar i executar un projecte.
Pista 1
Quan obris el Creator per primer cop, al panell Projects ha d'haver-hi un kit Desktop Qt 6.x amb el compilador i CMake detectats (sense triangles vermells).
Pista 2
Crea un projecte Qt Widgets Application amb CMake i executa'l. Si surt una finestra buida, ja ho tens. Fixa't en la versió de Qt que fa servir el kit.
a0601a Dos botons i una etiqueta numèrica
Fes una interfície gràfica (GUI) amb dos botons (+ i -) i una etiqueta numèrica.
Pista 1
Al Designer necessites dos QPushButton i un QLabel, cadascun amb un objectName que segueixi una convenció. El número el guarda un membre privat de la classe; els botons el modifiquen i l'etiqueta el mostra amb setNum.
Pista 2
Decideix què passa als extrems: fins on ha de pujar i baixar? I pensa ja en l'a0601c: quin senyal hauria d'emetre el teu giny quan canvia el número, i quin slot deixaria que algú de fora li'n posi un?
a0601b Barra lliscant i LCD de dos números
Fes una interfície gràfica (GUI) amb una barra lliscant (slider) i un giny LCD de dos números. La barra lliscant controla l'LCD.
Pista 1
Pensa quin senyal emet el slider quan el mous i quin slot de l'LCD serveix per mostrar un enter. El Designer té un mode per connectar-los sense escriure codi (F4).
Pista 2
L'LCD ha de tenir dues xifres (propietat digitCount) i el slider va, per defecte, de 0 a 99. Quan integris el slider a l'a0601c, necessitaràs que avisi cap a fora: repassa «Reenviar un senyal» al bloc 5.
a0601c Integrar els dos projectes
Fes una interfície gràfica (GUI) que integri els dos projectes anteriors. La botonera també controla la barra lliscant i l'LCD. La barra lliscant també controla el valor de l'etiqueta numèrica.
Pista 1
Crea una classe nova (un giny) amb dos layouts buits i, a cadascun, posa-hi amb addWidget un dels dos ginys anteriors. Copia els fitxers dels dos ginys al projecte nou i afegeix-los a qt_add_executable.
Pista 2
Cada giny necessita un senyal (avisa quan canvia el seu valor) i un slot (deixa que li'n posin un des de fora). Connecta'ls de forma creuada al constructor del pare. Abans de provar-ho, llegeix el bloc 7: què passa quan el botó mou el slider i el slider avisa el botó?
Què cal entregar i com explicar-ho
Un document PDF amb les solucions explicades i un .zip amb els codis, sense el CMakeLists.txt.user i sense la carpeta de compilació. Els noms exactes dels fitxers són els de l'enunciat. Perquè el PDF expliqui i no només ensenyi codi, comprova que per a cada exercici hi ha:
- una captura de la finestra funcionant;
- els ginys que has posat, amb el seu
objectNamei dins de quin layout; - un esquema de connexions: qui emet ▸ quin senyal ▸ qui rep ▸ quin slot, una fletxa per connexió;
- per què no hi ha bucle de senyals (quin dels dos costats fa de fre);
- un error que t'hagi sortit, amb el missatge de la consola, i com l'has resolt.
Entrena't: què passarà?
Llegeix el codi sense executar-lo, pensa la resposta i després mostra la solució. Cada cop surt un exercici diferent.
Troba l'error
Vuit casos curts, tots reals. Abans d'obrir cada targeta decideix: compila? què fa en executar-lo? Després compara amb el missatge que dona Qt de veritat.