// A06.00 i A06.01: ginys, senyals i slots

Primers passos amb Qt (I)

Un programa que espera, ginys dissenyats al Designer, senyals i slots, i ginys encastats dins d'altres ginys: el que cal per fer l'A06.01.

El que escrius (main.cpp)
#include "marcador.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Marcador w;
    w.show();
    return a.exec();
}
El que veu l'usuari
Marcador
7
+1+2+3Zero
i a.exec() es queda esperant un clic

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.

Usuariclic, tecla, ratolí
Bucle d'esdevenimentsdins d'a.exec()
El teu codiconstructors i slots

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

  1. Instal·la Qt Creator amb un kit Desktop de Qt 6. Al panell Projects comprova que el kit està seleccionat.
  2. Fitxer ▸ Nou projecte ▸ Application (Qt) ▸ Qt Widgets Application, amb CMake com a sistema de compilació.
  3. A la pàgina de detalls posa el nom de la classe amb majúscula inicial (Marcador, no marcador), deixa QWidget com a classe base i el Generate form marcat.
  4. Prem ▶: ha de sortir una finestra buida. El Creator ha creat main.cpp, la classe (.h i .cpp), el .ui i el CMakeLists.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.

EinaQuè faFitxer que generaActivada?
moc (AUTOMOC)Llegeix les classes amb Q_OBJECT i genera el codi dels senyals i la informació que permet connectar per nom.moc_marcador.cppsí, per qt_standard_project_setup()
uic (AUTOUIC)Converteix cada .ui (XML) en una classe C++.ui_marcador.hsí, per qt_standard_project_setup()
rcc (AUTORCC)Converteix els recursos (.qrc, per exemple imatges) en codi C++.qrc_ex04.cppno: 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

GinySenyal que interessaFunció per canviar-loNotes
QPushButtonclicked()setText(QString)el text es posa al Designer
QLabelsetNum(int), setText(QString)només mostra
QSlidervalueChanged(int)setValue(int)per defecte va de 0 a 99; orientation el fa vertical
QLCDNumberdisplay(int)la propietat digitCount fixa les xifres
QSpinBoxvalueChanged(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: bt per als botons, lb per a les etiquetes, sp per a les caixes numèriques, hl i vl per 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.cppla 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 servirper 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'emit no 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.

senyals del dial: 0 · senyals de la caixa: 0

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 cadenesModerna
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 variableel nom és una funció: l'editor l'autocompleta i renomenar el símbol l'actualitza
nom mal escrit: avís a la consola en executarnom 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, moc no genera el codi dels senyals i el programa no enllaça.
  • Un signal només es declara, sota signals:. No l'implementes: ho fa moc.
  • Un slot es declara sota public slots: (o private 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);
}
  1. new CampImport; crea un giny sense pare. Un giny sense pare és una finestra pròpia (comprovat: isWindow() és cert).
  2. addWidget el col·loca al layout, i el layout fa que el pare del camp sigui el Convertidor: 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.
  3. Els connect van 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

ProblemaEina
voltes infinitesel 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 valorquan canvies l'altre giny des del codi, silencia'n els senyals amb QSignalBlocker
les dues coses alhorauna 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è veusQuanQuè vol dir

El que sembla un error i no ho és

  • Deixar-te l'emit quan emets un senyal: compila i funciona igual.
  • No llistar el .ui a qt_add_executable: uic el 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.

Codi (línia acabada d'executar)
Estat
OnValor
La finestra
Traça de crides

⚡ 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.

Finestra CMatrioxca: barra lliscant vertical a l'esquerra, LCD amb el 53, botons + i - i una etiqueta amb el 53
Resultat esperat: el 53 surt a l'LCD i a l'etiqueta.
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 objectName i 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.