mercredi 30 novembre 2011

Changer l'orientation de l'écran "Portrait" / "Paysage" en QML

Aujourd'hui je vais vous montrer comment forcer un smartphone à basculer d'un mode d'orientation à un autre. Cela est très utile par exemple pour afficher un lecteur video en pleine page en mode paysage et revenir en mode portrait lorsque la vidéo est terminée.

Dans la mesure où il n'existe pas à l'heure actuelle d'éléments QML permettant de basculer d'un mode d'orientation à un autre, je propose d'exploiter le code Qt existant généré par le Wizard Qt Quick. En effet vous avez sans doute remarqué que le wizard générait une classe Qt dénommée QmlApplicationViewer. Cette classe dispose d'une méthode appropriée "setOrientation" dont l'implémentation consiste à jouer sur les attributs du QWidget pour changer l'orientation.

C'est exactement ce dont nous avons besoin : il faut donc rendre cette méthode "appelable" ("invokable" en anglais) depuis QML ; ensuite nous aurons qu'à déclarer une instance d'objet QmlApplicationViewer dans le contexte déclaratif QML.

Commençons par éditer le fichier qmlapplicationviewer.h afin de modifier la signature de la méthode "setOrientation" :

class QmlApplicationViewer : public QDeclarativeView
{
    Q_OBJECT

 
       Q_ENUMS(ScreenOrientation)


public:
    enum ScreenOrientation {
        ScreenOrientationLockPortrait,
        ScreenOrientationLockLandscape,
        ScreenOrientationAuto
    };

    explicit QmlApplicationViewer(QWidget *parent = 0);
    virtual ~QmlApplicationViewer();

    static QmlApplicationViewer *create();

    void setMainQmlFile(const QString &file);
    void addImportPath(const QString &path);

    
        // Note that this will only have an effect on Symbian and Fremantle.
    Q_INVOKABLE void setOrientation(ScreenOrientation orientation);


    void showExpanded();

private:
    explicit QmlApplicationViewer(QDeclarativeView *view, QWidget *parent);
    class QmlApplicationViewerPrivate *d;
};

QApplication *createApplication(int &argc, char **argv);

Vous remarquerez églalement que j'ai déclaré le type énuméré "ScreenOrientation"  comme exportable via le mot clef Q_ENUMS. Cela permettra d'utiliser cette enumération depuis QML comme paramètre lors de l'invocation de la méthode setOrientation.

Ensuite il faut éditer le contenu de la fonction main() afin :
  • de déclarer l'objet QmlViewerApplication dans le contexte QML
  • de déclarer le type énuméré "ScreenOrientation"
// Instantiation de l'objet QmlApplicationViewer
QmlApplicationViewer viewer;

viewer.setOrientation(QmlApplicationViewer::ScreenOrientationLockPortrait);


// Récupération du contexte déclaratif QML
QDeclarativeContext* context = viewer.rootContext();

// Exposition de l'objet QmlApplicationViewer afin de le rendre accessible en QML 
context->setContextProperty("appviewer", &viewer);

// Enregistrement du type QmlApplicationViewer afin d'utiliser ce type en QML

qmlRegisterType<QmlApplicationViewer>("AppViewer", 1, 0, "QmlApplicationViewer");


Et c'est tout pour la partie Qt C++. Maintenant il s'agit d'utiliser la méthode QmlApplicationViewer::setOrientation depuis QML. J'ai donc créé une interface QML toute simple comme l'atteste ces deux captures d'écran :


Mode portrait

Mode paysage

Vous avez sans doute compris le fonctionnement : le bouton permet de changer le mode d'orientation. Pour cela j'ai défini un type QML "Bouton" comme suit :

import QtQuick 1.0


