Comincio subito con il ringraziare b3h3m0th che mi ha segnalato che le QT non sono più di Nokia dalla scorsa estate e adesso appartengono a digia.
Dopo essere rimasto favorevolmente impressionato dalle QT ho deciso di approfondirle ulteriormente andando a complicare l’esempio di collegamento al database. L’idea era di avere una griglia con l’elenco dei record e un dettaglio separato (in modo da sincronizzare il record attivo sulla griglia con una parte separata ma dipendente)
La finestra ha l’aspetto in figura.
![Il nostro primo risultato](https://okpanico.wordpress.com/wp-content/uploads/2013/02/form.png?w=530&h=225)
Lo schema del database era il seguente:
CREATE TABLE tblProvincia
(cod character varying(2) NOT NULL,
des character varying(40)
);
CREATE TABLE tbllocalita
(
cod character varying(4) NOT NULL,
des character varying(40),
fk_provincia_cod character varying(2)
);
Il codice C++ invece è il seguente:
#include <QMessageBox>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QTableWidgetItem>
#include <QDir>
#include <QSqlQuery>
#include <QtSql>
#include <QDataWidgetMapper>
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
db.close();
delete ui;
}
void MainWindow::on_btnDBConnection_clicked()
{
//Connessione al DB
db = QSqlDatabase::addDatabase("QSQLITE");
QString path(QDir::home().path());
path.append(QDir::separator()).append("qtTest.sqlite");
path = QDir::toNativeSeparators(path);
db.setDatabaseName(path);
bool ok = db.QSqlDatabase::open();
if (!ok)
QMessageBox::critical(0, QObject::tr("Errore database"), db.lastError().text());
}
void MainWindow::on_btnQuery_clicked()
{
if (db.isOpen())
{
QSqlRelationalTableModel *model = new QSqlRelationalTableModel;
model->setTable("tbllocalita");
provIndex = model->fieldIndex("fk_provincia_cod");
model->setRelation(provIndex, QSqlRelation("tblprovincia", "cod", "des"));
model->setHeaderData(0, Qt::Horizontal, tr("Codice"));
model->setHeaderData(1, Qt::Horizontal, tr("Descrizione"));
model->setHeaderData(2, Qt::Horizontal, tr("Provincia"));
model->select();
ui->tblData->setModel(model);
QSqlTableModel *relModel = model->relationModel(provIndex);
ui->qcbProvincia->setModel(relModel);
ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));
QDataWidgetMapper *mapper = new QDataWidgetMapper;
mapper->setModel(model);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
mapper->addMapping(ui->pteCod, 0);
mapper->addMapping(ui->pteDes, 1);
mapper->addMapping(ui->qcbProvincia, provIndex);
mapper->toFirst();
connect(ui->tblData->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), mapper, SLOT(setCurrentModelIndex(QModelIndex)));
}
}
void MainWindow::on_btnExit_clicked()
{
QApplication::quit();
}
Rispetto al mio precedente esempio ci sono poche modifiche. La prima è che ho dovuto usare un modello dati di tipo QSqlRelationalTableModel. In questo modo ho potuto impostare una relazione tra il campo fk_provincia_cod della tabella tbllocalita e il codice della tblprovincia in modo da salvare il codice come dato ma vedere la sua descrizione come visualizzazione. In parole povere la classica normalizzazione della base dati.
Usando un modello di tipo QSqlRelationalTableModel ho dovuto eliminare la query ed impostare una tabella di partenza:
model->setTable("tbllocalita");
ed impostarne la relazione con la tblprovincia:
provIndex = model->fieldIndex("fk_provincia_cod");
model->setRelation(provIndex, QSqlRelation("tblprovincia", "cod", "des"));
Attenzione… se siete tentati come me nella prima versione a non dichiarare una variabile provIndex di tipo int e ad usare ovunque direttamente
model->fieldIndex("fk_provincia_cod")
non fatelo… otterreste un bruttissimo errore di “QComboBox::setModel: cannot set a 0 model” a runtime alla riga:
ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));
Sinceramente non capisco il perché dell’errore… se qualcuno ha dei suggerimenti e ben accetto 🙂
Dopo di che ho usato un QDataWidgetMapper per legare i widget alla sorgente dati:
QDataWidgetMapper *mapper = new QDataWidgetMapper;
mapper->setModel(model);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
mapper->addMapping(ui->pteCod, 0);
mapper->addMapping(ui->pteDes, 1);
mapper->addMapping(ui->qcbProvincia, provIndex);
E per ultimo per far si che allo spostarsi della cella attiva sulla griglia venissero aggiornati i dati dei widget sotto la stessa ho creato una connessione tra il segnale dell’oggetto mittente (sender) e il metodo dell’oggetto ricevente (receiver).
connect(ui->tblData->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), mapper, SLOT(setCurrentModelIndex(QModelIndex)));
Come si vede nella finestra anche la combo è popolata con le descrizioni giuste.
![Una bella combo box con i miei dati](https://okpanico.wordpress.com/wp-content/uploads/2013/02/form2.png?w=530&h=438)
Le istruzioni per caricare i dati nella combo sono state le seguenti:
ui->qcbProvincia->setModel(relModel);
ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));
...
...
mapper->addMapping(ui->qcbProvincia, provIndex);
E se si volesse fare un collegamento master detail? Altra prassi comune in applicativi database oriented… le cose 🙂 rimangono ancora semplici.
![Master - Detail](https://okpanico.wordpress.com/wp-content/uploads/2013/02/form3.png?w=530&h=395)
Le tabelle usate hanno la seguente struttura:
CREATE TABLE tblPersona (
ID INTEGER,
NOME character varying(40),
PRIMARY KEY (ID)
);
CREATE TABLE tblFilm (
ID INTEGER,
FK_PERSONA_ID INTEGER,
TITOLO character varying(40),
PRIMARY KEY (ID),
FOREIGN KEY (FK_PERSONA_ID) REFERENCES tblPersona
);
Il codice C++ è invece il seguente:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtSql/QSqlDatabase>
#include <QSqlRelationalTableModel>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_btnDBConnection_clicked();
void on_btnExit_clicked();
void updateDetailGrid();
void on_btnOpenTable_clicked();
private:
Ui::MainWindow *ui;
QSqlDatabase db;
QSqlRelationalTableModel *masterModel;
QSqlRelationalTableModel *detailModel;
};
e poi,
#include <QDir>
#include <QMessageBox>
#include <QSqlError>
#include <QSqlRelationalTableModel>
#include <QSqlRecord>
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
masterModel = NULL;
detailModel = NULL;
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
db.close();
if (masterModel == NULL)
delete masterModel;
if (detailModel == NULL)
delete detailModel;
delete ui;
}
void MainWindow::on_btnDBConnection_clicked()
{
//Connessione al DB
db = QSqlDatabase::addDatabase("QSQLITE");
QString path(QDir::home().path());
path.append(QDir::separator()).append("qtTest.sqlite");
path = QDir::toNativeSeparators(path);
db.setDatabaseName(path);
bool ok = db.QSqlDatabase::open();
if (!ok)
QMessageBox::critical(0, QObject::tr("Errore database"), db.lastError().text());
}
void MainWindow::updateDetailGrid()
{
QModelIndex index = ui->vieGridMaster->currentIndex();
if (index.isValid())
{
QSqlRecord record = masterModel->record(index.row());
int id = record.value("id").toInt();
detailModel->setFilter(QString("fk_persona_id = %1").arg(id));
}
else
detailModel->setFilter("fk_persona_id = -1");
detailModel->select();
ui->vieGridDetail->horizontalHeader()->setVisible(detailModel->rowCount() > 0);
}
void MainWindow::on_btnExit_clicked()
{
QApplication::quit();
}
void MainWindow::on_btnOpenTable_clicked()
{
if (db.isOpen())
{
masterModel = new QSqlRelationalTableModel;
masterModel->setTable("tblpersona");
masterModel->setHeaderData(0, Qt::Horizontal, tr("ID"));
masterModel->setHeaderData(1, Qt::Horizontal, tr("Nome"));
masterModel->select();
detailModel = new QSqlRelationalTableModel;
detailModel->setTable("tblfilm");
detailModel->setHeaderData(0, Qt::Horizontal, tr("ID"));
detailModel->setHeaderData(1, Qt::Horizontal, tr("fk_Perosna_id"));
detailModel->setHeaderData(2, Qt::Horizontal, tr("Titolo"));
detailModel->select();
ui->vieGridMaster->setModel(masterModel);
ui->vieGridDetail->setModel(detailModel);
connect(ui->vieGridMaster->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), this, SLOT(updateDetailGrid()));
ui->vieGridMaster->selectRow(0);
}
}
Si creano 2 datamodel di tipo QSqlRelationalTableModel. Dopo di che si crea un evento che collega il cambio di riga selezionata sulla griglia master e lo comunica alla funzione che si occuperà di aggiornare il dettaglio, la funzione updateDetailGrid. Quest’ultima per aggiornare i propri dati crea un filtro sul suo modello dati e aggiorna l’estrazione. Veramente facile 🙂 dopo che si ha acquisito un po’ di esperienza.
Per concludere il mio parere personale è che QT sia un’ottima libreria per lo sviluppo di applicativi client multipiattaforma (insomma mi ha dato una bella soddisfazione usarla per questi test 🙂 ).E’ sicuramente ben studiata e soprattutto va studiata… e con l’avvento di internet (e Google) almeno io tendo a cercare la soluzione piuttosto che leggermi i manuali, per cui l’occhio sui QDataWidgetMapper mi è caduto dopo vari tentativi e ore perse a leggermi vari post. La parte di accesso all’SQL sembra meno curata del resto (ma è la mia prima impressione da utilizzatore di altri framework). L’utilizzo di un filtro per il master/detail funziona bene con pochi record… ma se questi aumentassero? Sicuramente ci sono altri framwork di sviluppo (ad esempio Delphi e la sua VCL) che sono più flessibili e sofisticati (anche nell’integrazione con l’IDE), ma evidentemente gli sviluppatori QT hanno deciso di porre l’enfasi su altri aspetti del framework e in tutta franchezza CHE ASPETTI 🙂