IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Integrer Ogre à Qt - Partie 2


précédentsommairesuivant

II. Support de l'annulation

II-1. Présentation du framework Undo fourni par Qt

La possibilité d'annuler et réappliquer des modifications dans un document est devenue extrêmement courant. C'est en effet une fonctionnalité essentielle pour faciliter l'utilisation de votre logiciel. Les éditeurs 3D ou encore de niveaux n'échappent pas à la règle, nous allons donc voir comment implémenter ce système avec Qt.
Le framework Undo de Qt est basé sur le design pattern "Command". Je vous encourage à lire la définition écrite par Baptiste Wicht sur ce même site. Pour une description plus détaillée avec un exemple complet, vous pouvez vous référer à cette page.
L'idée est donc de wrapper nos actions sur la scène dans des objets, des instances d'une classe dérivant de QUndoCommand. Lors de toute modification de la scène, un tel objet sera donc créé de façon à définir comment annuler la modification, ainsi que la façon de l'appliquer. Chacun de ces objets sera poussé dans une QUndoStack. Cette dernière est en générale propre à chaque document. La dernière classe que nous utiliserons ici sera QUndoView. Elle permet d'afficher le contenu d'une QUndoStack, ainsi que d'y naviguer (c'est-à-dire cliquer sur un état pour voir le document tel qu'il l'était lors de ce snapshot).
Dans le cas d'une application permettant de gérer plusieurs documents, l'utilisation de QUndoGroup est recommandée. L'expression "plusieurs documents" est à prendre au sens général du terme. Prenons un éditeur 3D: vous pouvez vouloir gérer une pile d'undo pour les modifications de la scène, ainsi qu'une autre indépendante pour gérer un panel de matériau. Cette classe ne sera pas utilisée ici.

II-2. Implémentation

II-2-1. Préparatif

Avant d'entrer dans les détails de chaque modification, nous allons mettre en place le nécessaire pour supporter l'undo/redo.
Voici les éléments que nous voulons présenter:

  • ajout au menu "Divers" d'une action "Annuler" suivi d'une description de l'action,
  • ajout au menu "Divers" d'une action "Refaire" suivi d'une description de l'action,
  • annulation du changement de couleur de fond ainsi que du déplacement d'une entité,
  • possibilité de fusionner des déplacements de l'entité si le temps écoulé entre 2 commandes est de moins de 5 dixièmes de seconde (afin de prendre en compte le déplacement constant à la souris, au clavier ou aux spinners) et que l'entité est la même que la précédente,
  • possibilité d'afficher la pile des modifications.

La première étape consiste à créer une QUndoStack. Notre petit "éditeur" étant mono-document, nous allons la gérer à partir de la classe MainWindow. Une fois doté d'une instance de cette classe, nous allons ajouter les entrées de menus permettant de faire et défaire des actions:

 
Sélectionnez
QAction *undoAct = undoStack.createUndoAction(this, "Annuler : ");
undoAct->setShortcut(QKeySequence("Ctrl+Z"));

QAction *redoAct = undoStack.createRedoAction(this, "Refaire : ");
redoAct->setShortcut(QKeySequence("Ctrl+Shift+Z"));

Ces actions seront automatiquement mises à jour par QUndoStack lors de l'insertion ou du retrait d'éléments.
Nous allons maintenant ajouter "l'inspecteur", ce petit widget permettra d'avoir une vue sur l'évolution de la pile d'annulation. Il sera présenté dans un QDockWidget dont l'affichage sera pilotable par une entrée dans le menu "Divers". Voici le code nécessaire à la création de ce dock:

 
Sélectionnez
// Constructeur de MainWindow:
    undoView = new QUndoView(&undoStack, this);
    undoView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    undoView->setEmptyLabel("<Scene initiale>");
    undoView->setMinimumWidth(150);
    undoView->setMaximumWidth(150);
    
// [...]

// Ajout à MainWindow::createActionMenus():
QAction *undoHistoryAct = undoViewDock->toggleViewAction();
menu->addAction(undoHistoryAct);
    
// [...]

// Ajout à MainWindow::createDockWidget():
    undoViewDock = new QDockWidget(this);
    undoViewDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
    undoViewDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
    undoViewDock->setWidget(undoView);
    undoViewDock->setWindowTitle("Historique");
    undoViewDock->setVisible(true);
    addDockWidget(Qt::LeftDockWidgetArea, undoViewDock);

Prenez bien garde à appeler createDockWidget avant createActionMenus si vous vous servez du QAction fourni par QDockWidget pour piloter son affichage.
Il nous reste une dernière chose à faire avant de passer à l'implémentation des commandes que nous avons définies précédemment. Afin de pouvoir fusionner des commandes, nous allons avoir besoin d'un identifiant. Pour ce faire, j'ai choisi ici de les (enfin, "le" dans le cadre de ce tuto) stocker dans un en-tête sous la forme d'un enum:

 
Sélectionnez
#ifndef COMMANDS_H
#define COMMANDS_H

enum CommandID
{
    CID_MoveEntity
};

#endif

Et nous sommes enfin prêt à implémenter nos commandes !

II-2-2. Annulation du changement de couleur de fond

Les classes dérivant de QUndoCommand sont en général très simples puisqu'elles ne stockent que le nécessaire pour résoudre un changement d'état, voire des informations liées à la fusion des commandes de même type. Voici l'interface utilisée pour la commande de changement de couleur de fond:

 
Sélectionnez
class ChangeBackgroundColor : public QUndoCommand
{
public:
    ChangeBackgroundColor(OgreWidget *ogreWidget, const QColor &previousColor, const QColor &nextColor);

    virtual void undo();
    virtual void redo();
	
private:
    OgreWidget  * target;
    QColor        prev;
    QColor        next;
};

La méthode id() nous servira afin de déterminer si 2 commandes peuvent être fusionnées. Dans ce contexte, elle renvoie simplement CID_ChangeBGColor. Les méthodes undo() et redo() sont celles permettant à la magie d'opérer. Voici le corps d'undo(), à titre d'exemple (redo() étant similaire à l'exception de la couleur appliquée):

 
Sélectionnez
void ChangeBackgroundColor::undo()
{
    target->setBackgroundColor(prev);
    target->update();
}