Rectangle {

    property alias text: messageText.text
    property alias posY: textBox.y

    signal pressed
    property color onButtonPressed: "grey"
    property color onButtonReleased: "lightgrey"

    id: textBox
    color: "lightgrey"
    width: messageText.width+20; height: messageText.height+10
    border.width: 1
    border.color: "black"
    radius: 10
    smooth: true
    anchors.horizontalCenter: parent.horizontalCenter



    Text {
        id: messageText
        text: "Default"
        anchors.centerIn: parent
        font.pointSize: 12; font.bold: true; font.family: "Helvetica"
    }



    MouseArea {
        anchors.fill: parent
        onPressed: parent.color = parent.onButtonPressed
        onReleased: parent.color = parent.onButtonReleased
        onClicked: textBox.pressed()
    }
 }

Et j'ai créé une page principale utilisant ce bouton :

import QtQuick 1.0
import AppViewer 1.0


Rectangle {
    width: 360
    height: 640

    gradient:
        Gradient {
            GradientStop {
                position: 0
                color: "#ccc094"
            }

            GradientStop {
                position: 0.8
                color: "#777057"
            }

            GradientStop {
                position: 1
                color: "#000000"
            }
        }

    Button
    {
        id: myButton
        text: ""
        posY:100

        onPressed: {
            if(state == "portrait")
            {
                appviewer.setOrientation(QmlApplicationViewer.ScreenOrientationLockLandscape)
                state = "landscape"
            }
            else if(state == "landscape")
            {
                appviewer.setOrientation(QmlApplicationViewer.ScreenOrientationLockPortrait)
                state = "portrait"
            }
        }

        state: "portrait"
        states:[
            State{
                name: "portrait"
                PropertyChanges {
                    target: myButton
                    text: "Switch to landscape"
                }
            },
            State{
                name: "landscape"
                PropertyChanges {
                    target: myButton
                    text: "Switch to portrait"
                }
            }
        ]
    }
}

Les étapes importantes du listing ci-dessus sont les suivantes :
  • Importer le module "AppViewer" afin de pouvoir accéder à l'objet du même nom
  • Déclaration d'un bouton "Button" avec deux états : l'un gérant l'affichage en mode portrait, l'autre gérant l'affichage en mode paysage
  • Appeler la méthode appviewer.setOrientation lorsque l'utilisateur clique sur le bouton avec un paramètre dont la valeur dépend de l'état courant.
Et voilà. Le code source est diponible ici

vendredi 25 novembre 2011

Coverflow QML 2D

Un caroussel avec de la profondeur obéissant au doigt et à l'oeil procure une expérience utilisateur agréable. Avec QML il est relativement facile d'implémenter un caroussel d'images.



Côté réalisation, c'est très accessible :

  • Créer un projet Qt Quick
  • Importer dans votre projet un jeu d'images
  • Dimensionner votre rectangle de base avec un fond noir :
