"synchronisation" de vues

RocouRocou Membre
04:11 modifié dans API AppKit #1
Bonjour,

Dans une vue, j'affiche un "planning". C'est à  dire des colonnes avec des entêtes dans lesquelles j'inscris des dates. J'utilise une NSScrollView pour afficher un planning plus grand que l'écran.
Mon soucis est que évidemment, les entêtes disparaissent quand j'actionne l'ascenseur vertical.

Aussi, j'ai créé une seconde vue, placé juste au dessus de la première afin d'y placer ces entêtes. Et évidemment, l'ascenseur horizontal de la première vue n'a pas d'effet sur la seconde vue.
Comment faire pour synchroniser ces deux vues?

Y-a-t-il une façon de procéder plus astucieuse?

Réponses

  • CéroceCéroce Membre, Modérateur
    04:11 modifié #2
    Je dirais qu'il faudrait que tu crées une classe qui hérite de NSRulerView (voir Scroll View Programming Guide).

    ça ne m'a pas l'air facile.
  • RocouRocou Membre
    04:11 modifié #3
    dans 1243350717:

    Je dirais qu'il faudrait que tu crées une classe qui hérite de NSRulerView (voir Scroll View Programming Guide).

    ça ne m'a pas l'air facile.

    Oui, pas facile.

    Aussi, j'ai eu une autre idée. Elle fonctionne presque.
    Je ne garde qu'un vue. Je lie l'ascenseur à  une action qui me renvoie son déplacement.
    Puis j'ajoute cette valeur à  l'origine y de mes blocs en rouge sur la photo. Puis je demande une réactualisation de la vue ([maVue setNeedsDisplay:YES]).
    Ainsi l'entête se redessine toujours au même endroit, alors que mes blocs rouges se déplacent en fonction du scroll.
    Pour cela, j'ai trouve la méthode d'instance lineScroll ([monScrollVertical lineScroll]).

    Le problème, c'est que cette méthode me renvoie le déplacement de l'ascenseur en valeur absolue or j'aimerais pouvoir détecter si l'ascenseur descend ou si il monte afin de renvoyer une valeur positive ou négative.

    Pour le moment, je sèche.

  • fouffouf Membre
    04:11 modifié #4
    Tu devrais vérifier si on ne peut pas faire de KVO sur la propriété floatValue: de NSScroller (ca m'étonnerait énormément si ca n'était pas le cas). Comme ca tu seras au courant du fait que l'utilisateur a scroller et tu pourras récupérer aussi bien la nouvelle position du scroller que l'ancienne (grâce au dictionnaire que tu reçois dans la méthode observeValueForKeyPath:ofObject:change:context: et la clé NSKeyValueChangeOldKey) et ainsi connaà®tre le déplacement.
  • RocouRocou Membre
    04:11 modifié #5
    dans 1243355352:

    Tu devrais vérifier si on ne peut pas faire de KVO sur la propriété floatValue: de NSScroller (ca m'étonnerait énormément si ca n'était pas le cas). Comme ca tu seras au courant du fait que l'utilisateur a scroller et tu pourras récupérer aussi bien la nouvelle position du scroller que l'ancienne (grâce au dictionnaire que tu reçois dans la méthode observeValueForKeyPath:ofObject:change:context: et la clé NSKeyValueChangeOldKey) et ainsi connaà®tre le déplacement.

    :-\\
    Mince, tout ça, c'est du charabia pour moi.
    KVO chez HilleGass, c'est une demi page.
  • CéroceCéroce Membre, Modérateur
    mai 2009 modifié #6
    Voici un résumé qui va te faire gagner beaucoup de temps.

    Le code est à  ajouter à  ta vue, pour qu'elle observe l'ascenseur horizontal et se mette à  jour dés que sa propriété floatValue change. En espérant qu'il notifie effectivement des changements (appels à  -willChangeValueForKey: et -didChangeValueForKey:).


    Les contextes
    Il s'agit de références uniques qui permettent à  la méthode -observeValue:forKeyPath... de différencier la nature du changement. On utilise habituellement des chaà®nes de caractères, ce qui est plus pratique pour le débogage:

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



    Observation
    La méthode est appelée lorsqu'une clé observée change:

    <br />- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context<br />{<br />	if(context == HorizontalSliderContext)<br />	{<br />		// L&#39;objet est forcément l&#39;ascenseur horizontal; <br />		// le keypath est forcément celui qu&#39;on observe.<br />		NSNumber* xOriginNbr = [object valueForKeyPath:keyPath];<br />		xOrigin = [xOriginNbr floatValue];<br /><br />		// Dessiner la vue avec xOrigin comme point de départ<br />		[self setNeedsDisplay:YES];<br />	}<br />}
    


    Commencer l'observation

    - (void) awakeFromNib<br />{<br />	[horizontalSlider addObserver:self forKeyPath:@&quot;floatValue&quot; options:0 HorizontalSliderContext];	<br />}
    


    Je n'ai pas mis d'option, mais tu peux passer NSKeyValueChangeOldKey si tu veux l'ancienne valeur. Tu pourras alors récupérer cette valeur dans le dictionnaire change passé à  la méthode -observeValueForKeyPath...

    Arrêter l'observation

    - (void) dealloc<br />{<br />	[horizontalSlider removeObserver:self forKeyPath:@&quot;floatValue&quot;];<br />	<br />	[super dealloc];	<br />}
    



    Tout le reste de la doc d'Apple est du détail. Effectivement, la partie KVO / Bindings est le gros point faible du livre d'Hillegass.
  • RocouRocou Membre
    04:11 modifié #7
    dans 1243505026:

    Voici un résumé qui va te faire gagner beaucoup de temps.

    Merci beaucoup. Je comprends le principe mais je n'arrive pas à  l'implémenter dans mon code.

    horizontalSlider, c'est quoi? Un Outlet de type NSScrollView?

    Pas de problème à  la compilation mais on ne passe jamais dans la méthode:
    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{...}
    


    NB: j'ai corrigé:
    - (void) awakeFromNib
    {
    [horizontalSlider addObserver:self forKeyPath:@floatValue options:0 HorizontalSliderContext];
    }

      en
    - (void) awakeFromNib
    {
    [horizontalSlider addObserver:self forKeyPath:@floatValue options:0 context: HorizontalSliderContext];
    }
  • CéroceCéroce Membre, Modérateur
    04:11 modifié #8
    dans 1243507371:

    horizontalSlider, c'est quoi? Un Outlet de type NSScrollView?


    Non, un pointeur sur le NSScroller horizontal. Crée une outlet vers la NSScrollView puis dans awakeFromNib:
    horizontalScroller = [scrollView horizontalScroller];
    
  • RocouRocou Membre
    04:11 modifié #9
    dans 1243511500:

    Non, un pointeur sur le NSScroller horizontal. Crée une outlet vers la NSScrollView puis dans awakeFromNib:
    horizontalScroller = [scrollView horizontalScroller];
    


    Ha ok. Et y-a-t-il des chose à  faire sous IB? Faut-il appeler expressément observeValueForKeyPath?
    Car pour le moment, on n'y passe jamais.
  • CéroceCéroce Membre, Modérateur
    04:11 modifié #10
    Aà¯e ! Non, si on admet que ton code marche, ça veut dire que les NSScroller n'envoient pas de notifications de changement de leur floatValue. C'est ce que fouf et moi craignions.

    Le KVO n'est pas magique, il faut que l'objet observé dise qu'il est en train de changer. Voilà  comment doit être codée la méthode setFloatValue du NSScroller pour que les observateurs soient notifiés:

    - (void) setFloatValue:(float) newPosition<br />{<br />	[self willChangeValueForKey:@&quot;floatValue&quot;];<br />	floatValue = newPosition;<br />	[self didChangeValueForKey:@&quot;floatValue&quot;];<br />}
    



    Tu peux vérifier ton code en essayant d'observer la propriété floatValue d'un NSSlider, plutôt que celle d'un NSScroller. Je suis certain que les NSSliders envoient bien les notifications, sinon les bindings ne marcheraient pas.
  • RocouRocou Membre
    04:11 modifié #11
    Bon, je ne suis pas sûr d'avoir fait les choses comme il faut.

    J'ai mis tout le code dans MaVue (et pas dans appControlleur).

    dans Mavue.h j'ai définit un outlet dans l'interface:
    IBOutlet NSSlider *verticalSlide;
    


    J'ai ajouté l'objet graphique NSSlider et réalisé le lien sous IB avec mon outlet.

    Puis dans MaVue.m j'ai ajouté le code:
    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context<br />{<br />	NSLog(@&quot;on observe...&quot;);<br />	if(context == VerticalSliderContext)<br />	{<br />		// L&#39;objet est forcément l&#39;ascenseur vertical; <br />		// le keypath est forcément celui qu&#39;on observe.<br />		NSNumber* yOriginNbr = [object valueForKeyPath:keyPath];<br />		<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //setOrigineLocation permet d&#39;initialiser l&#39;origine<br />		[self setOrigineLocation: [yOriginNbr floatValue]];<br /><br />		<br />		// Dessiner la vue avec xOrigin comme point de départ<br />		[self setNeedsDisplay:YES];<br />	}<br />}<br />
    


    Puis, toujours dans MaVue.m:
    - (void)awakeFromNib<br />{<br /><br />	[verticalSlide addObserver:self forKeyPath:@&quot;floatValue&quot; options:0 context:VerticalSliderContext];	<br />		<br />}<br />
    


    J'ai oublié quelque chose?
  • CéroceCéroce Membre, Modérateur
    04:11 modifié #12
    Tu as tout fait comme il faut (sauf que c'est bien le scroller horizontal que tu veux observer), mais le problème, c'est qu'on ne sait pas quelle propriété observer.

    Nous savons qu'un slider est observable, sinon les bindings sur la valeur du slider ne marcheraient pas. J'ai joint un projet d'essai, qui observe le scroller et aussi un slider. Pour le slider, j'ai essayé d'observer les clés:

    - floatValue
    - doubleValue
    - intValue
    - objectValue

    sans succès. Bref, dans les cas des bindings, je suis prêt à  parier que les contrôleurs (NSObjectController, NSArrayController, etc.) observent une clé privée.



    Maintenant, pour revenir à  ton cas: ça veut dire que la méthode proposée par fouf, qui est plus simple, et donc qu'il fallait essayer, ne marche pas.

    Tu peux essayer l'autre méthode, celle de sous-classer NSRulerView. Je te conseille d'arriver à  afficher la RulerView standard avant de mettre la tienne à  la place.
  • fouffouf Membre
    04:11 modifié #13
    Sinon, tu peux essayer le truc suivant : tu sous-classes NSScroller en surchargeant setFloatValue: en faisant appel aux méthodes de KVO (will- et didChangeValueForKey:) et ensuite tu utilises la méthode magique poseAsClass: pour remplacer NSScroller par ta sous-classe. Ensuite

    Bon, je viens de vérifier et poseAsClass:, et c'est devenu deprecated sous 10.5. Donc, là  j'ai plus d'idées ...
  • AliGatorAliGator Membre, Modérateur
    mai 2009 modifié #14
    et ça alors ?
    Some application user interfaces require that scrolling in a scroll view causes synchronized scrolling in another scroll view. You synchronize the scrolling of two scroll view's by having the views register to receive notifications of the changes in the other's content view bounds.
    Au final il suffit de s'abonner à  la notification NSViewBoundsDidChangeNotification pour être informé du scroll. Le reste est tout décrit dans la doc ;)
  • fouffouf Membre
    04:11 modifié #15
    dans 1243537013:

    et ça alors ?
    Some application user interfaces require that scrolling in a scroll view causes synchronized scrolling in another scroll view. You synchronize the scrolling of two scroll view's by having the views register to receive notifications of the changes in the other's content view bounds.
    Au final il suffit de s'abonner à  la notification NSViewBoundsDidChangeNotification pour être informé du scroll. Le reste est tout décrit dans la doc ;)


    J'ai honte de ni avoir vu ca dans la doc, ni avoir pensé à  la notification ;)
  • AliGatorAliGator Membre, Modérateur
    04:11 modifié #16
    dans 1243537741:

    dans 1243537013:

    et ça alors ?
    Some application user interfaces require that scrolling in a scroll view causes synchronized scrolling in another scroll view. You synchronize the scrolling of two scroll view's by having the views register to receive notifications of the changes in the other's content view bounds.
    Au final il suffit de s'abonner à  la notification NSViewBoundsDidChangeNotification pour être informé du scroll. Le reste est tout décrit dans la doc ;)


    J'ai honte de ni avoir vu ca dans la doc, ni avoir pensé à  la notification ;)
    Toujours penser à  lire les Programming Guides, et pas juste se cantonner au class references... Parce que c'est pas dans la NSScrollView class reference que tu vas trouver ce genre de subtilités là , alors que dans les Programming Guides, y'a souvent des problèmes types du genre qui sont traités, et des How-To bien pratiques... mais on pense pas toujours à  aller fouiller de ce côté de la doc non plus, à  tort ;)
  • CéroceCéroce Membre, Modérateur
    04:11 modifié #17
    Le pire c'est que je l'ai survolé, mais le paragraphe s'intitule "synchronisation de deux scrollviews", alors je ne m'y suis pas plongé d'avantage (un peu marre de me taper 20 pages de doc à  chaque fois que je dois coder quoi que ce soit).
  • RocouRocou Membre
    04:11 modifié #18
    dans 1243537013:

    Au final il suffit de s'abonner à  la notification NSViewBoundsDidChangeNotification pour être informé du scroll. Le reste est tout décrit dans la doc ;)

    Bon, je n'ai plus qu'à  étudier le chapitre sur les "notifications". A très bientôt  :)
  • RocouRocou Membre
    04:11 modifié #19
    dans 1243537013:

    Au final il suffit de s'abonner à  la notification NSViewBoundsDidChangeNotification pour être informé du scroll. Le reste est tout décrit dans la doc ;)

    Bon, d'après ce que j'ai compris et après lecture du chapitre Notifications du bouquin d'Hillegass, voici mon code (pour test):

    Dans initWithFrame de ma classe NSView:

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];<br />		[nc addObserver:self selector:@selector(montest:) name:@&quot;viewBoundsDidChange&quot; object:nil];
    


    Puis j'ai implémenté les méthodes suivantes:


    -(void)montest<br />{<br />	NSLog(@&quot;titi&quot;);<br />}<br /><br />-(void)viewBoundsDidChange:(NSNotification *)aNotification<br />{<br />	NSLog(@&quot;toto&quot;);<br />}<br />
    


    J'ai du rater quelque chose car on ne passe jamais dans ces fonctions  :-\\

    Je ne suis pas sûr d'avoir compris comment désigner un "délégué"
  • AliGatorAliGator Membre, Modérateur
    04:11 modifié #20
    Pourquoi avoir mis cette chaà®ne bizarre dans "name" plutôt que la constante indiquée dans la doc Apple, à  savoir NSViewBoundsDidChangeNotification... c'est à  dire exactement ce qu'ils font dans le code d'exemple qu'ils fournissent quand tu cliques sur mon lien et où tout le modèle du code est déjà  fourni ?
  • RocouRocou Membre
    mai 2009 modifié #21
    dans 1243603554:

    Pourquoi avoir mis cette chaà®ne bizarre dans "name" plutôt que la constante indiquée dans la doc Apple, à  savoir NSViewBoundsDidChangeNotification... c'est à  dire exactement ce qu'ils font dans le code d'exemple qu'ils fournissent quand tu cliques sur mon lien et où tout le modèle du code est déjà  fourni ?

    Ben c'est en lisant le bouquin d'Hillegass, page 215:
    Si un objet Cocoa standard possède un délégué et poste des notifications, il est automatiquement inscrit comme observateur pour les méthodes implémentées par l'objet. Si nous mettons en oeuvre un tel délégué, comment connaà®tre le nom de la méthode?

    La convention de nommage est simple. Le nom débute par celui de la notification. Le préfixe NS est supprimé et la première lettre est mise en minuscule. Notification est supprimé à  la fin.



    Et puis si je mets NSViewBoundsDidChangeNotification, le programme plante tout de suite...

    Et enfin, je ne sais pas quoi faire du code de la doc. Sont-ce des méthodes qui sont appelée toutes seules? Où dois-je les mettre? Etc.
    Ce n'est pas clair du tout même les méthodes en elle-même ne sont pas très compliquées.
  • AliGatorAliGator Membre, Modérateur
    04:11 modifié #22
    Dans ton initWithFrame:
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];<br />      [nc addObserver:self selector:@selector(viewBoundsDidChange:) name:NSViewBoundsDidChangeNotification object:nil];
    
    J'ai mis [tt]@selector(viewBoundsDidChange:)[/tt] mais tu mets ce que tu veux comme nom de sélecteur, du moment que ça définit un sélecteur (une méthode) prennant un unique paramètre (de type NSNotification*) et sans retour (retournant void).

    Du coup dans la même classe que celle où tu as mis ce code de initWithFrame, tu mets
    -(void)viewBoundsDidChange:(NSNotification *)aNotification<br />{<br /> &nbsp; NSLog(@&quot;toto&quot;);<br />}
    
    où là  j'ai appellé ma méthode du même nom que celle que j'ai mentionné dans le @selector quand je l'ai enregistrée dans le NotificationCenter, donc si tu choisis un autre nom pour ta méthode faut mettre le même dans le @selector(...) de addObserver.

    Le problème avec ton code c'est que :
    1) Le paramètre passé à  "name" n'était pas le bon
    2) Vu comment tu as appelé addObserver:..., tu t'attends à  ce que la méthode montest: soit appellée... sauf qu'il faudrait que quand tu déclares cette méthode, elle prenne un argument, de type NSNotification*. Là  tu n'as pas de méthode "montest:", juste une méthode "montest" (note bien l'absence de ":" donc l'absence de paramètres)... et c'est ce qui fait toute la différence.
    C'est d'ailleurs pour ça que ton programme plante si tu mets bien NSViewBoundsDidChangeNotification comme paramètre pour "name" : car du coup ça enregistre bien la bonne notification, mais il essaye d'appeller la méthode "montest:" (méthode attendant un paramètre, ne pas oublier les ":" qui changent tout) alors qu'elle n'existe pas (tu as une méthode "montest", sans paramètres mais pas "montest:"). Donc appel de méthode inexistante = plantage.


    PS : Ne pas oublier d'arrêter d'observer la notification (appel à  removeObserver:... sur le notificationCenter) lors du "dealloc" de ta classe.
  • RocouRocou Membre
    mai 2009 modifié #23
    dans 1243608778:


    Le problème avec ton code c'est que :
    1) Le paramètre passé à  "name" n'était pas le bon
    2) Vu comment tu as appelé addObserver:..., tu t'attends à  ce que la méthode montest: soit appellée... sauf qu'il faudrait que quand tu déclares cette méthode, elle prenne un argument, de type NSNotification*. Là  tu n'as pas de méthode "montest:", juste une méthode "montest" (note bien l'absence de ":" donc l'absence de paramètres)... et c'est ce qui fait toute la différence.
    C'est d'ailleurs pour ça que ton programme plante si tu mets bien NSViewBoundsDidChangeNotification comme paramètre pour "name" : car du coup ça enregistre bien la bonne notification, mais il essaye d'appeller la méthode "montest:" (méthode attendant un paramètre, ne pas oublier les ":" qui changent tout) alors qu'elle n'existe pas (tu as une méthode "montest", sans paramètres mais pas "montest:"). Donc appel de méthode inexistante = plantage.


    PS : Ne pas oublier d'arrêter d'observer la notification (appel à  removeObserver:... sur le notificationCenter) lors du "dealloc" de ta classe.

    Ok, merci beaucoup. J'ai enfin compris.

    Cela dit, au lancement du programme, on passe bien dans mon sélecteur (deux fois) mais ensuite on y repasse plus du tout quoique je fasse avec ma vue (agrandissement, déplacement des ascenseurs, etc.). Normalement, c'était le but  :)


    EDIT: ha, j'ai trouvé, cela venait du NSScroller connecté à  une IBAction.

    Merci à  tous, il ne me reste plus qu'à  récupérer les données...
  • RocouRocou Membre
    04:11 modifié #24
    Youpi! ça fonctionne  :adios!:

    Encore une fois, merci à  tous!
  • AliGatorAliGator Membre, Modérateur
    04:11 modifié #25
    Bon je viens de relire mon post en de voir que j'avais simplement copié/collé ton code sur le addObserver sans trop faire gaffe... Et je vois à  l'instant que tu as laissé "nil" comme paramètre de "object:" ! Pas une bonne idée ça !

    En fait quand on prend l'appel à  addObserver, il est simple :
    [nc<br />&nbsp;  addObserver:self<br />&nbsp;  selector:@selector(viewBoundsDidChange:)<br />&nbsp;  name:NSViewBoundsDidChangeNotification<br />&nbsp;  object:otherView<br />];
    
    - [tt]addObserver:self[/tt] : Je veux que tu notifies "self" (moi, quoi, l'objet en cours)
    - [tt]selector:@selector(viewBoundsDidChange:)[/tt] : en appelant la méthode "viewBoundsDidChange:" (qui prend un unique paramètre)
    - [tt]name:NSViewBoundsDidChangeNotification[/tt] : quand la notification identifiée par "NSViewBoundsDidChangeNotification" (la notification de changement de bounds d'une view)
    - [tt]object:otherView[/tt] : est émise par l'objet "otherView".

    Autrement dit lorsque otherView change de bounds et émet alors une notification de type NSViewBoundsDidChangeNotification, appeller la méthode "viewBoundsDidChange:(NSNotification*)notif" sur l'objet "self".

    Si tu mets "nil" comme paramètre de "object", cela va appeller "viewBoundsDidChange:" sur ton objet self à  chaque fois qu'une notification NSViewBoundsDidChangeNotification sera émise... quel que soit l'émetteur : ton autre scrollview avec qui tu veux en effet te synchroniser... ou une autre. Si tu commences à  avoir plusieurs scrollviews ou plusieurs views pouvant changer leurs bounds, tu vas recevoir des notifications d'un peu tout le monde... Alors que toi à  la base ce qui t'intéresse c'est uniquement de t'abonner à  la notification envoyée par ton autre scrollview, et pas les autres, pour justement de synchroniser avec.
    C'est d'ailleurs ce que fait Apple dans son code d'exemple.
Connectez-vous ou Inscrivez-vous pour répondre.