Afin de supporter l'annulation du changement de couleur, il nous faut une méthode pour obtenir la couleur actuellement utilisée. Nous ajoutons donc une méthode getBackgroundColor() à OgreWidget qui se chargera de nous retourner un QColor:

 
Sélectionnez
QColor OgreWidget::getBackgroundColor() const
{
    if(ogreViewport)
    {
        Ogre::ColourValue bgColour = ogreViewport->getBackgroundColour();
        return QColor::fromRgbF(bgColour.r, bgColour.g, bgColour.b);
    }
    
    return QColor();
}

La dernière étape avant de pouvoir naviguer à travers l'historique de la scène est de créer la commande puis l'ajouter à notre QUndoStack. Le meilleur endroit pour le faire est MainWindow::chooseBgColor(), qui devient donc:

 
Sélectionnez
void MainWindow::chooseBgColor()
{
    QColor nextCol	= QColorDialog::getColor();
    QColor prevCol	= ogreWidget->getBackgroundColor();
    if (!nextCol.isValid())
        return;
    
    ChangeBackgroundColor *changeBgCommand = new ChangeBackgroundColor(ogreWidget, prevCol, nextCol);
    undoStack.push(changeBgCommand);
}

Il est très important de noter que QUndoStack::push(QUndoCommand*) va exécuter l'action dès le push. Il est dès lors inutile d'appeler OgreWidget::setBackgroundColor() par nous même. De même, attention aux récursions infinies si la méthode qui instancie la commande est celle la réalisant ;)
Vous pourrez constater qu'après avoir annulé une action, les actions suivantes sont supprimées si la couleur de fond est à nouveau changée. Plus que le fait de changer la couleur de fond, c'est l'acte de changer l'état de l'historique qui entraîne cet effet.
Il ne vous reste plus qu'à compiler et constater que vous pouvez annuler/refaire cette modification, et ce par notre QUndoView ou les actions ajoutées au menu:

Image non disponible
Couleur de fond la plus récente (pardonnez le mauvais goût :))
Image non disponible
Couleur de fond originellement modifiée

II-2-3. Annulation du déplacement d'un objet

