Accueil > Programmation Cocoa > Tout sur le Petit Globe Vert

Tout sur le Petit Globe Vert

Par Mike Beam, le 17/05/2002

Traduit par Thierry, le 29/12/2002.

A chaque fois que j’entends des gens parler des désagréments de Mac OS X, ce qui revient régulièrement tourne souvent autour des disfonctionnements du bouton de zoom des fenêtres. Vous savez tous de quoi je veux parler : vous cliquez sur ce petit globe couleur émeraude en vous attendant à ce que le fenêtre soit redimensionnée de façon à afficher l’ensemble de son contenu sans pour autant s’accaparer la totalité de l’écran (nous pouvons aller sous Windows pour ça). Mais au lieu de cela, il effectue quelque chose d’erratique ressemblant à un total manque de respect pour le Dock, ou alors remplit l’écran tout entier pour afficher une petite image. Ceci est des plus irritants.

Heureusement, développeurs Cocoa que nous sommes ne manquons pas de recours. Nous pouvons corriger ce qui est déjà mauvais (ce sera cependant corrigé lorsque Mac OS X aura mûri), mais nous ne pouvons pas prendre le temps d’implémenter intelligemment cette fonction. Le fait de faire ceci ajouterait du peaufinement à notre application, ce qui serait reçu par les utilisateurs avec gratitude et plaisir.

Aujourd’hui, je vais vous montrer le support intégré du comportement relatif au zoom de fenêtre en l’implémentant dans l’application de notre dernier article, ImageApp, que vous pouvez télécharger ici.

Zoom Fenêtre

Avant de pouvoir implémenter un zoom fenêtre efficace, il est nécessaire de passer en revue quelques prérequis sur la manière dont ce comportement est géré dans Cocoa. L’idée clé à comprendre à propos des zooms fenêtre tient dans le fait que Cocoa définit deux états d’existence d’une fenêtre.

L’état “standard” d’une fenêtre consiste en une taille et un emplacement définis par l’application. Notre boulot d’aujourd’hui sera d’écrire le code permettant d’obtenir la meilleure taille et le meilleur emplacement de fenêtre en fonction de la taille de l’image à afficher. Lorsque l’utilisateur redimensionne la fenêtre ou change son emplacement de sept pixels ou plus, alors la fenêtre passe dans un état “utilisateur”. Le globe vert bascule entre ces deux états—l’état “standard” défini par l’application, et l’état “utilisateur”, défini par l’utilisateur.

NSWindow est une des nombreuses classes Cocoa dont les instances comporte un objet délégué travaillant pour elles. Souvenez-vous, un délégué n’est qu’un objet qui agit au nom d’un autre objet. Rappelez-vous que dans le précédent article nous avons configuré le nib, IAWindow, qui contenait l’objet NSWindow, pour qu’il soit détenu par IAWindowController.

Quand nous avons créé cet arrangement, nous avons connecté l’outlet de la fenêtre de IAWindowController à la fenêtre. Aujourd’hui nous allons établir une connexion dans la direction opposée. En fait, nous souhaitons connecter l’objet File’s Owner (une instance de IAWindowController) à l’outlet déléguée de la fenêtre. Faites ceci maintenant et nous aurons fait tout ce qu’il y avait à faire pour affecter un délégué à la fenêtre.

Donc, pourquoi tout ce rafus à propos des délégués ? Une des méthodes que NSWindow déclare comme étant implémentée par le délégué est une méthode appelée –windowWillUseStandardFrame:defaultFrame:. Ceci est la méthode magique invoquée dans le délégué par la fenêtre quand l’utilisateur clique sur le bouton de zoom.

Tout bonnement, cette méthode retourne le cadre standard de fenêtre à l’émetteur, qui sera utilisé pour passer la fenêtre en un état standard. Le code que nous écrivons dans cette méthode taillera l’objet NSRect retourné aux dimensions du contenu de la fenêtre. Si nous écrivons cette méthode correctement, nous aurons des utilisateurs contents.

La méthode nous fournit deux arguments que nous pouvons utiliser. Le premier argument est la fenêtre qui envoie le message. Le second argument, le cadre par défaut, est le rect qui nous indique effectivement l’espace maximum utilisable—la largeur la plus grande de la fenêtre. Une partie crucial dans un zoom propre consiste à limiter la taille de la fenêtre à celle du cadre par défaut lorsque le contenu mérite une taille de fenêtre plus grande que l’écran.

