[Résolu] Zoom et recentrage d'une NSView

berfisberfis Membre
décembre 2013 modifié dans API AppKit #1

Bonsoir,


 


Le problème semble simple, et sans doute l'est-il, mais je ne vois pas comment procéder.


 


J'ai une image affichée dans une NSScrollView. Deux boutons, zoomIn et zoomOut, permettent d'agrandir et de rapetisser l'image d'un facteur 2. Cela fonctionne parfaitement, mais j'ai un petit souci d'esthétique: à  chaque changement de taille, mon image, redimensionnée, se recale à  la position (0;0).


 


Mon idée est de garder le même centre, quelle que soit la position initiale de l'image (un zoom centré).


 


Comment faire? il doit s'agir d'un calcul élémentaire basé sur la nouvelle taille, donnant le nouveau point sur lequel scroller, mais je me dispute depuis des heures avec des moitiés de racines carrées, sans obtenir de résultat.


 


Quelqu'un peut me souffler la réponse? Merci d'avance...


Mots clés:

Réponses

  • AliGatorAliGator Membre, Modérateur
    Un peu fatigué pour te sortir des calculs mathématiques à  faire, mais moi j'aborderai le problème en cherchant à  garder la scrollview centrée sur le même point. C'est à  dire pas chercher à  faire des calculs savants pour directement connaà®tre le facteur à  appliquer au contentOffset de la scrollView, mais passer par le point intermédiaire "centre de la scrollview".

    L'idée c'est de calculer avant ton zoom quelles sont les coordonnées du point C de ton contenu se trouve pile au centre de ta scrollview. Puis de zoomer. Puis de partir du principe que tu veux ensuite scroller de sorte que ce point C calculé auparavant se retrouve à  nouveau au centre de ta scrollView après le zoom (et donc de calculer le contentOffset à  partir de ce point C, genre en lui soustrayant la moitié du bounds.size de ta scrollView)

    Par rappel, quand on applique un facteur de zoom à  une ScrollView, sa frame ne bouge pas, mais ses bounds changent ; les bounds sont les coordonnées internes de la scrollView, après y avoir appliqué le facteur de zoom et le scrollOffset. Donc ne pas confondre / s'emmêler les pinceaux entre frame et bounds. Relire les Programming Guides si besoin pour éclaircir ce point.
  • CéroceCéroce Membre, Modérateur

    Comment fais-tu le zoom ?


    En pratique, la bonne manière est d'utiliser une matrice de transformation.


     


    Comme l'indique for justement AliGator, il y a une différence entre les rectangles frame et bounds. C'est cela qu'il faut comprendre avant tout. Ce n'est pas clair dans la doc d'Apple, mais changer bounds sans changer frame modifie la matrice.




  • Comment fais-tu le zoom ?


    En pratique, la bonne manière est d'utiliser une matrice de transformation.


     


    Comme l'indique for justement AliGator, il y a une différence entre les rectangles frame et bounds. C'est cela qu'il faut comprendre avant tout. Ce n'est pas clair dans la doc d'Apple, mais changer bounds sans changer frame modifie la matrice.




    Je suis pas le seul du coup ... J'arrive pas a voir la difference entre frame et bounds... Enfin plus quoi correspond a quoi...

  • CéroceCéroce Membre, Modérateur
    novembre 2013 modifié #5

    bounds est exprimé dans le système de coordonnées de la vue.


    frame est exprimé dans le système de coordonnées de la vue parente.


     


    Ainsi, par exemple, si la vue mesure 200 x 200 avec un zoom de 100 %, on peut obtenir un zoom à  200% en faisant:


    1) mettre la frame à  400 x 400


    2) mettre bounds à  200 x 200 


    (Dans cet ordre).


     


    Dans les deux cas, puisque la vue dessine toujours par rapport au rectangle bounds, elle aura toujours l'impression de dessiner à  la même taille. Cependant, la différence entre frame et bounds va modifier la matrice de transformation courante.


  • Merci pour les precisions !


  • AliGatorAliGator Membre, Modérateur
    Pour formuler autrement :

    - frame c'est les coordonnées de la vue dans sa vue parente. Si tu augmentes la frame.size ta vue va être plus grande comme si tu l'agrandissait dans IB. Si tu changes frame.origin ta vue va être à  un autre endroit comme si tu la déplaçais dans IB.
    - bounds c'est les coordonnées du contenu de ta vue. Par exemple utilisés pour dessiner. Si tu augmentes bounds.size (en gardant le frame.size d'origine), par exemple si frame.size.width vaut 200 mais que tu mets bounds.size.width à  400, ça veut dire que si tu dessines dans ta vue, pour avoir un rectangle qui prend toute la largeur de ta vue, il faudra le dessiner avec une largeur de 400. Pour un bounds.origin.x = 0 et bounds.size.width = 400, le point x=400 correspond au point le plus à  droite du contenu de ta vue. Si tu dessines un point en x = 400 il sera sur le bord droit de ta vue. Si tu dessines un point en x = 200 il sera au milieu, x = 0 il sera à  gauche de ta vue.
    Du coup tu peux considérer que tu as fait un zoom x0.5, puisque si tu dessines un rectangle qui va de x=0 à  x=400 il va être affiché à  l'écran dans une vue qui fait 200 de large (frame.size.width=200)


    En général sans transformation particulière appliquée, bounds.size et frame.size valent la même chose, et bounds.origin = {0,0}. Donc si ta frame fait 200 de large, tes coordonnées de dessin du contenu de ta vue vont de 0 (bounds.origin.x) à  200 (bounds.origin.x + bounds.size.width) et tu dessines à  l'échelle 1, avec l'origine des coordonnées en 0,0.

    Si on prend le problème dans le sens inverse, disons que tu es dans une scrollview, vu que tu scrolles le contenu, même si la frame de ta ScrollView ne bouge pas, son bounds.origin augmente. Ainsi si tu scrolles de 50px vers la droite, bounds.origin.x va valoir 50, et c'est ainsi que si tu veux dessiner un truc qui apparait à  ce moment là  calé sur le bord gauche de ta ScrollView, il faut le dessiner en x = 50 puisque c'est les coordonnées du bord gauche. Si tu dessines un truc à  x = 0 ou x = 20 il ne sera pas visible car trop à  gauche, à  moins que tu rescrolles à  gauche pour le voir.

    De même si tu as une ScrollView dont la frame.size est 200x200, et que tu mets une image de 200x200 dedans, au début tu vas voir toute l'image. Le coin en haut à  droite de la vue va bien afficher le pixel 200,200 de l'image, ton image sera entière. Si maintenant tu zoom x2, tout en gardant bounds.origin à  0,0, tu ne verras que le quart inférieur gauche de ton image. Le coin inférieur gauche de ta vue affichera le pixel 0,0 de ton image, le coin supérieur droit affichera le pixel qui est au milieu de ton image (puisque tu auras zoomé x2), donc le pixel 100,100 de ton image. Autrement dit, tes bounds sont 0,0,100,100.
  • MalaMala Membre, Modérateur


     


    Quelqu'un peut me souffler la réponse? Merci d'avance...




    Regardes CGZoomView...


    http://apptree.net/code.htm

  • CGZoomView utilise scaleUnitSquareToSize, qui ne m'est pas très utile, étant donné qu'il ne s'agit pas d'une NSImage, mais d'une vue entièrement dessinée avec Quartz. Ainsi, une ligne de 1 pixel reste à  1 pixel quelle que soit l'échelle d'affichage, ce qui n'est pas le cas avec scaleUnitSquareToSize...


     


    En revanche, CGZoomView utilise la solution d'AliGator, à  savoir passer par le point central de la vue avant et après affichage, quitte à  faire un peu d'arithmétique afin d'amener le point en position centrale après le zoom. Mais... il reste encore à  faire... :*


  • MalaMala Membre, Modérateur


    CGZoomView utilise scaleUnitSquareToSize, qui ne m'est pas très utile, étant donné qu'il ne s'agit pas d'une NSImage, mais d'une vue entièrement dessinée avec Quartz. Ainsi, une ligne de 1 pixel reste à  1 pixel quelle que soit l'échelle d'affichage, ce qui n'est pas le cas avec scaleUnitSquareToSize...


     


    En revanche, CGZoomView utilise la solution d'AliGator, à  savoir passer par le point central de la vue avant et après affichage, quitte à  faire un peu d'arithmétique afin d'amener le point en position centrale après le zoom. Mais... il reste encore à  faire... :*




    Je dois avoir loupé quelque chose. J'utilise zoomViewToAbsoluteScale: depuis des années pour de l'affichage vectoriel/bitmap avec quartz.

  • CéroceCéroce Membre, Modérateur
    novembre 2013 modifié #11
    @Mala: il ne dit pas le contraire. Mais comme il l'explique, si on modifie la matrice de transformation (soit en utilisant scaleUnitSquareToSize(), soit en modifiant bounds comme je le fais), alors si on fait un zoom x 2, un trait d'une épaisseur de 1 point aura une épaisseur de 2 pixels. Alors que Berfis a l'air de vouloir que l'épaisseur soit constamment de 1 pixel.

    À partir de là , effectivement, la solution est
    - soit de recalculer les coordonnées
    - soit d'appliquer une matrice, mais de moduler l'épaisseur des traits.

    Personnellement, je serais parti sur la seconde solution, plus simple à  mettre en oe“uvre, mais qui demande un peu d'expérience.
  • MalaMala Membre, Modérateur

    Ok, je vois. Rien à  voir avec Quartz en fait. De mémoire, je m'étais basé sur ta seconde solution pour le tracé de bounding box lors d'une sélection multiple.


  • berfisberfis Membre
    novembre 2013 modifié #13

    Ouais ouais ouais... j'ai présenté le problème à  un collègue (docteur en maths pourtant), et il a séché dessus. D'un côté ça me rassure, de l'autre... ben j'erre toujours. Je me sens super bête à  tâtonner à  la recherche d'une solution qui va tenir en deux lignes, j'en suis sûr...


  • MalaMala Membre, Modérateur

    Je vois vraiment pas la difficulté avec la classe que je t'ai indiqué. ???


     


    Si tu veux une épaisseur constante d'un pixel pour l'épaisseur de tes traits dessinés à  la mano, il faut juste que tu  dessines avec une épaisseur qui soit l'inverse du zoom soit 1/[self scale].


     


    Par exemple, moi dans mon cas, je voulais que mon rectangle de sélection multiple ne dépende pas du zoom (ce qui est assez logique) afin que le contour fasse toujours 2 pixels de large. Cela me donne quelque chose comme ça à  la fin de mon drawRect:



    if( drawingSelectionRect == YES)
    {
        // Draw the selection Box if needed...

        NSBezierPath *selectionPath = [NSBezierPath bezierPath];    
        [selectionPath setLineWidth:2/[self scale]];   
        [[[NSColor alternateSelectedControlColor] colorWithAlphaComponent:0.6] set];
        [selectionPath appendBezierPathWithRect:selectionRect];
        [selectionPath setLineJoinStyle:NSRoundLineJoinStyle]; 
        [selectionPath stroke];
            
        selectionPath = [NSBezierPath bezierPath];    
        [selectionPath setLineWidth:2/[self scale]];   
        [[[NSColor whiteColor] colorWithAlphaComponent:0.4] set];
        [selectionPath appendBezierPathWithRect:selectionRect];
        [selectionPath setLineJoinStyle:NSRoundLineJoinStyle]; 
        [selectionPath fill];
    }

    Pour l'implémentation, le documentView est juste notre classe héritée de GCZoomView et on dessine en surchargeant son drawRect. On est bien d'accord?


  • CéroceCéroce Membre, Modérateur
    novembre 2013 modifié #15

    Ouais ouais ouais... j'ai présenté le problème à  un collègue (docteur en maths pourtant), et il a séché dessus.



     


    En gros, voici la situation:


     


  • berfisberfis Membre
    décembre 2013 modifié #16

    Merci à  tous, j'ai finalement fait quelque chose qui semble marcher.


     


    Dans le code qui suit:


    _halfFrame est le centre de la largueur/longueur de ma scrollview (carrée)


    _cellSize est la taille d'une "cellule" (carrée aussi) de la NSView, découpée en kGridSize x kGridSize  cellules


    _zoomFactor est un float entre 1.0 (100%) et 32.0 (3200 %)



    NSClipView *cv =(NSClipView*)[self superview];
    NSRect fr = [cv documentVisibleRect];
    NSPoint cp;
    cp.x = fr.origin.x + _halfFrame;
    cp.y = fr.origin.y + _halfFrame;

    NSScrollView* sv = (NSScrollView*)cv.superview;
    [self setFrame:NSMakeRect(0, 0, (_cellSize+1)*kGridSize, (_cellSize+1)*kGridSize)];
    [cv setDocumentView:self];

    cp.x =MAX(0,(cp.x*_zoomFactor/oldZoomFactor) - _halfFrame);
    cp.y =MAX(0,(cp.y*_zoomFactor/oldZoomFactor) - _halfFrame);
    [cv scrollToPoint :cp];
    [cv reflectScrolledClipView:cv];

    Remarquez au passage reflectScrolledClipView, qui "officialise" les réglages des scrollers, sinon c'est la mauvaise surprise garantie au premier clic sur la vue zoomée (elle saute à  la coordonnée zéro)...


Connectez-vous ou Inscrivez-vous pour répondre.