Rectangle {
    width: 360
    height: 640
    color: "black" 
  • A l"intérieur de ce dernir, créer votre nouvel élément visuel (ou Item) que j'ai identifié comme suit :
Item {
   id: coverFlow
   anchors.fill: parent 
  • Ajouter lui un model de données (dans mon cas j'ai choisi un modèle statique mais il est tout à fait possible d'utiliser un modèle dynamique)
ListModel {
id: appModel
ListElement { name:"Photo 0"; description:"Ceci est la photo n°0"; cover: "0.jpg"}
ListElement { name:"Photo 1"; description:"Ceci est la photo n°1"; cover: "1.jpg"}
ListElement { name:"Photo 2"; description:"Ceci est la photo n°2"; cover: "2.jpg"}
ListElement { name:"Photo 3"; description:"Ceci est la photo n°3"; cover: "3.jpg"}
ListElement { name:"Photo 4"; description:"Ceci est la photo n°4"; cover: "4.jpg"}
ListElement { name:"Photo 5"; description:"Ceci est la photo n°5"; cover: "5.jpg"}
ListElement { name:"Photo 6"; description:"Ceci est la photo n°6"; cover: "6.jpg"}
ListElement { name:"Photo 7"; description:"Ceci est la photo n°7"; cover: "7.jpg"}
ListElement { name:"Photo 8"; description:"Ceci est la photo n°8"; cover: "8.jpg"}
ListElement { name:"Photo 9"; description:"Ceci est la photo n°9"; cover: "9.jpg"}
ListElement { name:"Photo 10"; description:"Ceci est la photo n°10"; cover:"10.jpg"}
}

  • Ensuite il faut créer l'élément clef du caroussel :  "PathView"
PathView{
   id:view
   width: 360
   height: 360
   anchors.horizontalCenter: parent.horizontalCenter
   anchors.verticalCenter: parent.verticalCenter
   model:appModel
   delegate:appDelegate
   path: Path{
       startX: 0
       startY: 180
       PathAttribute { name: "coverScale"; value: 0.5 }
       PathAttribute { name: "z"; value: 0 }
       PathAttribute { name: "coverOpacity"; value:  0.1 }
       PathLine{x:180 ;y: 180}
       PathAttribute { name: "coverScale"; value: 1 }
       PathAttribute { name: "z"; value: 100 }
       PathAttribute { name: "coverOpacity"; value: 1 }
       PathLine{x:380;y: 180}
       PathAttribute { name: "coverScale"; value: 0.5 }
       PathAttribute { name: "z"; value: 0 }
       PathAttribute { name: "coverOpacity"; value:  0.1 }
   }
}

Donc je définis une zone de 360px par 360px  dont les caractéristiques sont les suivantes :
  1. Le modèle associé est ma liste déléments précédemment définie : ce sont ces éléments qui vont être affichés sur le "path"
  2. Un délégué en charge de dessiner chaque élément de mon modèle
  3. Un chemin ("path") rectiligne le long de l'axe des Y avec une variation de taille d'image (facteur "coverScale"), une variation de la profondeur (facteur "z") et une variation de la transparence d'image (facteur "coverOpacity). Le chemin est construit de telle manière à mettre en avant l'image courante au centre et à attenuer les images adjacentes. Plus on s'écarte du centre et plus l'opacitié et la réduction d'image augmente

Le délégué en charge de dessiner l'image est basique :

Component{
   id: appDelegate

   Item {
      z:PathView.z
      scale: PathView.coverScale
      height: imageCover.height
      width: imageCover.width
      opacity: PathView.coverOpacity
      Image {
          id: imageCover
          smooth: true
          width: 225
          height: 225
          source: cover
      }
  }
}

Chaque image possède des propriétés "bindées" aux attributs variables du path : à savoir la profondeur (z), la taille (scale) et la transparence (opacity). Par ailleurs la source de l'image pointe sur le champ "cover" (c'est à dire le fichier image) défini dans le modèle.

Dernier point l'affichage du titre et de la description de l'image :
// Title of current cover
Text {
    id:title
    text: appModel.get(view.currentIndex).name
      color: "white"
    font.bold: true
    font.pointSize: 14
    anchors.horizontalCenter: parent.horizontalCenter
    anchors.top: view.bottom
}

// Description of current cover
Text {
    id:description
    text: appModel.get(view.currentIndex).description
        color: "white"
    font.pointSize: 10
    anchors.horizontalCenter: parent.horizontalCenter
    anchors.top: title.bottom
}

A noter le binding des propriétés "text" avec l'élément courant du modèle.

Alors il n'est pas beau mon coverflow ;-)

Le code source complet est diponible ici

Ouvrir le client mail natif de Symbian en QML

Plutôt que de redévelopper un écran pour l'envoi d'un email, souhaiteriez-vous utiliser l'application native Symbian de client mail ?

Rien de plus simple ...

Créer un projet Qt Quick de base et modifier le fichier "main.qml" comme suit :

import QtQuick 1.0
Rectangle {
    width: 360
    height: 360
    Text {
        text: "Cliquer pour ouvrir le client mail natif"
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.openUrlExternally("mailto:alain.dupont@gmail.com?subject=Mes préférences&body=à voir demain.");
        }
    }
}

Et voici le rendu en deux temps :

  • Lorsqu'on clique sur l'écran, la boite de dialogue native suivante s'ouvre afin de demander à l'utilisateur de sélectionner son compte mail à utiliser pour envoyer le message.



  • Une fois le compte séléctionné, l'application mail native s'ouvre :

Comme vous pouvez le constater, les contenus des champs "mailto", "subject" et "body" utilisés via l'appel à Qt.openUrlExternally() sont directement exploités par l'application native.

Remarque : sur les versions Symbian^3 antérieures à la version Anna, les caractères accentués ne sont pas gérés. Un workaround existe en Qt C++ :


void EmailHelper::sendEmail(const QString & rTo, const QString & rSubject, const QString & rBody)
{
    QString urlString = "mailto:";
    urlString += rTo;
    urlString += "?subject=";
    urlString += rSubject;
    urlString += "&body=";
    urlString += rBody;
    QUrl url(urlString);
    QString encodedUrlString = QString::fromAscii(url.toEncoded());
    QDesktopServices::openUrl(QUrl(encodedUrlString));
}

La méthode QString::fromAscii permet de convertir la chaîne de caractère au format Unicode.

vendredi 18 novembre 2011

Créer un client mail sur Symbian

Je vous proprose de créer une petite application Qt permettant d'envoyer des emails depuis son téléphone Symbian. Voici le rendu final :


N'étant pas infographiste, soyez indulgent pour la qualité visuelle. Avant de me lancer dans des explications de code, je tiens à préciser plusieurs choses :
  1. J'ai utilisé la dernière version du sdk Qt, à savoir la version 1.1.4, avec certains des composants Qt Quick pour Symbian.
  2. J'ai validé cette application sur un Nokia E7 upgradé avec Symbian Anna. La config de compilation Qt utilisée est donc "Qt 4.7.4 pour Symbian Anna".
  3. L'application ne permet pas d'envoyer un email avec le simulateur
  4. Il vous faut créer au moins un compte email sur votre téléphone Symbian
  5. J'ai fait le choix de gérer la partie purement graphique en QML et la partie logique métier en C++
Partie C++

J'ai créé une classe "helper" dénommée "EmailHelper" comme suit :

class EmailHelper : public QObject
{
    Q_OBJECT
public:
    explicit EmailHelper(QObject *parent = 0);
    ~EmailHelper();
    Q_INVOKABLE bool sendEmail(QString accountName, QString recipient, QString subject, QString body);
    Q_INVOKABLE QStringList getEmailAccountList();
signals:
    void stateMsg(const QString &statemsg);
    void errorMsg(const QString &errormsg);
private slots:
    void messageStateChanged(QMessageService::State s);
    
private:
    QMessageService iMessageService;
    QMessageManager iManager;
    QMessageService::State state;
    QMap<QString, QMessageAccountId> accountList;
};

Comme vous le constatez cette classe me permet de récupérer la liste des comptes email configurés sur le téléphone (méthode getEmailAccountList) et d'envoyer un email depuis un compte donné (méthode sendEmail). Notez la signature particulière de ces deux méthodes : j'ai utilisé la macro "Q_INVOKABLE" afin de les rendre accessible depuis le contexte d'execution déclaratif QML. Enfin pour gérer l'envoi d'email, je m'appuie sur le module "Messaging" de Qt Mobility via l'usage des deux classes QMessageService et QMessageManager.

Coté implémentation, rien de bien compliqué :

  • Un constructeur où je m'abonne au signal "stateChanged" émis par le service de messagerie:
EmailHelper::EmailHelper(QObject *parent) :
    QObject(parent)
{
    state = QMessageService::InactiveState;
    connect(&iMessageService, SIGNAL(stateChanged(QMessageService::State)), this, SLOT(messageStateChanged(QMessageService::State)));
}
  • La méthode de récupération des comptes emails :
QStringList EmailHelper::getEmailAccountList()
{
    QStringList accountNameList;

#ifdef SENDEMAIL_ENABLED
    // Récupération de la liste des comptes email et ajout dans la liste
    foreach (const QMessageAccountId &id, iManager.queryAccounts())
    {
        QMessageAccount account(id);
        if (account.messageTypes() & QMessage::Email)
        {
            QString name(account.name());
            accountList.insert(name, account.id());
            accountNameList.append(name);
        }
    }
#endif
    return accountNameList;
}

Note : le service de messagerie étant commun aux SMS, MMS et Email, je mets un filtre en place sur les emails. Globalement un compte email est composé d'un libellé et d'un identifiant ; je stocke ces informations dans une propriété de type QMap afin de ré-utiliser la correspondance Libellé/Identifiant utlérieurement pour l'envoi d'un email. Et je retourne uniquement la liste de libellés de comptes à destination de l'affichage géré en QML

  • La méthode d'envoi d'un email : 

bool EmailHelper::sendEmail(QString accountName, QString recipient, QString subject, QString body)
{
#ifdef SENDEMAIL_ENABLED
    if (!QMessageAccount::defaultAccount(QMessage::Email).isValid())
    {
        emit errorMsg("Aucun compte configuré pour envoyer un email.");
        return false;
    }
    if (state == QMessageService::InactiveState || state == QMessageService::FinishedState)
    {
        QMessage message;
        QMessageAccountId accountId = accountList[accountName];
        message.setType(QMessage::Email);
        message.setParentAccountId(accountId);
 
        // Email de destination
        QMessageAddress::Type addrType(QMessageAddress::Email);
        QMessageAddressList toList;
        toList.append(QMessageAddress(addrType, recipient));
        message.setTo(toList);
 
        // Objet du mail
        if (subject.isEmpty()) {
            emit errorMsg("Aucun objet spécifié");
            return false;
        }
        else
            message.setSubject(subject);
 
        // Corps du mail
        if (body.isEmpty()) {
            emit errorMsg("Aucun contenu de message spécifié");
            return false;
        }
        else
            message.setBody(body);
 
        // Envoi de l'email
        bool result = iMessageService.send(message);
        return result;
    }
    else
    {
        return false;
    }
}

Cette méthode initialise un objet QMessage à partir du compte email séléctionné par l'utilisateur avec :
1. Une adresse de destination
2. Un objet
3. Un corps de mail

Note : pour aller plus loin dans la prise en main de l'api de messaging, je vous invite à ajouter une pièce jointe.

Afin de rendre ma classe visible et instantiable dans le contexte déclaratif QML, j'ai ajouté la ligne suivante dans la fonction "main" du programme :

qmlRegisterType<EmailHelper>("EmailHelper",1,0,"EmailHelper");

Ainsi, comme nous le verrons plutard, je pourrai instantier mon objet depuis QML via une simple déclaration comme cela :

EmailHelper {
}
  • Pour finir j'ai implémenté le slot privé "messageStateChange" dont le rôle est de relayer le signal public "stateMsg" à la partie QML afin d'en informer de manière asynchrone du statut de l'envoi d'email :
void EmailHelper::messageStateChanged(QMessageService::State s){
       state = s;
    if (s == QMessageService::InactiveState)
    {
        emit stateMsg("InactiveState");
    }
    else if (s == QMessageService::ActiveState)
    {
        emit stateMsg("ActiveState");
    }
    else if (s == QMessageService::CanceledState)
    {
        emit stateMsg("CanceledState");
    }
    else if (s == QMessageService::FinishedState)
    {
        emit stateMsg("FinishedState");
    }
    else
    {
        emit stateMsg(QString("QMessageService::%1").arg(s));
    }
}

 
Et voilà pour la partie C++

Partie Environnement :

Afin de pouvoir compiler et envoyer un email de manière effective, il est INDISPENSABLE de modifier son fichier projet (*.pro) comme suit :
  • Ajouter les deux lignes suivantes pour l'utilisation du module "Messaging" de Qt Mobility :
CONFIG += mobility
MOBILITY += messaging
  • Ajouter les permissions suivantes (notamment pour accéder aux comptes email de l'utilisateur) :
symbian:TARGET.CAPABILITY += NetworkServices \
                             LocalServices \
                             ReadUserData \
                             WriteUserData \
                             UserEnvironment \
                             ReadDeviceData \
                             WriteDeviceData
  • Ajouter la dépendance aux composants Qt Quick
CONFIG += qt-components

Dernier point : il faut que l'application soit signée avec un certificat "Express Signed" ; le mode "auto-signé" ne fonctionne pas car nous avons besoin de la permission "ReadDeviceData". Donc soit vous utiliserez un certificat fourni par PublishToOVI via votre compte Nokia Publisher, soit vous utiliserez un certificat SymbianSign.

Partie UI en QML :

Ayant utilisé le template de projet Qt Quick pour Symbian avec utilisation des composants Qt Quick, je me retrouve donc avec deux fichiers :

1. main.qml
2. MainPage.qml

J'ai mis tout le code dans le fichier MainPage.qml ; pour un vrai projet il est recommendé de modulariser ses composants via une arborescence de fichiers bien organisés.

Commençons par les inclusions de module :
  • import com.nokia.symbian 1.1 : pour utiliser les composants Qt Quick
  • import Qt.labs.components 1.1 : pour utiliser le regroupement exclusif des boutons radios
  • import EmailHelper 1.0 : notre classe "helper" pour envoyer des emails
Ensuite j'ai crée un petit fond à base de gradients de couleur :

Rectangle {
    id: rootWindow
    anchors.fill: parent
    width: 360
    height: 640

    gradient: Gradient {
        GradientStop {
             position: 0
             color: "#ccc094"
        }
        GradientStop {
             position: 0.8
             color: "#777057"
        }
        GradientStop {
             position: 1
             color: "#000000"
        }
    }

Je définis ensuite une petite boîte de dialogue servant à afficher les messages d'erreur et de succés :

CommonDialog {
    id: dialog
    titleText: "Information"
    buttonTexts: ["OK"]
    content: Rectangle {
                 width: 300
                 height: 200
                 anchors.horizontalCenter: parent.horizontalCenter
                 color: "transparent"
                 Text {
                    id: dialogMessage
                    anchors.centerIn: parent
                    textFormat: Text.StyledText
                    width:290
                    text:""
                    wrapMode: Text.Wrap
                    color: "white"
                 }
             }
    onButtonClicked: {
        if(mainPage.quitRequested == true) Qt.quit()
    }
}

Puis j'instancie mon élément "EmailHelper" :

EmailHelper {
    id: helper
    property int accountNumber;
    property variant accountNameList: []
    onStateMsg: {
        console.log(statemsg)
        if(statemsg == "FinishedState")
        {
            indicator.visible = false
            indicator.running = false
            dialogMessage.text = qsTr("Email envoyé avec succès")
            dialog.open()
        }
    }
    onErrorMsg: {
        console.log(errormsg)
        indicator.visible = false
        indicator.running = false
        dialogMessage.text = errormsg
        dialog.open()
    }
}

Comme vous pouvez le constater j'y défini les slots pour capturer les signaux "errorMsg" et "stateMsg" ; j'y rattache également la liste des noms de comptes emails via la variable "accountNameList" qui sera initialisée au chargement de la page QML comme le montre le code suivant :

Component.onCompleted: {
    console.log("Component.onCompleted")
    helper.accountNameList = helper.getEmailAccountList()
    helper.accountNumber = helper.accountNameList.length
    if( helper.accountNumber == 0)
    {
        mainPage.quitRequested = true
        dialogMessage.text = qsTr("Désolé vous n'avez aucun compte mail configuré sur ce téléphone")
        dialog.open()
    }
    else console.log("Vous avez "+ helper.accountNumber +" comptes");

    for(var i=0;i<helper.accountNumber;i++){
        console.log("Compte "+i+" : "+helper.accountNameList[i])
    }
 }

Par conséquent, la liste des comptes mails est récupérée au chargement de la page ; si cette liste est vide ou indisponible, j'affiche un popup invitant l'utilisateur à créer un compte email sur son téléphone.

Enfin j'affiche le formulaire de saisie permettant de contruire l'email à envoyer ; ce formulaire possède un header avec un bouton d'envoi :

// Fixed Header
Item {
    id: header
    width: rootWindow.width
    height: 60
    z:3
    Rectangle {
        color: "black"
        anchors.fill: parent
        BusyIndicator {
            id: indicator
            anchors.leftMargin: 5
            running: false
            visible: false
        }
        Text {
            id: title
            text: qsTr("Custom Mail Client")
            color: "white"
            anchors.centerIn: parent
            font.bold: true
            font.pixelSize:20
        }
        Button {
            id: send
            iconSource: "send.png"
            x:rootWindow.width-send.width
            anchors.rightMargin: 5
            width:header.height-5
            height:header.height-5
            onClicked: {
                if(mailAccount.account.length>0     &&
                   mailRecipient.recipient.length>0 &&
                   mailSubject.subject.length>0     &&
                   mailBody.body.length>0)
                {
                    var requestResult = helper.sendEmail(mailAccount.account, mailRecipient.recipient, mailSubject.subject, mailBody.body);
                    if(requestResult)
                    {
                        indicator.visible = true
                        indicator.running = true
                    }
                    else
                    {
                        dialogMessage.text = qsTr("Echec de la demande d'envoi d'email !");
                        dialog.open()
                    }
                }
                else if(mailRecipient.recipient.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez saisir une addresse de destination")
                    dialog.open()
                }
                else if(mailSubject.subject.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez renseigner un objet")
                    dialog.open()
                }
                else if(mailBody.body.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez renseigner le corps du mail")
                    dialog.open()
                }
            }
        }
    }
}

La partie formulaire sera positionnée dans une zone scrollable (Flickable) :

Flickable {
   id:flickable
   width: parent.width
   height:parent.height-header.height
   anchors.top: header.bottom
   contentWidth: flickable.width;
   contentHeight: mailAccount.height+flickable.height*3/2
   flickableDirection: Flickable.VerticalFlick

 
La partie bouton radio du formulaire pour sélectionner le compte email :

// Sélection du compte email
Item {
    property string account: helper.accountNameList[0]
    id: mailAccount
    width: rootWindow.width-20
    height: helper.accountNumber*50
    anchors.top: parent.top
    anchors.topMargin: 10
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: mailAccountLabel
        text: qsTr("Compte Mail :")
        font.bold: true
        font.pixelSize:20
    }
    CheckableGroup { id: group }
        Column {
            id: column
            anchors.top: mailAccountLabel.bottom
            anchors.topMargin: 5
            spacing: platformStyle.paddingMedium
            Repeater {
                model: helper.accountNumber
                RadioButton {
                     text: helper.accountNameList[index]
                     platformExclusiveGroup: group
                         onClicked: {
                             mailAccount.account = text
                         }
                     }
                 }
            }
        }

Ensuite il s'agit d'ajouter les zones de saisies et le tour est joué. Une fois les données saisies, l'utilisateur n'a plus qu'à valider le bouton situé dans le header du formulaire et l'email est envoyé.

// Destinataire du message
Item {
    property alias recipient: recipientRect.text
    id: mailRecipient
    width: rootWindow.width-20
    height: 50
    anchors.top: mailAccount.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: mailLabel
        text: qsTr("Email de destination :")
        font.bold: true
        font.pixelSize:20
    }
    TextField {
        id: recipientRect
        anchors.top: mailLabel.bottom
        anchors.topMargin: 5
        width: parent.width
    }
}
 
// Objet du mail
Item {
    property alias subject: subjectRect.text
    id: mailSubject
    width: rootWindow.width-20
    height: 50
    anchors.top: mailRecipient.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: subjectLabel
        text: qsTr("Objet :")
        font.bold: true
        font.pixelSize: 20
    }
    TextField {
        id: subjectRect
        anchors.top: subjectLabel.bottom
        anchors.topMargin: 5
        width: parent.width
    }
}
 
// Corps du mail
Item {
    property alias body: bodyInput.text
    id: mailBody
    width: rootWindow.width-20
    height: 150
    anchors.top: mailSubject.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: bodyLabel
        text: qsTr("Corps du message :")
        font.bold: true
        font.pixelSize: 20
    }
    TextArea {
        id: bodyInput
        anchors.top: bodyLabel.bottom
        anchors.topMargin: 5
        width: parent.width
        height : 120
   }
}
 
Le code source complet du projet est téléchargeable ici 
Bonne lecture.