Une chose à noter à propos des cadres par défaut est que cela ne correspond pas à la taille de l’écran. La taille du cadre par défaut est plus petite que celle de l’écran pour prendre en compte la barre de menus, le dock et d’autres éléments d’interface. Cela comprend aussi un espace fin situé en bas de l’écran pour permettre de cliquer au travers sur des fenêtres placées derrière la fenêtre active. L’image ci-dessous illustre le bord du cadre par défaut sur l’écran de mon iBook.

Default Frame
Le cadre par défaut pour l’écran de mon iBook.

Nous allons écrire cette méthode en deux parties. Dans la première, notre objectif principal est de redimensionner la fenêtre pour l’adapter à l’image mise à l’échelle. Le code utilisé pour implémenter ceci est presque identique au code de redimensionnent de fenêtre que nous avons écrit quand nous avons appris comment animer un redimensionnement de fenêtre. Dans la deuxième partie, nous allons prendre en considération les restrictions du cadre par défaut. Entre ces deux passages, nous allons observer une autre qualité du fonctionnement des zooms.

Maintenant, le premier passage :

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender
   defaultFrame:(NSRect)defaultFrame {     // récupérer l'origine 'y' de la vue défilante
                                           // pour utilisation dans le calcul de newHeight
   int svOffset = [[[view superview] superview] frame].origin.y;
   NSSize viewSize = [view frame].size;
   float newHeight = viewSize.height + svOffset;
   float newWidth = viewSize.width;
   NSRect stdFrame = [NSWindow contentRectForFrameRect:[sender frame]
                                             styleMask:[sender styleMask]];
   stdFrame.origin.y += stdFrame.size.height;
   stdFrame.origin.y -= newHeight;
   stdFrame.size.height = newHeight;
   stdFrame.size.width = newWidth;
   stdFrame = [NSWindow frameRectForContentRect:stdFrame
                                      styleMask:[sender styleMask]];
   return stdFrame;
}

Comme je l’ai dit avant, l’essentiel de ce code est identique au code de redimensionnement animé de fenêtre dont on a parlé deux articles auparavant. Si vous n’avez pas lu cet article, ou si vous avez une mémoire dans le genre de la mienne, vous souhaiterez peut être le parcourir rapidement.

La première chose à noter à propos de ce code tient dans la variable svOffset, et dans la façon dont nous avons ajouté sa valeur à celle de newHeight. Cet offset n’est rien d’autre, comme vous pouvez le voir dans le code, que l’origine du cadre de la vue défilante dans la vue du contenu de la fenêtre.

Pourquoi s’en préoccuper ? L’idée principale consiste à redimensionner la vue du contenu de la fenêtre à la taille du cadre de la vue de l’image, ce qui est, en fait, la taille de l’image mise à l’échelle. Cependant, il y a plus à faire avec la fenêtre qu’avec la vue de l’image : nous disposons ce petit groupe de contrôle de zoom (sur l’image pas sur la fenêtre) situé en bas de la fenêtre. L’espace occupé par ces contrôles doit être pris en considération au moment de redimensionner la fenêtre.

La chose la plus facile à faire est d’ajouter à la hauteur de la vue du nouveau contenu—newHeight—la taille verticale de l’espace occupés par ces contrôles. Puisqu’ils sont situés en bas de la fenêtre, l’origine ‘y’ de la vue défilante limitant cet espace nous donne une mesure excellente de la taille de l’espace que nous devons maintenir. Voilà donc toute l’histoire de svOffset et de ses effets sur newHeight.

Une chose à noter à propos de cela tient dans la manière dont nous avons obtenu l’objet vue défilante, avec un message -superview envoyé à l’objet retourné par le message -superview transmis à la vue. Cela a été largement discuté dans le dernier article.

La deuxième chose à noter à propos de cette méthode tient dans le fait que plutôt que passer stdFrame comme argument à -setFrame:display:animate:, comme cela fut fait précédemment pour animer le redimensionnement, nous retournons stdFrame à l’émetteur là où l’état standard de la fenêtre sera défini.

