Générateur d'images fractales (9)

14042009 In: Pas à pas

Quand on zoome on avant, notre ensemble fractal n'est plus très détaillé. Nous allons ajouter le réglage du nombre maximal d'itérations.

Modification de la couche Modèle

Jusqu'à présent, le nombre maximal d'itérations était codé en dur:

#define MAX_ITERATIONS  18

Nous remplaçons cette définition par la variable d'instance maxIterations

@interface CFRMandelbrotRender : NSObject {

            …
    unsigned char maxIterations; 
}

Je l'ai typée en unsigned char, pour qu'elle soit comprise entre 0 et 255. En effet, notre bitmap en 256 niveaux de gris ne saurait de toute façon pas stocker l'image avec plus de précision. Inutile donc de calculer plus de 255 itérations. Cependant, ce paramètre étant réglable par binding, il va être accédé par Key-Value Coding, qui ne gère que les entiers signés. J'ai donc rusé en utilisant des int pour les accesseurs:

- (void) setMaxIterations:(int)iterations
{
    if(iterations > 255)
        NSLog(@"-[CFRMandelbrotRender setMaxIterations:].
Le nombre d'itération doit être <= 255.");
    maxIterations = iterations;
}

- (int) maxIterations
{
    return maxIterations;   
}

On n'oublie pas de l'initialiser dans la méthode -init:

maxIterations = 18;

J'ai conservé cette valeur de 18 par défaut.

Ajout des contrôles

Ouvrez MyDocument.nib. Ajoutez à la fenêtre un NSLabel, un NSSlider et un NSTextField.

Edition du NIB

Réglez-les (avec le troisième onglet de l'Inspecteur) pour qu'ils restent scotchés au coin supérieur droit.

Configuration du NSSlider

  • Réglez son minimum à 3
    moins de trois itérations ne procure pas un résultat intéressant.
  • Réglez son maximum à 255
  • J'ai réglé Current à 10
    Peu importe, une fois bindé, il prendra la valeur par défaut du modèle, soit 18.

Configuration du NSTextField

  • J'ai tapé la valeur 255 dedans… pour m'assurer qu'elle y rentre bien.
  • Glissez un NSNumberFormatter sur le champ éditable.
    Réglez ses contraintes: minimum à 3 et maximum à 255. Décochez Allows Float, nous ne voulons que des valeurs entières.

Binding

Contrôleur

Tout d'abord, pour binder, il nous faut un objet Contrôleur qui soit compatible avec les bindings. Comme nous gérons une seule instance du Modèle, nous utiliserons un NSObjectController:

  • Glissez un NSObjectController dans MyDocument.nib
  • Renommez-le en MandelbrotRenderController.
  • Dans le premier onglet de l'inspecteur, réglez Class Name à CFRMandelbrotRender.
  • En maintenant la touche Contrôle appuyée, tirez son l'outlet content vers l'instance de Mandelbrot Render (le cube bleu).

Binding du NSSlider

Sélectionnez le slider et passez sur le 4ème onglet de l'Inspecteur ("Bindings"). Pour binder la clé Value:

  • Bind to: MandelbrotRenderController
  • Controller Key = selection
  • Model Key Path = maxIterations

Binding du NSTextField

Bindez le text field exactement de la même façon.

Résultat

Vous pouvez maintenant lancer le programme. Glissez le curseur Itérations maximales: l'ensemble reste le même. Allez sur la vue, et déplacez le repère ou zoomez: enfin, la nouvelle valeur est prise en compte.

Ce qui se passe est que le binding modifie effectivement maxIterations dans le CFRMandelbrotRender, mais cela ne provoque ni le recalcul de l'ensemble, ni l'affichage du nouvel ensemble.

Observation

Il faut un moyen pour que la vue sache que les paramètres ont été modifiés, que l'ensemble doit être recalculé, puis la vue rafraîchie. Une technique pourrait être de relier les actions du NSSlider et du NSTextField à la vue. Cependant, je pense ajouter par la suite des NSTextField pour permettre l'édition des autres paramètres, et cela va commencer à faire beaucoup d'actions. C'est pourquoi nous utiliserons ici la technique plus sophistiquée du Key-Value Observing.

Signaler le besoin d'une mise à jour

L'idée est que le CFMandelbrotRender sait quand il doit être recalculé: dès qu'un de ses paramètres de calcul est modifié. Nous allons lui ajouter une variable d'instance besoinMAJ, indiquant qu'une mise à jour est nécessaire:

@interface CFRMandelbrotRender : NSObject {

            …

    BOOL    besoinMAJ;  // Indique qu'une mise à jour est nécessaire

}

Ensuite, dès qu'un paramètre de calcul est modifié, nous mettrons besoinMAJ à YES:

- (void) setLargeurBitmap:(NSUInteger)largeurPixels
{
    largeurBitmap = largeurPixels;

    [self willChangeValueForKey:@"besoinMAJ"];
    besoinMAJ = YES;
    [self didChangeValueForKey:@"besoinMAJ"];

}

Notez l'encadrement par willChangeValueForKey: et didChangeValueForKey:. Ceux-ci sont indispensables au Key-Value Observing pour savoir que la variable besoinMAJ a changé.

Il faut faire de même dans les méthodes setHauteurBitmap:, zoomerDuFacteur:, decalerCentreX:y: puisqu'on y modifie les paramètres de calcul. Pour cette même raison, modifiez le setter de maxIterations:

- (void) setMaxIterations:(int)iterations
{
    if(iterations > 255)
        NSLog(@"-[CFRMandelbrotRender setMaxIterations:]. Le nombre d'itération doit être <= 255.");
    maxIterations = iterations;

    [self willChangeValueForKey:@"besoinMAJ"];
    besoinMAJ = YES;
    [self didChangeValueForKey:@"besoinMAJ"];
}

À la fin du rendu, repositionnez la variable:

- (NSBitmapImageRep*) bitmapImageRep
{
            …

    [self willChangeValueForKey:@"besoinMAJ"];
    besoinMAJ = NO;
    [self didChangeValueForKey:@"besoinMAJ"];

    return bitmapRep;
}

Observer les besoins de mise à jour

Passons maintenant à la vue.

Contexte

Créons d'abord un contexte. De quoi s'agit-il ? D'objets uniques qui permettent d'identifier quel keypath a changé par une rapide comparaison de pointeurs, plutôt qu'une lente comparaison de chaînes de caractères:

static void* ContexteMAJRendu = (void*) @"ContexteMAJRendu";

Le contenu de la chaîne n'a aucune importance, puisque c'est le pointeur de l'objet que nous allons utiliser. On utilise habituellement une chaîne parce que c'est plus simple pour le débogage, mais n'importe quel autre objet peut faire l'affaire, pourvu que son pointeur soit unique.

Changements

Pour être tenu au courant des changements des clés observées, implémentons la méthode:

- (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object
    change:(NSDictionary *)change
    context:(void *)context
{
    if(context == ContexteMAJRendu)
    {
        [self setNeedsDisplay:YES]; 
    }

}

Nous identifions le contexte et demandons l'affichage de la vue le cas échéant. Pour l'instant, il n'y a qu'une variable à observer, donc qu'un seul contexte, mais ça peut évoluer.

Nous n'avons plus besoin d'appeler setNeedsDisplay: ailleurs. Retirez-le des deux autres endroits.

Commencer l'observation

Ajoutez la méthode:

- (void) awakeFromNib
{
    [render addObserver:self forKeyPath:@"besoinMAJ" options:0 context:ContexteMAJRendu];   
}

Elle est appelée dès que tous les objets du NIB sont prêts. En particulier, ici, c'est l'objet render qui doit l'être. Nous ajoutons un observateur (=la vue), du keyPath besoinMAJ. Nous laissons les options par défaut, et fournissons le contexte à passer lors des changements.

Arrêter l'observation

Arrêter l'observation est toujours délicat, car il faut qu'elle ait cessé avant que l'objet observé reçoive un message -[dealloc]. Nous allons le faire quand la fenêtre se ferme.

- (void)viewWillMoveToWindow:(NSWindow *)newWindow
{
    if(newWindow == nil)
        [render removeObserver:self forKeyPath:@"besoinMAJ"];
}

Cette méthode est habituellement appelée lorsque la vue est insérée dans la fenêtre. Cependant, quand newWindow est nulle, cela signifie que la fenêtre va être close.

Et là ça marche ?

Vous pouvez essayer de lancer à nouveau l'appli. Cette fois-ci, ça fonctionne. À la prochaine !

Le projet Xcode complet à télécharger.

Renaud Pradenc
Céroce

Articles similaires