En esta página

Qt Quick Controls - Tutorial de chat

Este tutorial muestra cómo escribir una aplicación básica de chat usando Qt Quick Controls. También explicará cómo integrar una base de datos SQL en una aplicación Qt.

Capítulo 1: Configuración

Cuando se configura un nuevo proyecto, lo más fácil es usar Qt Creator. Para este proyecto, elegimos la plantilla de aplicaciónQt Quick , que crea una aplicación básica "Hola Mundo" con los siguientes archivos:

  • CMakeLists.txt - Indica a CMake cómo debe construirse nuestro proyecto
  • Main.qml - Proporciona una interfaz de usuario por defecto que contiene una ventana vacía
  • main.cpp - Carga main.qml
  • qtquickcontrols2.conf - Indica a la aplicación qué estilo debe utilizar

main.cpp

El código por defecto en main.cpp tiene dos includes:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

El primero nos da acceso a QGuiApplication. Todas las aplicaciones Qt requieren un objeto de aplicación, pero el tipo exacto depende de lo que haga la aplicación. QCoreApplication es suficiente para aplicaciones no gráficas. QGuiApplication es suficiente para aplicaciones gráficas que no usen Qt Widgetsmientras que QApplication es necesario para las que sí lo hacen.

El segundo include hace que QQmlApplicationEngine esté disponible, permitiéndonos cargar nuestro QML.

Dentro de main(), configuramos el objeto de aplicación y el motor QML:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.loadFromModule("chattutorial", "Main");

    return app.exec();
}

QQmlApplicationEngine es una envoltura conveniente sobre QQmlEngine, proporcionando la función loadFromModule para cargar fácilmente QML para una aplicación. También añade cierta comodidad para el uso de selectores de archivos.

Una vez que hemos configurado todo en C++, podemos pasar a la interfaz de usuario en QML.

Main.qml

Vamos a modificar el código QML por defecto para adaptarlo a nuestras necesidades.

import QtQuick
import QtQuick.Controls

Verás que el módulo Qt Quick ya ha sido importado. Esto nos da acceso a primitivas gráficas como Item, Rectangle, Text, etc. Para ver la lista completa de tipos, consulta la Qt Quick QML Types documentación.

Añade una importación del módulo Qt Quick Controls. Entre otras cosas, esto proporciona acceso a ApplicationWindow, que reemplazará al tipo raíz existente, Window:

ApplicationWindow {
    width: 540
    height: 960
    visible: true
    ...
}

ApplicationWindow es un Window con algunas facilidades añadidas para crear un header y un footer. También proporciona la base para popups y soporta algunos estilos básicos, como el fondo color.

Hay tres propiedades que casi siempre se establecen al utilizar ApplicationWindow: width, height, y visible. Una vez que las hemos configurado, tenemos una ventana vacía del tamaño adecuado, lista para ser llenada con contenido.

Nota: Se elimina la propiedad title del código por defecto.

La primera "pantalla" de nuestra aplicación será una lista de contactos. Sería bueno tener algún texto en la parte superior de cada pantalla que describa su propósito. Las propiedades header y footer de ApplicationWindow podrían funcionar en esta situación. Tienen algunas características que las hacen ideales para elementos que deben mostrarse en cada pantalla de una aplicación:

  • Están anclados a la parte superior e inferior de la ventana, respectivamente.
  • Ocupan todo el ancho de la ventana.

Sin embargo, cuando el contenido de la cabecera y el pie de página varía en función de la pantalla que esté visualizando el usuario, resulta mucho más sencillo utilizar Page. Por ahora, sólo añadiremos una página, pero en el próximo capítulo, demostraremos cómo navegar entre varias páginas.

    Page {
        anchors.fill: parent
        header: Label {
            padding: 10
            text: qsTr("Contacts")
            font.pixelSize: 20
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
        }
    }

Primero, añadimos una Página, que se dimensiona para ocupar todo el espacio de la ventana utilizando la propiedad anchors.fill.

A continuación, asignamos un Label a su propiedad header. Label extiende el elemento primitivo Text del módulo Qt Quick añadiendo estilo y herencia de font. Esto significa que una etiqueta puede tener un aspecto diferente dependiendo del estilo que se esté utilizando, y también puede propagar su tamaño en píxeles a sus hijos.

Queremos cierta distancia entre la parte superior de la ventana de la aplicación y el texto, así que establecemos la propiedad padding. Esto asigna espacio extra a cada lado de la etiqueta (dentro de sus límites). También podemos establecer explícitamente las propiedades topPadding y bottomPadding.

Establecemos el texto de la etiqueta utilizando la función qsTr(), que asegura que el texto pueda ser traducido por el sistema de traducción de Qt. Es una buena práctica a seguir para el texto que es visible para los usuarios finales de su aplicación.

Por defecto, el texto se alinea verticalmente en la parte superior de sus límites, mientras que la alineación horizontal depende de la dirección natural del texto; por ejemplo, el texto que se lee de izquierda a derecha se alineará a la izquierda. Si utilizáramos estos valores por defecto, nuestro texto estaría en la esquina superior izquierda de la ventana. Esto no es deseable para un encabezado, así que alineamos el texto al centro de sus límites, tanto horizontal como verticalmente.

El archivo del proyecto

El archivo CMakeLists.txt contiene toda la información que necesita CMake para construir nuestro proyecto en un ejecutable que podamos ejecutar.

Para una explicación detallada de este archivo, ver Construir una aplicación QML.

Este es el aspecto actual de nuestra aplicación cuando se ejecuta:

Ventana de aplicación vacía con la cabecera Contactos

Capítulo 2: Listas

En este capítulo, explicaremos cómo crear una lista de elementos interactivos utilizando ListView y ItemDelegate.

ListView Qt Quick ItemDelegate proviene del módulo Qt Quick Controls y proporciona un elemento de vista estándar para su uso en vistas y controles como ListView y ComboBox. Por ejemplo, cada ItemDelegate puede mostrar texto, activarse y desactivarse, y reaccionar a los clics del ratón.

Este es nuestro ListView:

        ...

        ListView {
            id: listView
            anchors.fill: parent
            topMargin: 48
            leftMargin: 48
            bottomMargin: 48
            rightMargin: 48
            spacing: 20
            model: ["Albert Einstein", "Ernest Hemingway", "Hans Gude"]
            delegate: ItemDelegate {
                id: contactDelegate
                text: modelData
                width: listView.width - listView.leftMargin - listView.rightMargin
                height: avatar.implicitHeight
                leftPadding: avatar.implicitWidth + 32

                required property string modelData

                Image {
                    id: avatar
                    source: "images/" + contactDelegate.modelData.replace(" ", "_") + ".png"
                }
            }
        }
        ...

Tamaño y posición

Lo primero que hacemos es establecer un tamaño para la vista. Debe llenar el espacio disponible en la página, así que usamos anchors.fill. Ten en cuenta que Page se asegura de que su cabecera y su pie de página tienen suficiente espacio reservado, así que la vista en este caso se situará debajo de la cabecera, por ejemplo.

A continuación, establecemos margins alrededor de ListView para poner cierta distancia entre él y los bordes de la ventana. Las propiedades de margen reservan espacio dentro de los límites de la vista, lo que significa que las áreas vacías aún pueden ser "desplazadas" por el usuario.

Los elementos deben estar bien espaciados dentro de la vista, por lo que la propiedad spacing se establece en 20.

Modelo

Para rellenar rápidamente la vista con algunos elementos, hemos utilizado un array JavaScript como modelo. Uno de los puntos fuertes de QML es su capacidad para crear prototipos de una aplicación de forma extremadamente rápida, y este es un ejemplo de ello. También es posible simplemente asignar un número a la propiedad del modelo para indicar cuántos elementos necesitas. Por ejemplo, si asigna 10 a la propiedad model, el texto de visualización de cada elemento será un número de 0 a 9.

Sin embargo, una vez que la aplicación supera la fase de prototipo, rápidamente se hace necesario utilizar datos reales. Para ello, lo mejor es utilizar un modelo propio de C++ en subclassing QAbstractItemModel.

Delegar

Vamos a delegate. Asignamos el texto correspondiente del modelo a la propiedad text de ItemDelegate. La forma exacta en que los datos del modelo se ponen a disposición de cada delegado depende del tipo de modelo utilizado. Consulte Modelos y vistas en Qt Quick para obtener más información.

En nuestra aplicación, la anchura de cada elemento de la vista debe ser la misma que la anchura de la vista. Esto garantiza que el usuario tenga mucho espacio para seleccionar un contacto de la lista, lo cual es un factor importante en dispositivos con pantallas táctiles pequeñas, como los teléfonos móviles. Sin embargo, la anchura de la vista incluye nuestros márgenes de 48 píxeles, por lo que debemos tenerlo en cuenta en nuestra asignación a la propiedad width.

A continuación, definimos un Image. Esto mostrará una imagen del contacto del usuario. La imagen tendrá 40 píxeles de ancho y 40 píxeles de alto. Basaremos la altura del delegado en la altura de la imagen, para no tener ningún espacio vertical vacío.

Lista de contactos que muestra tres entradas con avatares

Capítulo 3: Navegación

En este capítulo aprenderás a utilizar StackView para navegar entre las páginas de una aplicación. Aquí está la versión revisada de main.qml:

import QtQuick.Controls

ApplicationWindow {
    id: window
    width: 540
    height: 960
    visible: true

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: ContactPage {}
    }
}

Como su nombre sugiere, StackView proporciona navegación basada en pilas. El último ítem en ser "empujado" a la pila es el primero en ser removido, y el ítem más alto es siempre el que está visible.

De la misma manera que hicimos con Page, le decimos a StackView que llene la ventana de la aplicación. Lo único que queda por hacer después es darle un elemento para mostrar, a través de initialItem. StackView acepta items, components y URLs.

Verás que hemos movido el código de la lista de contactos a ContactPage.qml. Es una buena idea hacer esto tan pronto como tengas una idea general de las pantallas que contendrá tu aplicación. Hacer esto no sólo hace que tu código sea más fácil de leer, sino que asegura que los elementos sólo se instancian desde un componente dado cuando es completamente necesario, reduciendo el uso de memoria.

Nota: Qt Creator proporciona varias soluciones rápidas para QML, una de las cuales le permite mover un bloque de código a un archivo separado (Alt + Enter > Move Component into Separate File).

Otro aspecto a tener en cuenta al utilizar ListView es si se debe hacer referencia a él mediante id, o utilizar la propiedad adjunta ListView.view. La mejor opción depende de varios factores. Si se asigna un id a la vista, las expresiones de vinculación serán más cortas y eficientes, ya que la propiedad attached tiene muy poca sobrecarga. Sin embargo, si planeas reutilizar el delegado en otras vistas, es mejor utilizar las propiedades adjuntas para evitar atar el delegado a una vista en particular. Por ejemplo, utilizando las propiedades adjuntas, la asignación width de nuestro delegado pasa a ser:

width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin

En el capítulo 2, añadimos un ListView debajo de la cabecera. Si ejecutas la aplicación de ese capítulo, verás que el contenido de la vista puede desplazarse por encima de la cabecera:

Esto no es muy agradable, especialmente si el texto de los delegados es lo suficientemente largo como para alcanzar el texto de la cabecera. Lo ideal es tener un bloque sólido de color bajo el texto de la cabecera, pero por encima de la vista. Esto asegura que el contenido del listview no pueda interferir visualmente con el contenido de la cabecera. Tenga en cuenta que también es posible lograr esto estableciendo la propiedad clip de la vista a true, pero hacerlo can affect performance.

ToolBar es la herramienta adecuada para este trabajo. Es un contenedor de acciones y controles tanto para toda la aplicación como sensibles al contexto, como botones de navegación y campos de búsqueda. Lo mejor de todo es que tiene un color de fondo que, como siempre, procede del estilo de la aplicación. Aquí está en acción:

    header: ToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

No tiene diseño propio, así que nosotros mismos centramos la etiqueta dentro de él.

El resto del código es el mismo que en el capítulo 2, excepto que hemos aprovechado la señal clicked para insertar la siguiente página en la vista de pila:

            onClicked: root.StackView.view.push("ConversationPage.qml", { inConversationWith: modelData })

Cuando se empuja un Component o url a StackView, a menudo es necesario inicializar el elemento (eventualmente) instanciado con algunas variables. StackView La función 's push() tiene esto en cuenta, tomando un objeto JavaScript como segundo argumento. Lo utilizamos para proporcionar a la página siguiente el nombre de un contacto, que luego utilizará para mostrar la conversación correspondiente. Observe la sintaxis root.StackView.view.push; es necesaria debido a cómo funcionan las propiedades adjuntas.

Veamos ConversationPage.qml, empezando por las importaciones:

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

Estos son los mismos que antes, excepto por la adición de la importación QtQuick.Layouts, que cubriremos en breve.

Page {
    id: root

    property string inConversationWith

    header: ToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }
    ...

El elemento raíz de este componente es otra Página, que tiene una propiedad personalizada llamada inConversationWith. Por ahora, esta propiedad determinará simplemente lo que muestra la etiqueta de la cabecera. Más adelante, la utilizaremos en la consulta SQL que rellena la lista de mensajes de la conversación.

Para permitir al usuario volver a la página de Contacto, añadimos un ToolButton que llama a pop() cuando se hace clic en él. Un ToolButton es funcionalmente similar a Button, pero proporciona un aspecto más adecuado dentro de un ToolBar.

Existen dos formas de distribuir elementos en QML: los posicionadores de elementos y las distribuciones deQt Quick . Los posicionadores de elementos (Row, Column, etc.) son útiles para situaciones en las que el tamaño de los elementos es conocido o fijo, y todo lo que se necesita es colocarlos ordenadamente en una determinada formación. Los diseños de Qt Quick Layouts pueden posicionar y redimensionar elementos, lo que los hace muy adecuados para interfaces de usuario redimensionables. A continuación, utilizamos ColumnLayout para colocar verticalmente un ListView y un Pane:

    ColumnLayout {
        anchors.fill: parent

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            ...

        }
        ...

        Pane {
            id: pane
            Layout.fillWidth: true
            ...
    }

Pane es básicamente un rectángulo cuyo color proviene del estilo de la aplicación. Es similar a Frame, con la única diferencia de que no tiene trazo alrededor de su borde.

Los elementos que son hijos directos de un diseño disponen de varios attached properties. Utilizamos Layout.fillWidth y Layout.fillHeight en ListView para asegurarnos de que ocupa todo el espacio que puede dentro de ColumnLayout. Lo mismo se hace para el panel. Como ColumnLayout es un diseño vertical, no hay elementos a la izquierda o a la derecha de cada hijo, por lo que cada elemento ocupará todo el ancho del diseño.

Por otro lado, la sentencia Layout.fillHeight en ListView le permitirá ocupar el espacio restante que queda después de acomodar el Panel.

Veamos el listview en detalle:

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.margins: pane.leftPadding + messageField.leftPadding
            displayMarginBeginning: 40
            displayMarginEnd: 40
            verticalLayoutDirection: ListView.BottomToTop
            spacing: 12
            model: 10
            delegate: Row {
                id: messageDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property int index
                readonly property bool sentByMe: index % 2 == 0

                Rectangle {
                    id: avatar
                    width: height
                    height: parent.height
                    color: "grey"
                    visible: !messageDelegate.sentByMe
                }

                Rectangle {
                    width: 80
                    height: 40
                    color: messageDelegate.sentByMe ? "lightgrey" : "steelblue"

                    Label {
                        anchors.centerIn: parent
                        text: messageDelegate.index
                        color: messageDelegate.sentByMe ? "black" : "white"
                    }
                }
            }

            ScrollBar.vertical: ScrollBar {}
        }

Después de rellenar el ancho y alto de su padre, también establecemos algunos márgenes en la vista. Esto nos da una buena alineación con el texto del marcador de posición en el campo "redactar mensaje":

Los márgenes de la vista de lista se alinean con el campo de redacción de mensajes inferior

A continuación, establecemos displayMarginBeginning y displayMarginEnd. Estas propiedades aseguran que los delegados fuera de los límites de la vista no desaparezcan al desplazarse por los bordes de la vista. Es más fácil entender esto comentando las propiedades y viendo lo que ocurre al desplazar la vista.

A continuación, invertimos la dirección vertical de la vista, de modo que los primeros elementos estén en la parte inferior. Los delegados se espacian 12 píxeles, y se asigna un modelo "ficticio " a efectos de prueba, hasta que implementemos el modelo real en el capítulo 4.

Dentro del delegado, declaramos un Row como elemento raíz, ya que queremos que al avatar le siga el contenido del mensaje, como se muestra en la imagen anterior.

Los mensajes enviados por el usuario deben distinguirse de los enviados por un contacto. Por ahora, establecemos una propiedad ficticia sentByMe, que simplemente utiliza el índice del delegado para alternar entre diferentes autores. Usando esta propiedad, distinguimos entre diferentes autores de tres maneras:

  • Los mensajes enviados por el usuario se alinean a la derecha de la pantalla estableciendo anchors.right en listView.contentItem.right.
  • Estableciendo la propiedad visible del avatar (que por ahora es simplemente un Rectángulo) en función de sentByMe, sólo lo mostramos si el mensaje ha sido enviado por un contacto.
  • Cambiamos el color del rectángulo en función del autor. Como no queremos mostrar un texto oscuro sobre un fondo oscuro, y viceversa, también establecemos el color del texto dependiendo de quién sea el autor. En el capítulo 5, veremos cómo el estilo se encarga de asuntos como éste por nosotros.

En la parte inferior de la pantalla, colocamos un elemento TextArea para permitir la entrada de texto multilínea, y un botón para enviar el mensaje. Usamos Pane para cubrir el área bajo estos dos elementos, de la misma manera que usamos ToolBar para evitar que el contenido del listview interfiera con la cabecera de la página:

        Pane {
            id: pane
            Layout.fillWidth: true
            Layout.fillHeight: false

            RowLayout {
                width: parent.width

                TextArea {
                    id: messageField
                    Layout.fillWidth: true
                    placeholderText: qsTr("Compose message")
                    wrapMode: TextArea.Wrap
                }

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                }
            }
        }

El TextArea debe llenar el ancho disponible de la pantalla. Asignamos un texto de marcador de posición para proporcionar una señal visual al usuario sobre dónde debe empezar a escribir. El texto dentro del área de entrada se envuelve para asegurar que no salga de la pantalla.

Por último, el botón sólo se activa cuando hay un mensaje que enviar.

Capítulo 4: Modelos

En el capítulo 4, te llevaremos a través del proceso de creación de modelos SQL tanto de sólo lectura como de lectura-escritura en C++ y su exposición a QML para rellenar vistas.

QSqlQueryModel

Para simplificar el tutorial, hemos decidido que la lista de contactos de usuario no sea editable. QSqlQueryModel es la elección lógica para este propósito, ya que proporciona un modelo de datos de sólo lectura para conjuntos de resultados SQL.

Echemos un vistazo a nuestra clase SqlContactModel que deriva de QSqlQueryModel:

#include <QQmlEngine>
#include <QSqlQueryModel>

class SqlContactModel : public QSqlQueryModel
{
    Q_OBJECT
    QML_ELEMENT

public:
    SqlContactModel(QObject *parent = nullptr);
};

No hay mucho que hacer aquí, así que pasemos al archivo .cpp:

#include "sqlcontactmodel.h"#include <QDebug>#include <QSqlError>#include <QSqlQuery>static void createTable() { if (QSqlDatabase::database().tables().contains(QStringLiteral("Contactos"))) { // La tabla ya existe; no necesitamos hacer nada. return; } QSqlQuery query; if (!query.exec( " CREATE TABLE IF NOT EXISTS 'Contacts' (" " 'nombre' TEXT NOT NULL," " PRIMARY KEY(nombre)"")" )){        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    } query.exec("INSERT INTO Contacts VALUES('Albert Einstein')"); query.exec("INSERT INTO Contacts VALUES('Ernest Hemingway')"); query.exec("INSERT INTO Contacts VALUES('Hans Gude')"); }

Incluimos el fichero de cabecera de nuestra clase y los que necesitemos de Qt. A continuación definimos una función estática llamada createTable() que usaremos para crear la tabla SQL (si no existe ya), y luego la rellenaremos con algunos contactos ficticios.

La llamada a database() puede parecer un poco confusa porque aún no hemos configurado una base de datos específica. Si no se pasa ningún nombre de conexión a esta función, devolverá una "conexión por defecto", cuya creación veremos en breve.

SqlContactModel::SqlContactModel(QObject *parent) : QSqlQueryModel(padre) { createTable();    QSqlQuery query; if (!query.exec("SELECT * FROM Contactos"))        qFatal("Contacts SELECT query failed: %s", qPrintable(query.lastError().text()));

    setQuery(std::move(query)); if (lastError().isValid())        qFatal("Cannot set query on SqlContactModel: %s", qPrintable(lastError().text()));
}

En el constructor, llamamos a createTable(). A continuación, construimos una consulta que se utilizará para rellenar el modelo. En este caso, simplemente estamos interesados en todas las filas de la tabla Contacts.

QSqlTableModel

SqlConversationModel es más complejo:

#include <QQmlEngine>
#include <QSqlTableModel>

class SqlConversationModel : public QSqlTableModel
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString recipient READ recipient WRITE setRecipient NOTIFY recipientChanged)

public:
    SqlConversationModel(QObject *parent = nullptr);

    QString recipient() const;
    void setRecipient(const QString &recipient);

    QVariant data(const QModelIndex &index, int role) const override;
    QHash<int, QByteArray> roleNames() const override;

    Q_INVOKABLE void sendMessage(const QString &recipient, const QString &message);

signals:
    void recipientChanged();

private:
    QString m_recipient;
};

Utilizamos las macros Q_PROPERTY y Q_INVOKABLE, y por lo tanto debemos hacérselo saber a moc utilizando la macro Q_OBJECT.

La propiedad recipient se establecerá desde QML para que el modelo sepa de qué conversación debe recuperar los mensajes.

Sobreescribimos las funciones data() y roleNames() para poder utilizar nuestros roles personalizados en QML.

También definimos la función sendMessage() que queremos llamar desde QML, de ahí la macro Q_INVOKABLE.

Echemos un vistazo al archivo .cpp:

#include "sqlconversationmodel.h"#include <QDateTime>#include <QDebug>#include <QSqlError>#include <QSqlRecord>#include <QSqlQuery>static const char *conversationsTableName = "Conversaciones";static void createTable() { if (QSqlDatabase::database().tables().contains(conversationsTableName)) { // La tabla ya existe; no necesitamos hacer nada. return; } QSqlQuery query; if (!query.exec( " CREATE TABLE IF NOT EXISTS 'Conversations' (""' author' TEXT NOT NULL,""' recipient' TEXT NOT NULL,""' timestamp' TEXT NOT NULL,""' message' TEXT NOT NULL,"" FOREIGN KEY('author') REFERENCES Contacts ( name ),"" FOREIGN KEY('recipient') REFERENCES Contacts ( name )" ")" )){        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    } query.exec("INSERT INTO Conversations VALUES('Yo', 'Ernest Hemingway', '2016-01-07T14:36:06', '¡Hola!')"); query.exec("INSERT INTO Conversations VALUES('Ernest Hemingway', 'Yo', '2016-01-07T14:36:16', 'Buenas tardes')"); query.exec("INSERT INTO Conversations VALUES('Yo', 'Albert Einstein', '2016-01-01T11:24:53', '¡Hola!')"); query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Yo', '2016-01-07T14:36:16', 'Buenos días.')"); query.exec("INSERT INTO Conversations VALUES('Hans Gude', 'Me', '2015-11-20T06:30:02', 'God morgen. Har du fått mitt maleri?')"); query.exec("INSERT INTO Conversations VALUES('Me', 'Hans Gude', '2015-11-20T08:21:03', 'God morgen, Hans. Ja, det er veldig fint. Gracias. " "Hvor mange timer har du brukt på den?")"); }

Esto es muy similar a sqlcontactmodel.cpp, con la excepción de que ahora estamos operando en la tabla Conversations. También definimos conversationsTableName como una variable estática constante, ya que la utilizamos en un par de lugares a lo largo del fichero.

SqlConversationModel::SqlConversationModel(QObject *parent) :
    QSqlTableModel(parent)
{
    createTable();
    setTable(conversationsTableName);
    setSort(2, Qt::DescendingOrder);
    // Ensures that the model is sorted correctly after submitting a new row.
    setEditStrategy(QSqlTableModel::OnManualSubmit);
}

Al igual que con SqlContactModel, lo primero que hacemos en el constructor es crear la tabla. Le decimos a QSqlTableModel el nombre de la tabla que usaremos a través de la función setTable(). Para asegurarnos de que los últimos mensajes de la conversación se muestran primero, ordenamos los resultados de la consulta por el campo timestamp en orden descendente. Esto va de la mano con el ajuste de la propiedad verticalLayoutDirection de ListView a ListView.BottomToTop (que vimos en el capítulo 3).

QString SqlConversationModel::recipient() const
{
    return m_recipient;
}

void SqlConversationModel::setRecipient(const QString &recipient)
{
    if (recipient == m_recipient)
        return;

    m_recipient = recipient;

    const QString filterString = QString::fromLatin1(
        "(recipient = '%1' AND author = 'Me') OR (recipient = 'Me' AND author='%1')").arg(m_recipient);
    setFilter(filterString);
    select();

    emit recipientChanged();
}

En setRecipient(), establecemos un filtro sobre los resultados devueltos por la base de datos.

QVariant SqlConversationModel::data(const QModelIndex &index, int role) const
{
    if (role < Qt::UserRole)
        return QSqlTableModel::data(index, role);

    const QSqlRecord sqlRecord = record(index.row());
    return sqlRecord.value(role - Qt::UserRole);
}

La función data() vuelve a la implementación de QSqlTableModel si el rol no es un rol de usuario personalizado. Si el rol es un rol de usuario, podemos restarle Qt::UserRole para obtener el índice de ese campo y luego usarlo para encontrar el valor que necesitamos devolver.

QHash<int, QByteArray> SqlConversationModel::roleNames() const
{
    QHash<int, QByteArray> names;
    names[Qt::UserRole] = "author";
    names[Qt::UserRole + 1] = "recipient";
    names[Qt::UserRole + 2] = "timestamp";
    names[Qt::UserRole + 3] = "message";
    return names;
}

En roleNames(), devolvemos un mapeo de nuestros valores de roles personalizados a nombres de roles. Esto nos permite utilizar estos roles en QML. Puede ser útil declarar un enum para contener todos los valores de rol, pero como no nos referimos a ningún valor específico en código fuera de esta función, no nos molestamos.

void SqlConversationModel::sendMessage(const QString &recipient, const QString &message) { const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);    QSqlRecord newRecord = record(); newRecord.setValue("author", "Me"); newRecord.setValue("recipient", recipient); newRecord.setValue("timestamp", timestamp); newRecord.setValue("message", message); if (!insertRecord(rowCount(), newRecord)) {        qWarning() << "Failed to send message:" << lastError().text();
       return; }

La función sendMessage() utiliza el recipient dado y un message para insertar un nuevo registro en la base de datos. Debido a nuestro uso de QSqlTableModel::OnManualSubmit, debemos llamar manualmente a submitAll().

Conexión a la base de datos y registro de tipos con QML

Ahora que hemos establecido las clases modelo, echemos un vistazo a main.cpp:

#include <QtCore>#include <QGuiApplication>#include <QSqlDatabase>#include <QSqlError>#include <QtQml>static void connectToDatabase() { QSqlDatabase base de datos = QSqlDatabase::database(); if (!database.isValid()) { database = QSqlDatabase::addDatabase("QSQLITE"); if (!database.isValid())            qFatal("Cannot add database: %s", qPrintable(database.lastError().text()));
    } const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (!writeDir.mkpath("."))        qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));

   // Asegúrate de que tenemos una ubicación con permisos de escritura en todos los dispositivos. const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3"; // Al utilizar el controlador SQLite, open() creará la base de datos SQLite si no existe.database.setDatabaseName(fileName); if (!database.open()) {        qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
        QFile::remove(fileName); } }int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); connectToDatabase();    QQmlApplicationEngine engine; engine.loadFromModule("chattutorial", "Main"); if (engine.rootObjects().isEmpty()) return-1; return app.exec(); }

connectToDatabase() crea la conexión a la base de datos SQLite, creando el archivo real si no existe ya.

Dentro de main(), llamamos a qmlRegisterType() para registrar nuestros modelos como tipos dentro de QML.

Usando los Modelos en QML

Ahora que tenemos los modelos disponibles como tipos QML, hay que hacer algunos cambios menores en ContactPage.qml. Para poder utilizar los tipos, primero debemos importarlos utilizando el URI que establecimos en main.cpp:

import chattutorial

A continuación, sustituimos el modelo ficticio por el adecuado:

        model: SqlContactModel {}

Dentro del delegado, utilizamos una sintaxis diferente para acceder a los datos del modelo:

            text: model.display

En ConversationPage.qml, añadimos la misma importación chattutorial, y sustituimos el modelo ficticio:

            model: SqlConversationModel {
                recipient: root.inConversationWith
            }

Dentro del modelo, establecemos la propiedad recipient al nombre del contacto para el que se está mostrando la página.

El elemento delegado raíz cambia de Fila a Columna, para acomodar la marca de tiempo que queremos mostrar debajo de cada mensaje:

            delegate: Column {
                id: conversationDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property string author
                required property string recipient
                required property date timestamp
                required property string message
                readonly property bool sentByMe: recipient !== "Me"

                Row {
                    id: messageRow
                    spacing: 6
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined

                    Image {
                        id: avatar
                        source: !conversationDelegate.sentByMe
                            ? "images/" + conversationDelegate.author.replace(" ", "_") + ".png" : ""
                    }

                    Rectangle {
                        width: Math.min(messageText.implicitWidth + 24,
                            listView.width - (!conversationDelegate.sentByMe ? avatar.width + messageRow.spacing : 0))
                        height: messageText.implicitHeight + 24
                        color: conversationDelegate.sentByMe ? "lightgrey" : "steelblue"

                        Label {
                            id: messageText
                            text: conversationDelegate.message
                            color: conversationDelegate.sentByMe ? "black" : "white"
                            anchors.fill: parent
                            anchors.margins: 12
                            wrapMode: Label.Wrap
                        }
                    }
                }

                Label {
                    id: timestampText
                    text: Qt.formatDateTime(conversationDelegate.timestamp, "d MMM hh:mm")
                    color: "lightgrey"
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined
                }
            }

Los mensajes de chat muestran marcas de tiempo debajo del texto del mensaje

Ahora que tenemos un modelo adecuado, podemos utilizar su función recipient en la expresión para la propiedad sentByMe.

El rectángulo que se utilizó para el avatar se ha convertido en una imagen. La imagen tiene su propio tamaño implícito, por lo que no necesitamos especificarlo explícitamente. Como antes, sólo mostramos el avatar cuando el autor no es el usuario, excepto que esta vez establecemos el source de la imagen a una URL vacía en lugar de usar la propiedad visible.

Queremos que el fondo de cada mensaje sea ligeramente más ancho (12 píxeles a cada lado) que su texto. Sin embargo, si es demasiado largo, queremos limitar su anchura al borde del listview, de ahí el uso de Math.min(). Cuando el mensaje no ha sido enviado por nosotros, un avatar siempre vendrá antes que él, así que lo tenemos en cuenta restando la anchura del avatar y el espacio entre filas.

Por ejemplo, en la imagen anterior, la anchura implícita del texto del mensaje es el valor más pequeño. Sin embargo, en la imagen inferior, el texto del mensaje es bastante largo, por lo que se elige el valor menor (el ancho de la vista), asegurando que el texto se detiene en el borde opuesto de la pantalla:

El texto de los mensajes largos se ajusta al ancho de la vista

Para mostrar la fecha y hora de cada mensaje que hemos comentado anteriormente, utilizamos una Etiqueta. La fecha y la hora se formatean con Qt.formatDateTime(), utilizando un formato personalizado.

El botón "enviar " debe ahora reaccionar al ser pulsado:

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                    onClicked: {
                        listView.model.sendMessage(root.inConversationWith, messageField.text)
                        messageField.text = ""
                    }
                }

Primero, llamamos a la función invocable sendMessage() del modelo, que inserta una nueva fila en la tabla de la base de datos Conversaciones. A continuación, borramos el campo de texto para dejar espacio a futuras entradas.

Capítulo 5: Estilo

Los estilos en Qt Quick Controls están diseñados para funcionar en cualquier plataforma. En este capítulo, haremos algunos ajustes visuales menores para asegurarnos de que nuestra aplicación se ve bien cuando se ejecuta con los estilos Basic, Material y Universal.

Hasta ahora, sólo hemos estado probando la aplicación con el estilo Basic. Si la ejecutamos con el estilo Material, por ejemplo, veremos inmediatamente algunos problemas. Aquí está la página de Contactos:

Página de contactos con estilo Material que muestra un contraste de texto deficiente

El texto de la cabecera es negro sobre un fondo azul oscuro, lo que resulta muy difícil de leer. Lo mismo ocurre con la página Conversaciones:

Página de conversación con estilo Material que muestra un contraste de texto deficiente

La solución es decirle a la barra de herramientas que debe utilizar el tema "Oscuro ", para que esta información se propague a sus hijos, permitiéndoles cambiar el color de su texto a algo más claro. La forma más sencilla de hacerlo es importar directamente el estilo Material y utilizar la propiedad Material attached:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

Sin embargo, esto conlleva una fuerte dependencia con el estilo Material; el plugin de estilo Material debe ser desplegado con la aplicación, incluso si el dispositivo de destino no lo utiliza, de lo contrario el motor QML fallará al encontrar la importación.

En su lugar, es mejor confiar en el soporte integrado de Qt Quick Controls para selectores de archivos basados en estilos. Para ello, debemos trasladar ToolBar a su propio archivo. Lo llamaremos ChatToolBar.qml. Esta será la versión "por defecto" del archivo, lo que significa que se utilizará cuando el estilo Básico (que es el estilo que se utiliza cuando no se especifica ninguno) esté en uso. Este es el nuevo archivo:

import QtQuick.Controls

ToolBar {
}

Como en este archivo sólo utilizamos el tipo ToolBar, sólo necesitamos la importación Qt Quick Controls. El código en sí no ha cambiado de como estaba en ContactPage.qml, que es como debería estar; para la versión por defecto del archivo, nada necesita ser diferente.

De vuelta en ContactPage.qml, actualizamos el código para utilizar el nuevo tipo:

    header: ChatToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

Ahora necesitamos añadir la versión Material de la barra de herramientas. Los selectores de archivos esperan que las variantes de un archivo estén en directorios con nombres apropiados que existan junto a la versión por defecto del archivo. Esto significa que tenemos que añadir una carpeta llamada "+Material" en el mismo directorio en el que se encuentra ChatToolBar.qml: la carpeta raíz. El "+" es necesario en QFileSelector para garantizar que la función de selección no se active accidentalmente.

Aquí está +Material/ChatToolBar.qml:

import QtQuick.Controls
import QtQuick.Controls.Material

ToolBar {
    Material.theme: Material.Dark
}

Haremos los mismos cambios en ConversationPage.qml:

    header: ChatToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

Ahora ambas páginas parecen correctas:

Página de contactos con estilo Material y contraste de cabecera corregido

Página de conversión con estilo Material y contraste de cabecera corregido

Probemos el estilo Universal:

Página de contactos con estilo universal

Página de conversación con estilo Universal

No hay problemas. Para una aplicación relativamente sencilla como ésta, deberían ser necesarios muy pocos ajustes al cambiar de estilo.

Ahora probemos el tema oscuro de cada estilo. El estilo Básico no tiene tema oscuro, ya que añadiría una ligera sobrecarga a un estilo que está diseñado para ser lo más eficiente posible. Probaremos primero el estilo Material, así que añade una entrada a qtquickcontrols2.conf que le diga que use su tema oscuro:

[Material]
Primary=Indigo
Accent=Indigo
Theme=Dark

Una vez hecho esto, compila y ejecuta la aplicación. Esto es lo que deberías ver:

Página de contactos con estilo Material en tema oscuro

Página de conversación con estilo Material en tema oscuro

Ambas páginas se ven bien. Ahora añade una entrada para el estilo Universal:

[universal]
Theme=Dark

Después de construir y ejecutar la aplicación, debería ver estos resultados:

Página de contactos con estilo Universal en tema oscuro

Página de conversación con estilo Universal en tema oscuro

Resumen

En este tutorial, te hemos guiado a través de los siguientes pasos para escribir una aplicación básica usando Qt Quick Controls:

  • Creación de un nuevo proyecto utilizando Qt Creator.
  • Configuración de un ApplicationWindow básico .
  • Definición de encabezados y pies de página con Page.
  • Visualización de contenidos en ListView.
  • Refactorización de componentes en sus propios archivos.
  • Navegación entre pantallas con StackView.
  • Utilizar diseños para permitir que una aplicación cambie de tamaño con elegancia.
  • Implementación de modelos personalizados de sólo lectura y escritura que integran una base de datos SQL en la aplicación.
  • Integración de C++ con QML a través de Q_PROPERTY, Q_INVOKABLE, y qmlRegisterType().
  • Probar y configurar múltiples estilos.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.