Voici donc notre premier coup dans la configuration de l’état standard de la fenêtre. Recompilez ImageApp et lancez la pour tester les changements. Au moment de l’exécution de ImageApp, vous devriez remarquer quelque chose sur le comportement général de notre application. La première chose à remarquer tient dans le fait que la fenêtre a toujours la même taille quand un document est ouvert pour la première fois. Cette taille correspond à celle configurée dans Interface Builder. Idéalement, nous devrions faire en sorte que la taille de la fenêtre corresponde à la taille initiale de l’image.

Une autre chose à noter tient dans le fait que quand l’image est zoomée, la fenêtre ne fait rien pour s’adapter aux changements des informations affichées. Ces deux choses peuvent être réglées avec une simple ligne de code qui tire profit du zoom sur fenêtre.

Quittez l’application et ouvrez IAWindowController.m dans Project Builder. Tout en bas de la méthode -scaleImageTo: ajoutez cette simple ligne de code :

[[self window] zoom:nil];

La méthode -zoom: de NSWindow est la même méthode invoquée lorsque l’utilisateur clique sur le bouton de zoom de la fenêtre. Nous l’invoquons directement en réponse à toute activité liée au zoom, et nous assurons ainsi que l’image affichée l’est au mieux. Effectivement, l’application appuye sur le bouton vert à votre place dès que l’échelle de l’image change. La raison pour laquelle le problème de taille incorrecte de la fenêtre initiale est pris e compte réside dans la ligne de code exécutée dans –windowDidLoad:

[self scaleImageTo:1.0];

En envoyant ce message au moment où l’application ouvre un nouveau document, nous effectuons toujours un zoom sur la fenêtre de façon à ce qu’elle s’adapte à l’image en cours.

Maintenant, compilez et lancez le programme de nouveau, et jetons un oeil sur un autre aspect comportemental. Ouvrez une image et entrez une grande valeur d’échelle, telle que 1000 pourcent, qui rendra l’image plus grande que l’écran. Avez-vous remarqué ce qui s’est passé ? Le mécanisme Cocoa de zoom sur fenêtre sait appréhender le fait que la fenêtre va dépasser la taille de l’écran. Si nous retournons un cadre standard plus large que l’espace utilisable de l’écran, qui exclut la barre de menus et le dock, la fenêtre le saura et se dimensionnera toute seule à la taille de l’espace utilisable.

Donc, étant donné ce comportement intégré, pourquoi devrions-nous tester la taille de stdFrame par rapport à celle du cadre par défaut ? Comparez les deux images ci-dessous. Celle du haut utilise le mécanisme intégré de redimensionnement restrictif ; dans celle du bas, je compare stdFrame par rapport à defaultFrame. Voyez-vous la différence ?


La restriction de taille basée sur le comportement intégré est montrée en haut. La restriction de taille implémentée par nous-mêmes en utilisant le cadre par défaut est montrée en bas.

La différence est subtile, mais importante à mes yeux d’utilisateur. Elle tient dans la présence d’une fine bande (10 pixels approximativement) vide située sous la fenêtre. Le cadre par défaut considère cette aire comme étant hors d’atteinte, tout comme le dock et la barre de menus. L’utilité de ceci est que nous disposons de ce fait d’un espace commode pour cliquer sur les fenêtres ou le bureau situés en arrière-plan. Si vous pensez comme moi que c’est important, alors continuez à lire comment nous allons implémenter notre propre version de restriction de taille basée sur le cadre par défaut.

Lorsque l’on prend en compte le cadre par défaut, il y a deux choses à considérer. D’abord le cas évident où le stdFrame est plus grand que le cadre par défaut. Nous gérons ce cas dans les directions x et y indépendamment. Donc, si le stdFrame est plus large que le cadre par défaut, nous positionnons la largeur de stdFrame à celle de defaultFrame, et l’origine x de stdFrame à celle de defaultFrame. Même chose pour la direction verticale. Si stdFrame est plus haut que defaultFrame, alors nous positionnons la hauteur de stdFrame et le point d’origine y de stdFrame aux mêmes valeurs que defaultFrame.

