Partage
  • Partager sur Facebook
  • Partager sur Twitter

MVC avec Qt

Sujet résolu
    24 janvier 2019 à 9:03:26

    Hello ! :)

    Pour une nouvelle application avec Qt, je voulais essayer un nouvelle façon d'organiser mon projet. D'habitude, j'ai plutôt une classe MainWindow assez remplie et fourre tout (qui fait un peu office de main controller en fait vu que c'est un peu le point de départ de l'application) et donc qui fait le lien entre interface et "modèles".

    En voulant cette fois gérer une classe "Projet" dans ma nouvelle application, je me suis rendu compte que beaucoup de logique liée à la gestion de la création, de la sauvegarde...etc se retrouvait dans ma MainWindow (demander le nom du nouveau projet, demander s'il veut sauvegarder son projet si ça n'est pas fait lorsqu'il veut changer...).

    Et c'est là où je me suis dit que ce simple démarrage remplissait déjà bien ma main window. :D Et j'ai réalisé que ce qu'il me manquait c'est un simple project controller. J'ai donc ma classe MainWindow (équivalent application) qui contient l'ui d'un côté et mon project controller de l'autre, qui lui-même possède ma classe project. Ici l'application contient uniquement les connexions des boutons vers les slots du project controller.

    Mon problème vient de la rétro-action, comment le project controller met à jour l'interface en retour ? :-° Ca fait ajouter tout un tas de signaux dans l'autre sens. J'ai pensé à faire un signal projectModified(Project project) histoire de limiter le nombre de signaux et d'avoir un max d'infos dans ce signal mais ça ne suffit même pas (pour griser les boutons de sauvegarde par exemple).

    D'où ma question, l'architecture MVC est-elle possible avec Qt ?

    J'espère avoir été clair. :)

    Merci ! ;)

    • Partager sur Facebook
    • Partager sur Twitter
      24 janvier 2019 à 18:45:26

      Salut,

      Bien sur que l'on peut utiliser le MVC avec Qt.  Il est même particulièrement recommandé de le faire, et Qt a dés le départ été développée en ce sens ;)

      Le truc, c'est que tu dois convaincre d'une chose particulièrement importante : ton IHM n'est  jamais qu'une manière particulière de représenter les données que tu manipules!

      L'idéal est donc de commencer par développer ce que l'on appelle les "données métiers"  (le modèle). Toutes les notions qui apparaissent dans ton analyse des besoins doivent avoir un équivalent dans ton code (qui n'a -- a priori  -- absolument rien à voir avec Qt).  Je m'explique:

      Chaque nom (groupe nominal) dans ton analyse des besoins doit pouvoir être associé à une donnée ou à un type de donnée dans ton code et chaque verbe (groupe verbal) de ton analyse des besoin doit pouvoir être associé à une fonction dans ton code.

      De cette manière, tu devrais pouvoir manipuler toutes les données dont tu as besoin, de la manière requise par ton projet, et au besoin avoir un affichage console ou créer un fichier texte simple pour t'assurer que les données sont correctement manipulées.

      Une fois que ce sera fait, tu pourras injecter ces données dans un modèle de Qt (dans une des classes dérivant QAbstractItemModel, on va dire), et faire en sorte que la partie contrôleur de Qt s'adresse au modèle pour les différentes manipulation.

      Cela devrait se faire en donnant certains ordres au modèle de Qt qui se chargerait de les répercuter sur tes données métier.

      Par la suite, il ne te restera qu'à mettre en place la logique de la fonction data() du modèle, qui se chargera de créer des données manipulables par l'IHM (des  QVariant, le plus souvent).

      Au final, la classe dérivée de QAbstactItemModel que tu utilises pour un modèle particulier (tu  peux sans aucun problème créer plusieurs modèles, séparés, adaptés à différentes situations, spécifiques à certains aspects particuliers de tes données) agiront plus comme des délégués que comme des modèles, mais tout se mettra en place du fait de l'imbrication entre la (les) vue(s), les différents contrôleurs et les modèle (Qt) ;)

      • Partager sur Facebook
      • Partager sur Twitter
      Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
        25 janvier 2019 à 9:06:52

        Merci pour ta réponse. :)

        Alors pour la partie des modèles Qt (au sens classes héritant de QAbstractItemModel) je suis d'accord. Par exemple la liste des fichiers de mon projet peut-être dans un TreeModel et être affiché ensuite dans une TreeView.

        Mais est-ce que tout est modèle ? Pour les update d'UI les plus simples comme le grisage d'un bouton (comme le bouton save par exemple), est-ce que ça nécessite aussi d'être dans un modèle ? J'ai l'impression que certaines update d'UI sortent du cadre de notre modèle de données.

        Après peut-être tout simplement que je dois créer mon type particulier de modèle qui pourra contenir en plus du fileTree, le booléen saved.

        • Partager sur Facebook
        • Partager sur Twitter
          25 janvier 2019 à 17:24:51

          Maluna34 a écrit:

          Mais est-ce que tout est modèle ? Pour les update d'UI les plus simples comme le grisage d'un bouton (comme le bouton save par exemple), est-ce que ça nécessite aussi d'être dans un modèle ? J'ai l'impression que certaines update d'UI sortent du cadre de notre modèle de données.

          A partir du moment où tu décides d'utiliser un modèle, tu as sans doute intérêt à le faire pour tout.

          De plus, tu te rendras assez rapidement compte que, d'une certaine manière, il est beaucoup plus facile et logique de le faire.  Je m'explique:

          Mettons donc que tu aies un QPushButton Save dans ton interface graphique, que ce bouton doit avoir la propriété enabled à true si le fichier a été modifié depuis la dernière fois qu'il a été enregistré et à false dans le cas contraire.

          Il n'y a pas grand chose à faire : ce qui justifie le changement d'état de ce bouton, ce sont ... les actions que tu entreprends au niveau de ton modèle :

          • si tu décides de sauvegarder ton fichier, tu fais passer la valeur enabled de ton bouton false;
          • si tu décides d'ajouter un caractère à ton fichier, tu fais passer cette valeur à true.

          Tu me diras sans doute que le fait est que la savegarde en elle-même ne sera provoquée que lors du "trigger" sur le bouton en lui-même. Et que pour cela, le bouton doit être actif.

          Tu pourrais donc prévoir un slot saveFile() auquel tu connecterais le signal triggered de ton bouton sous une forme proche de

          /* Le constructeur de ta classe, pour la connexion du trigger */
          TaClasse::TaClasse(){
              /* tout ce qu'il faut avant*/
              /* la connexion qui nous intéresse */
              connect(ui->saveButton, &QPushButton::triggered, this, &TaClasse::saveFile);
          }
          void TaClasse::saveFile(){
              /* on commence par sauvegarder le fichier 
               * je te le laisse faire à ton aise 
               */
              /* Comme le fichier a été sauvegardé, le bouton doit
               * être désactié
               */
              ui->saveButton->setEnable(false); 
          }

          Mais le fait est que ton bouton devra être réactivé dés que le modèle a été modifié.  Et, pour cela, tu devras de toutes manières connecter un signal (fileModified ? ) de ton modèle sur un slot de ta vue qui se chargera de ... réactiver le bouton.  Sans doute sous une forme proche de

          * Le constructeur de ta classe, pour la connexion du trigger */
          TaClasse::TaClasse(){
              /* tout ce qu'il faut avant*/
              /* la connexion qui nous intéresse */
              connect(myModel, &MyModel::fileModified, this, &TaClasse::enableSaveButton);
          }
          void TaClasse::enableSaveButton(){
              ui->saveButton->setEnable(true); 
          }

          Toute cette approche n'est absolument pas mauvaise, car, l'un dans l'autre, tu te rends bien compte que l'on arrive à traiter l'ensemble des cas qui nous intéressent.

          Mais elle présente malgré tout un inconvénient majeur : la partie "dynamique" de ces deux logiques est clairement séparée, car elle prend place dans ta vue pour désactiver le bouton et dans le modèle pour le (ré)activer.

          Or, le fait que le bouton soit désactivé ou activé ne représente que les deux faces d'une même pièce : le bouton ne peut être désactivé que s'il est actif à la base, le bouton ne peut être activé que s'il est inactif à la base.

          Nous sommes donc face à ce que l'on pourrait appeler un "effet miroir" : l'activation du bouton n'étant que la réponse commune à la désactivation, et inversement.

          Et, du coup, on se rend compte qu'il serait "cohérent" de faire en sorte que ces deux logiques prennent leur source au même endroit.  Ne serait-ce que parce que, autrement, dans six mois ou dans un an, lorsque tu voudras vérifier ton code pour une raison ou u ne autre, tu risques de t'étonner de trouver un signal qui provoque l'activation du bouton au niveau du modèle et de ne pas trouver de signal qui en provoque ... la désactivation.

          Rien que pour cette raison, il est donc "cohérent" de se dire que les deux actions (activation et désactivation du bouton) doivent être provoquées par le modèle en lui-même ;)

          Après, on n'a que l'embarras du choix quant à la manière de s'y prendre, dans le sens où l'on peut parfaitement envisager d'avoir deux signaux bien distincts : fileHasBeenSaved() et fileNeedToBeSaved() (par exemple), dont le premier indique que le bouton doit être désactivé et que le second indique que le bouton doit être (ré)activé.

          Ou bien, on peut aussi envisager de n'avoir qu'un seul signal : savedStatusChanged(bool) (par exemple) dont la valeur émise permettra de déterminer si le bouton doit être activé ou désactivé (true : le bouton doit être activé, false, il doit être désactivé, par exemple, ou l'inverse).

          Maluna34 a écrit:

          Après peut-être tout simplement que je dois créer mon type particulier de modèle qui pourra contenir en plus du fileTree, le booléen saved.

          C'est effectivement ce que l'on s'attend à ce que tu fasse dans la plupart des cas.

          Dis toi bien que toutes les classes fournies par Qt dont le nom termine par Model se limitent, à la base, à fournir les comportements minimum essentiels et cohérents que l'on attend de leur part, mais que l'on s'attend forcément à ce que ces comportements ne soient pas "suffisamment précis" pour les besoins auxquels tu fais face.

          Dans le meilleur des cas, les comportements proposés seront considérés plus comme "une bonne base de travail" que comme "quelque chose de parfaitement fini"

          C'est un peu comme si tu achetais une pâte à tarte au magasin : la pâte est prête, et tu peux l'utiliser telle quelle.  Mais, ce qui fera le succès de la tarte, c'est ce que tu mettra dessus ;)

          • Partager sur Facebook
          • Partager sur Twitter
          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
            26 janvier 2019 à 14:50:43

            Toujours épaté par la qualité et surtout la longueur des réponses que tu prends le temps de faire. :D

            Merci à toi, c'est plus clair maintenant. ;)

            • Partager sur Facebook
            • Partager sur Twitter

            MVC avec Qt

            × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
            × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
            • Editeur
            • Markdown