L'approche à adopter pour historiser les positions successives d'une entité est très similaire. La grosse différence est le point d'instanciation de l'action, ainsi que la fusion des commandes. Rien dans MainWindow ne nous permet de savoir que la modification de la position est effective. MySceneNode::setPosition est le point central de modification d'un noeud de la scène (ici l'entité), c'est donc dans cette méthode que l'instanciation de la commande d'annulation/application se fera.
Vous pourriez être tenté de simplement créer et pousser la commande dans la stack à partir de cette méthode. Je dois insister sur un point si vous n'avez pas vraiment lu la doc de QUndoStack: la méthode QUndoStack::push() applique la commande en l'ajoutant à la pile! Nous allons devoir créer une méthode supplémentaire afin de régler la position d'un noeud sans le faire directement afin d'éviter une récursion infinie. Si c'est encore un peu flou, tout devrait s'éclaircir avec le code peu à peu.

Commençons donc par regarder l'interface de la commande qui va nous permettre ceci:

 
Sélectionnez
class SceneNodeMoved : public QUndoCommand
{
public:
    SceneNodeMoved(MySceneNode *target, const Ogre::Vector3 &previousPosition, const Ogre::Vector3 &nextPosition);
    
    virtual int   id() const;
    virtual bool  mergeWith(const QUndoCommand *command);
    virtual void  undo();
    virtual void  redo();
	
private:
    MySceneNode   * node;
    Ogre::Vector3   prev;
    Ogre::Vector3   next;
    QTime           actionTime;
};

La méthode virtuelle id nous permettra de nous assurer qu'une fusion est possible entre 2 commandes. Elle se contente de retourner la valeur CID_MoveEntity. La dite fusion s'évalue au sein de la méthode mergeWith(). Si elle renvoie true, alors vous devez fusionner vous même les infos nécessaires; en voici le code:

 
Sélectionnez
bool SceneNodeMoved::mergeWith(const QUndoCommand *command)
{
    if(command->id() != CID_MoveEntity)
        return false;
    
    const SceneNodeMoved *otherCommand = dynamic_cast<const SceneNodeMoved*>(command);
    if (!otherCommand || node != otherCommand->node)
        return false;
    
    int msElapsed = qAbs(actionTime.msecsTo(otherCommand->actionTime));
    if(msElapsed > 500)
        return false;
    	
    next = otherCommand->next;
    actionTime = otherCommand->actionTime;
    
    return true;
}

Comme précisé plus haut, la première chose que nous faisons est de nous assurer que la commande est bien du même type que la notre. Une fois ceci fait, les contraintes précisées en introduction de ce chapitre sont implémentées:

  • le temps écoulé entre ces 2 commandes est de moins de 5 dixièmes de seconde,
  • l'entité est la même que la précédente.

Si ces 2 conditions sont remplies, nous nous approprions les infos de la commande la plus récente (le paramètre).
Les méthodes undo() et redo() s'occupent de positionner l'élément de cette façon:

 
Sélectionnez
void SceneNodeMoved::undo()
{
    node->setPositionDirect(prev);
}

setPositionDirect() est la méthode dont je parlais plus haut que nous allons ajouter à MySceneNode afin d'éviter une récursion infinie.

Passons maintenant à la création de cette commande. C'est OgreWidget qui s'occupe actuellement d'instancier les éléments de la scène, nous allons donc lui fournir un pointeur vers undoStack à partir de MainWindow, qui elle même le transférera aux objets nécessaires.
Une fois ceci fait, la dernière étape consiste à créer l'action et implémenter le déplacement de l'objet proprement dit:

 
Sélectionnez
void MySceneNode::setPositionDirect(const Ogre::Vector3 &pos)
{
    ogreSceneNode->setPosition(pos);
    
    emit positionChanged(pos);
}

void MySceneNode::setPosition(const Ogre::Vector3 &pos)
{
    if (undoStack)
    {
        Ogre::Vector3 currentPos = ogreSceneNode->getPosition();
        SceneNodeMoved *nodeMovedCommand = new SceneNodeMoved(this, currentPos, pos);
        undoStack->push(nodeMovedCommand);
    }
    else
        setPositionDirect(pos);
}

MySceneNode::setPosition() s'occupe maintenant de créer la commande et la pousser sur l'historique du document. Pour garantir le bon fonctionnement même si une QUndoStack n'est pas fournie, nous transférons l'appel à la bonne méthode. Notez bien que nous n'avons pas à toucher à la moindre des connections pour que le programme continue de tourner correctement (notre widget CoordModifier3D reste toujours synchronisé).
Si tout va bien, vous devriez obtenir ce genre de résultats:

Image non disponible
Etat le plus récent
Image non disponible
Quelques modifications auparavant...

Vous trouverez les sources correspondant à ce chapitre à cette adresse (miroir http).


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Denys Bulant. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.