La deuxième chose à considérer ce sont les situations dans lesquelles la taille de stdFrame n’est pas plus large que le cadre par défaut, mais où une partie de la fenêtre peut être hors de l’écran. Nous gérons ceci en déplaçant stdFrame sur la fenêtre. Jetons un oeil aux modifications apportées à notre méthode pour voir comment tout ceci s’accorde :

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender
   defaultFrame:(NSRect)defaultFrame {
       // obtenir le point d'origine y de la vue défilante pour utilisation          // dans le calcul de newHeight
       float stdX, stdY, stdW, stdH, defX, defY, defW, defH;
       int svOffset = [[[view superview] superview] frame].origin.y;
       NSSize viewSize = [view frame].size;
       float newHeight = viewSize.height + svOffset;
       float newWidth = viewSize.width;
       NSRect stdFrame = [NSWindow contentRectForFrameRect:[sender frame]
                                                 styleMask:[sender styleMask]];
       stdFrame.origin.y += stdFrame.size.height;
       stdFrame.origin.y -= newHeight;
       stdFrame.size.height = newHeight;
       stdFrame.size.width = newWidth;
       stdFrame = [NSWindow frameRectForContentRect:stdFrame
                                          styleMask:[sender styleMask]];
       stdX = stdFrame.origin.x;
       stdY = stdFrame.origin.y;
       stdW = stdFrame.size.width;
       stdH = stdFrame.size.height;
       defX = defaultFrame.origin.x;
       defY = defaultFrame.origin.y;
       defW = defaultFrame.size.width;
       defH = defaultFrame.size.height;
       if ( stdH > defH ) {
           stdFrame.size.height = defH;
           stdFrame.origin.y = defY;
       }
       else
       if ( stdY < defY ) {
           stdFrame.origin.y = defY;
       }
       if ( stdW > defW ) {
           stdFrame.size.width = defW;
           stdFrame.origin.x = defX;
       }
       else
       if ( stdX < defX ) {
           stdFrame.origin.x = defX;
       }
       return stdFrame;
  }

La première chose ajoutée dans cette implémentation par rapport à la précédente est la ligne de déclarations de variables de type float. Elles sont utilisées juste après que la méthode précédente se termine en guise de commodité pour les instructions if qui suivent. Comme vous pouvez le voir, nous assignons à chacune de ces huit variables les valeurs des différentes parties des deux cadres que nous souhaitons comparer.

Ensuite, nous avons deux instructions if, semblable conceptuellement, mais opérant sur les deux dimensions de la fenêtre. D’abord, nous regardons les dimensions verticales des deux cadres. Dans la première instruction if nous testons la hauteur du cadre standard par rapport à celle du cadre par défaut. Si elle est plus grande, alors nous positionnons la hauteur de stdFrame à defH (ce qui représente la hauteur du cadre par défaut), et le point d’origine vertical de stdFrame à celui du cadre par défaut (defY).

Si cette première condition est fausse, nous testons alors que le bas du cadre standard par rapport au bas de l’écran. Si il est plus bas, nous réinitialisons alors le point d’origine du cadre standard en lui donnant la valeur du point d’origine du cadre par défaut (une fois encore, dans la direction y), et nous poursuivons. Nous n’avons pas à nous soucier de la transformation du point d’origine de stdFrame en celui de defaultame (afin d’éviter que le haut de la fenêtre soit plus haut que l’écran) puisque nous avons testé auparavant que la hauteur de stdFrame était inférieure à celle de defaultFrame.

Voici donc la logique qui se cache derrière le cadre par défaut. Ce que nous avons fait pour la direction y est exactement applicable à la direction x, à part que les Y sont remplacés par des X et les H par des W. Poursuivez, compilez ImageApp et lancez le pour essayer ces changements récents.

Maintenant que nous savons comment le zoom sur fenêtre fonctionne sous Cocoa, nous aurons des utilisateurs du logiciel plus heureux. Si cela vous intéresse vous pouvez télécharger le projet contenant les changements que nous avons apportés à ImageApp aujourd’hui. Dans le prochain article, nous allons apprendre comment nous pouvons donner plus de finesse à ImageApp.

Textes originaux en anglais sur O’Reilly : All About the Little Green Glob par Mike Beam

Thierry Programmation Cocoa , , ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire