Manipuler les Images Bitmap ; retour aux Applications de Gestion de Documents
Un des aspects les plus utiles et les plus fonctionnels de Cocoa tient dans son architecture reposant sur des applications de gestion de document. Cela fait un moment que nous avons abordé ce sujet—à l’époque où nous apprenions à construire des applications Cocoa grace à l’application Traitement de textes simples. Cependant, cette visite était un coup d’oeil superficiel qui ne rendait pas justice aux possibilités fournies par cette partie de Cocoa.
L’article d’ajourd’hui est le premier d’une série qui ira plus dans le détail d’un certain nombre d’aspects précédemment négligés relatifs aux fonctions Cocoa se rapportant aux applications de gestion de documents. L’application que nous allons construire en tant que plateforme d’explorartion de ces concepts est un visualisateur d’images (qui deviendra éditeur par la suite) appelé ImageApp.
Des articles à venir permettront d’ajouter des fonctionnallités d’exportation d’images, d’impression, d’annulation et un paquet d’autres fonctions. Puisque nous construisons une application de gestion de graphiques, nous allons aussi en apprendre plus sur les graphiques Cocoa, en particulier ses classes de gestion des images. Ces classes comprennent NSImage, NSImageView, NSImageRep et ses sous-classes, et plus encore.
Pour démarrer, passons d’abord en revue la masse des informations conceptuelles qui va nous fournir le contexte de notre programmation.
Retour sur les Applications de gestion de documents
Dans l’architecture des applications de gestion de documents, trois classes de l’Application Kit priment. Ce sont NSDocument, NSDocumentController et NSWindowController. Chacune de ces classses a un rôle spécifique dans l’architecture des applications de gestion de documents, qui respecte les principes du modèle Model-View-Controller (MVC) (NdT : Voir l’article à ce sujet : Le modèle de conception “Model-View-Controller”).
Rappelez-vous que dans le modèle MVC la fonctionnalité d’une application est divisée en plusieurs classes. Le modèle prototype MVC comporte trois classes—une classe qui modélise les données (le modèle), une classe qui affiche les données (la vue) et une classe qui joue le rôle de médiateur entre le modèle et la vue (le contrôleur). Dans les applications passées, nous avons appris comment implémenter ce modèle en créant une classe contrôleur et en utilisant des classes Cocoa en guise de classes modèle et vue.
L’architecture des applications de gestion de documents adopte le modèle MVC de part son trio de classes et étend quelque peu le principe en créant des dispositions pour deux classes contrôleur. Une classe contrôleur contrôle et gère le modèle de données. C’est le contrôleur de modèle. L’autre contrôleur joue le rôle traditionnel de contrôle de l’interface utilisateur—la vue. C’est le contrôleur de vue.
Sous Cocoa, NSDocument est le contrôleur de modèle et NSWindowController est le contrôleur de vue. Une aute façon d’aborder la chose consiste à considérer que NSDocument détient et gère les objets représentant le modèle de données du document, ce qui dans notre exemple ne sera qu’une instance de NSImage. Une partie du boulot consistant à s’approprier les objets de données revient à savoir comment charger et sauvegarder les données persistentes, ce qui fait partie de ce que maîtrise totalement NSDocument.
NSWindowController en tant que contrôleur de vue est le propriétaire et le contrôleur de tous les objets qui constituent l’interface utilisateur, et en particulier, elle contrôle la manière dont sont affichés les contenus du document. En complément du modèle, les classes qui n’ont pas de rapport avec les documents, telles que NSImage en tant que modèle de données et NSImageView agissant en tant qu’objet visible principal.
NSDocumentController ne répond pas réellement au modèle MVC, ce qui est bien, puisque son objectif est différent dans l’application de la gestion à la présentation des données à l’utilisateur. De manière simple, NSDocumentController gère les documents—elle sait comment créer de nouveaux documents (en tant qu’objet, pas les données d’un document), comment ouvrir des documents et et comment effectuer d’autres actions de niveau applicatif se rapportant aux documents. Cela comprend une connaissance des types de fichiers que l’application peut ouvrir (un rôle de visualisateur), et que l’application peut modifier et sauvegarder (un rôle d’éditeur). Normalement, les développeurs n’ont pas besoin de créer des sous-classes de NSDocumentController.
Tout ceci est très différent de la manière dont nous avons commencé à l’origine à construire des applications de gestion de documents. Vous vous souvenez du simple Editeur de Texte que vous avez construit l’année dernière ? Cette application était construite autour d’une seule classe, MyDocument, qui contenait tout le code de gestion et de contrôle de l’interface utilisateur, au même titre que les données. Non seulement, elle jouait le rôle de contrôleur de modèle, mais elle jouait aussi le rôle de contrôleur de vue. A la lumière des récents propos sur le MVC, c’était une mauvaise conception.
Heureusement, SimpleTextEditor est resté une petite application (enfin, jusqu’à la limite de cet article), et nous ne sous sommes pas retrouvés dans des situations conceptuelles difficiles qui auraient été le résultat d’une pauvre approche. Clairement, si nous avions du modulariser notre code en respectant les principes de la conception orientée objet et les dictates du MVC, nous aurions séparer le code qui contrôle l’interface de celui qui gère le modèle de données. Cela devient excessivement important quand les applications deviennent de plus en plus complexes, ce qui est inévitable, il est donc mieux de démarrer en étant le plus proche de l’état final souhaité.
Avant de continuer, laissez moi dire quelques mots à propos des associations existantes entre les trois classes d’applications de gestion de documents dont nous disposons. Les trois classes NSDocumentController, NSDocument et NSWindowController se rapportent à une autre selon une relation 1-n.
En fait, les applications de gestion de document comportent un objet NSDocumentController qui gère un ou plusieurs objets NSDocument. A leur tour, les objets NSDocument ont un ou plusieurs objets NSDocumentController. De manière similaire, les instances de NSDocument peuvent détenir plusieurs instances de NSWindowController, tandis que tout objet NSWindowController n’a qu’un seul maître NSDocument. Les instances de NSDocument gardent une liste des contrôleurs de fenêtre que, lorsque nous écrivons le code d’utilisation d’un contrôleur de fenêtre personnalisé, nous devons enrichir.
Si vous vous demandez quel type d’application aurait besoin de plusieurs fenêtres (chaque contrôleur de fenêtre a une fenêtre qui lui appartient), pensez à une application de CAD dans laquelle un seul document peut avoir trois ou quatre vues d’un objet, chacun étant affiché dans une fenêtre séparée. Ce type d’interface de document serait implémentée en instanciant le nombre requis de NSWindowControllers individuels et en les joutant à la liste des contrôleurs de fenêtre du document.
C’est donc là que nous en sommes avec les applications de gestion de document. Ma description a été assez schématique, donc si vous souhaitez plus de détails sur ce que j’ai dit, allez jeter un oeil sur la page de la documentation Cocoa principale d’Apple appelée “Application Design for Scripting, Documents, and Undo“; elle se trouve dans la section “Legacy“.
Allons maintenant plus loin et construisons notre application.
Construction de ImageApp
Pour démarrer, lancez Project Builder et créer un nouveau projet de type “Cocoa Document-Based Application”. Intitulez le projet ImageApp. Contraitement à ce que nous avons effectué avec les applications précédentes, nous allons fait assez peu de choses dans Interface Builder (IB). L’interface utilisateur de ImageApp est assez directe—rien de plus qu’une fenêtre avec une vue à défilement pour gérer l’affichage de l’image et un champ texte dans lequel l’utilisateur peut changer le zoom sur le document.
Cependant, avant de construire l’interface, parlons un peu plus de la structure de notre application et écrivons un peu de code. Notre application consistera en trois classes intitulées MyDocument, IAWindowController et IAImageView. Ces classes héritent de NSDocument, NSWindowController et de NSImageView, respectivement. IAWindowController servira la double fonction de contrôleur de fenêtre de MyDocument, en tant que propriétaire et contrôleur de l’interface utilisateur. NSImageView est une classe dont le rôle principal est d’afficher une seule NSImage dans une vue. Nous allons en créer une sous-classe pour ajouter quelques fonctionnalités dont nous verrons l’utilité plus tard.
Passons à la corvée de créer les fichiers de classe pour IAWindowController et IAImageView. Créez IAImageView en créant une sous-classe Objective-C de NSView via File -> New File…. Cela crée une sous-classe de NSView, mais nous avons besoin que IAImageView soit une sous-classe de NSImageView. Cela est rectifié en changeant la ligne @interface de IAImageView.h : à la place de @interface IAImageView : NSView, placez @interface IAImageView : NSImageView. Maintenant créez la sous-classe contrôleur de fenêtre en choisissant la sous-classe Objective-C NSWindowController à partir de l’assistant nouveau fichier et intitulez la IAWindowController.
Implémentation de la Sous-classe de NSDocument
Parce que nous utilisons une sous-classe personnalisée de NSWindowController pour notre contrôleur de fenêtre document, nous devons changer des parties de l’implémentation par défaut de MyDocument de façon à ce que le document charge IAWindowController en tant que contrôleur de fenêtre et interface. Nous allons retirer la méthode -windowNibName et mettre à la place la méthode -makeWindowControllers.
L’objectif de -windowNibName est de retourner le nom du nib contenant l’interface du document. MyDocument utilise ceci lorsqu’il crée le NSWindowController par défaut utilisé pour gérer les fenêtres de document. A la place, nous allons implémenter une méthode de NSDocument appelée -makeWindowControllers:. Cette méthode est appelée quand de nouveaux documents sont créés quand des documents existants sont chargés—en fait, quand une nouvelle instance de la classe document est créée. Tout ce que nous avons à faire ici est d’instancier et initialiser IAWindowController puis l’ajouter à la liste de documents des contrôleurs de fenêtre.
Pour charger avec un NSWindowController un fichier nib contenant l’interface du document, nous utilisons la méthode d’initialisation -initWithWindowNibName:. Cette méthode prend le nom du fichier nib (sans l’extension) comme son unique argument, puis IAWindowController prend soin de charger le nib, effectuant toute initialisation nécessaire et affichant la fenêtre principale du contrôleur de fenêtre.
Avant d’implémenter -makeWindowControllers, changeons le nom du fichier nib de MyDocument.nib en IAWindow.nib, pour mieux refléter l’idée qu’une instance de IAWindowController gérera le nib à la place de MyDocument. Nous verrons dans un moment comment ce concept d’appartenance est implémenté dans Interface Builder.
Donc, nous savons maintenant comment initialiser une instance de IAWindowController; maintenant nous devons savoir comment ajouter ce contrôleur de fenêtre à la liste des documents des contrôleurs de fenêtre. Pour faire ceci, nous utilisons la méthode addWindowController: de NSDocument. Créons et ajoutons un contrôleur de fenêtre au document avec -makeWindowControllers dans MyDocument.m :
- (void)makeWindowControllers {
windowController = [[IAWindowController alloc] initWithWindowNibName:@"IAWindow"];
[self addWindowController:windowController];
}
Comme vous pouvez le voir, windowController est une variable d’instance variable qui doit être déclarée dans MyDocument.h comme :
IAWindowController *windowController;
En plus, vous devez importer IAWindowController.h dans MyDocument.h de façon à ce que le compilateur sache ce qui se passe avec IAWindowController et évite de nous balancer des tonnes d’avertissements.
Retournons à notre méthode. Tout ce que nous avons a consisté à faire un alloc et un init du contrôleur de fenêtre en utilisant la méthode susmentionnée -initWithWindowNibName:, lui passant le nom de notre nib en guise de argument-@"IAWindow". Après ça, nous avons envoyé un message -addWindowController: à elle-même (self) avec windowController comme argument. Ainsi, c’est tout ce que nous avons à faire dans MyDocument pour supporter notre sous-classe NSWindowController.
Si vous devez faire quelques initialisations dans MyDocument directement avant ou après que le contrôleur de fenêtre ne charge le nib alors NSDocument nous donne l’option de surpasser -windowControllerWillLoadNib: et -windowControllerDidLoadNib:. Comme leurs noms l’indiquent, ces méthodes sont appelée avant et après le chargement du nib, respectivement.
Puisque notre modèle de donnée sera une instance de NSImage, et que dans un moment nous allons écrire du code pour créer cette objet quand un fichier est chargé, il serait bien que l’on parle un peu de NSImage.
NSImage
NSImage est la machine à tout faire de la gestion d’image sous Cocoa. NSImage fournit aux développeurs un front-end commode et facile d’usage en liaison avec un back-end flexible et puissant constitué de plusieurs classes de l’AppKit. Une des idées clé qui se cache derrière NSImage repose sur les représentations de l’image. Imaginez NSImage comme quelque chose qui fournirait un concept de haut niveau et plus abstrait qu’une image, tandis que les représentations de l’image via NSImageRep et ses sous-classes fournissent une interface plus spécialisée aux différentes manières qu’ont les données image d’exister.
Tout ceci est plus compréhenseible si vous jetez un oeil aux noms des nombreuses sous-classes de NSImageRep : NSBitmapImageRep, NSPDFImageRep, NSEPSImageRep, NSPictImageRep, NSCustomImageRep et NSCachedImageRep. Chacune de ces sous-classes sait comment effectuer le rendu d’images selon différents formats, et la diversité de ces classes est cachée derrière la simplicité de NSImage.
Une autre idée clé cachée derrière NSImage tient dans le fait que NSImage garde de multiples représentations pour une image. En faisant ainsi NSImage est capable de fournir à n’importe quel périphérique de rendu graphique (écrans en millier de couleurs, écrans en million de couleurs, imprimantes, traceuses, et autres) la représentation la plus adaptée à l’affichage de l’image sur ce périphérique particulier. Nous verrons dans les deux prochains articles comment nous pouvons exploiter encore plus cette structure pour réaliser des tâches variées.
En ce qui cocerne notre objectif actuel, tout ce que nous avons à faire est de créer une NSImage à partir des données d’un fichier image. On arrive à cela en utilisant l’initialisateur -initWithData:, ce qui est commode puisqu’avec -loadDataRepresentation:ofType: c’est un objet NSData qui est passé en premier argument. La donnée passée dans cet argument est celle initialisée à partir du fichier sélectionné dans la fenêtre d’ouverture par l’utilisateur.
Fin de constitution de MyDocument
L’objet image du document sera affecté à une variable d’instance nommée activeImage; ajoutons donc ceci à MyDocument.h:
NSImage *activeImage;
L'implémentation de -loadDataRepresentation:ofType: est effectuée de la mnière suivante :
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType {
activeImage = [[NSImage alloc] initWithData:data];
return (activeImage != nil);
}
Notez que la valeur de retour est le résultat d’une comparaison entre activeImage et nil. Si -initWithData: est incapable d’initialiser le NSImage avec les données fournies, alors l’objet NSImage est libéré et la valeur nil est retournée. Donc, en comparant activeImage à nil nous avons une indication du succès de l’opération d’ouverture du fichier que nous pouvons utiliser comme valeur de retour de la méthode.
Finalement, ajoutons une méthode -activeImage pour donner à d’autres objets un accès à l’objet image du document :
- (NSImage *)activeImage { return activeImage; }
Vous devrez aussi déclarer cette méthode dans MyDocument.h de façon à ce que le compilateur soit au courant de sa présence.
Puisqu’ImageApp est une application de visualisation, nous n’allons pas implémenter -dataRepresentationOfType:; cela sera fait dans le prochain article pour la satisfaction de tout le monde. Avec cela, nous avons mis en place une implémentation squelette requise pour ouvrir des documents, et pour configurer le contrôleur de fenêtre de document. Passons à la construction de l’interface et au codage de IAWindowController.
IAWindowController et l’Interface
Les NSWindowControllers ont la responsabilité de détenir et de contrôler une fenêtre. Des sous-classes peuvent étendre cette fonctionnalité en surpassant les méthodes de façon à donner à la fenêtre un style personnifié, un mécanisme de zoom intelligent par exemple, et d’autres choses. Comme nous le savons maintenant, les sous-classes de NSWindowController au sein des applications de gestion de document ont une charge supplémentaire, celle de gérer l’interface utilisateur associée au document.
Dans cette gestion de l’interface utilisateur, IAWindowController a deux responsabilités. La première est de contrôler la présentation d’une image dans laquelle les données du document sont affichées. La seconde est de prendre en compte le réglage du zoom effectué par l’utilisateur à partir d’un contrôle d’interface et d’indiquer à la vue de l’image d’adapter la taille de l’image en rapport, ce qui réprésente une fonction standard dans la plupart des applications graphiques que je connaisse. Ainsi, si l’utilisateur veut voir l’image dans sa totalité, il saisit 100%, s’il souhaite n’en voir que la moitié, il saisit 50%, et ainsi de suite.
Le mécanisme de mise à l’échelle sera implémenté par IAImageView—IAWindowController agit en tant qu’intermédiaire entre le contrôle d’interface utilisateur et la vue. La seconde responsabilité est d’obtenir une image de document et de la régler de façon à ce qu’elle soit affichée dans la vue, le tout dans sa partie consacrée aux initialisations.
Avant de pouvoir passer à l’acte dans Interface Builder, nous devons déclarer quelques actions et outlets dans IAWindowController sous Project Builder. Dans notre cas, nous avons une action, -changeScale:, et deux outlets—une pour la vue d’image et une autre pour le contrôle de zoom. Après que tout ait été dit et fait IAWindowController.h devrait ressembler à ceci :
#import
@class IAImageView;
@interface IAWindowController : NSWindowController {
IBOutlet IAImageView *view;
IBOutlet NSTextField *zoomControl;
}
- (IBAction)changeScale:(id)sender;
@end
Notez que plutôt que d’importer l’interface dans IAWindowController, nous utilisons la directive Objective-C de compilation @class pour indiquer que IAImageView est une classe. Cela permet d’économiser du temps dans les gros projets puisque c’est un fichier de moins à importer.
En plus, nous évitons les avertissements du compilateur s’il n’avait pas su ce qu’était IAImageView (notez que nous devons toujours importer IAImageView.h dans IAWindowController.m). Nous aurions aussi pu faire de même dans MyDocument.h—en ajoutant @class IAWindowController plutôt que d’importer le fichier en-tête.
Avec une interface configurée pour IAWindowController sous Project Builder nous pouvons maintenant l’importer dans notre nib et construire l’interface autour. Nous devons aussi importer l’interface de IAImageView. Ouvrez IAWindow.nib et glisser-déposez les fichiers en-têtes IAWindowController.h et IAImageView.h de Project Builder vers la fenêtre du nib. Nous sommes maintenant prêts à travailler sur l’interface.
Construction de l’Interface
La façon d’établir les connexions aux outlets et aux actions de IAWindowController consiste à changer la classe du Possesseur de Fichier dans le nib. A ce moment sa classe est réglée sur MyDocument; nous allons la changer en IAWindowController. Cela veut dire que nous transférons la propriété du nib de MyDocument vers IAWindowController. Faites ceci en sélectionnnat l’icône File’s Owner et en ouvrant le panneau des informations d’attributs. Dans ce panneau, vous pouvez choisir IAWindowController dans la liste, et avec cela, nos outlets et nos actions de IAWindowController peuvent être connectées, par le biais du File’s Owner. Terrible.
A part faire devenir IAWindowController le File’s Owner, nous devons établir une connexion clé entre la fenêtre et le File’s Owner. Glisser-déposez une connexion entre le File’s Owner et l’icône de la fenêtre, et si elle n’est pas déjà connectée, établissez une connexion vers la fenêtre outlet. Cela fait effectivement devenir IAWindowController le possesseur de la fenêtre.
Maintenant, effacez tout texte présent dans la fenêtre et glisser-déposez un objet NSImageView sur la fenêtre. Cela se trouve dans la partie supérieure gauche de la palette “Cocoa-Others“, avec une image de montage en guise d’icône. La vue d’image étant en place dans la fenêtre, nous souhaitons maintenant changer la bordure de la vue de son style actuel à un style sans bordure. Nous devons aussi changer le comportement de mise à l’échelle en le passant de “Proportionally” à “To Fit” (ajuster). Tout cela se fait dans le panneau des informations d’attributs. Enfin, nous changeons la classe de l’image de NSImageView en IAImageView à partir du panneau “custom class” (Commande-5).
L’étape suivante consiste à placer la vue d’image à l’intérieur d’une vue à défilement. Une vue à défilement est une sous-classe de NSView qui affiche une autre vue, mais qui prend soin des contenus importants en fournissant des barres de défilement (instances de NSScroller, appelées des “scrollers”) pour afficher les différentes parties de la vue intérieure, qui dans notre cas est notre vue d’image. Pour faire en sorte que notre vue d’image soit une “sous-vue” de notre vue à défilement nous sélectionnons simplement l’objet vue d’image puis nous sélectionnons Layout -> Make subviews of -> Scroll View.
Remarquez comment le container de vue est scotché dans le coin inférieure gauche de la vue à défilement. Cela provient du fait qu’il est supposé que la taille de la vue sera changée programmatiquement, et que donc son emplacement actuel dans IB n’est pas important. Nous souhaitons aussi nous assurer que les barres de support et les ressorts de la vue à défilement sont configurés de façon à ce que la taille de la vue à défilement soit flexible et s’adapte au redimensionnement de la fenêtre, comme illustré dans l’image ci-dessous.
![]() Configurez les barres de support et les ressorts de la vue à défilement comme je l’ai fait ici. |
Finalement, ajoutez un Zoom : placez un champ texte avec son intitulé sous la vue à défilement, et arrangez le tout pour que cela ressemble à ce que nous avons ci-dessous :
![]() Voici comment est arrangée mon interface ; faites la vôtre de la même façon. La bordure de la vue image n’est montrée que pour vous indiquer sa présence ; ce sera effectivement une vue image sans bordure. |
La dernière chose à faire avant d’établir les connexions consiste à attacher un formatteur de nombre au champ texte. Un formatteur de nombre est une instance de NSNumberFormatter qui définit la manière dont seront affichés les nombres dans un champ texte. Pour notre contrôleur de zoom nous souhaitons qu’un pourcentage soit affiché.
|
L’objet NSNumberFormatter dans la palette “Cocoa-Others”. |
Pour attacher un NSNumberFormatter à un champ texte, faites glisser à partir de la palette Cocoa-Views l’objet doté d’un signe dollar et d’une flèche au dessus du champ texte. Cette action attache le formatteur au champ texte.
Pour voir et modifier les options du formatteur, sélectionnez le champ texte auquel il est attaché (le seul de notre fenêtre dans notre cas) et pressez Commande-6. Cela provoquera l’apparition du panneau Info relatif aux options du NSNumberFormatter. Dans ce panneau, vous trouverez une liste des nombreux mode d’affichage des nombres négatifs et positifs. Sélectionnez celui qui va de 100% à -100%.
Les formatteurs constituent une classe d’objets bien pratiques. Toute ce que l’utilisateur a à faire est de taper 85 et “85%” est affiché. Les formatteurs sont aussi capables d’effectuer des arrondis ; si l’utilisateur saisit 12,3 alors “12%” s’affiche. Etant donné que nous avons complètement configuré le formatteur dans IB, nous pouvons effectuer un test de cette fonction en lançant l’interface (Commande-R).
Pour en finir, effectuons quelques connexions entre le File’s Owner et l’interface. Faites glisser un cable entre le File’s Owner et l’objet IAImageView situé dans la vue à défilement et connectez le à la vue. De la même manière, établissez une connexion entre le File’s Owner et le champ texte (via l’outlet zoomControl), et entre le champ texte et l’action changeScale: du File’s Owner.
Voilà, ceci est notre interface ; rien de plus à ajouter.
Implémentation de la Classe
Passons à l’implémentation de IAWindowController avec notre action -changeScale:. Cette action fonctionnera aux côtés d’une autre méthode de IAWindowController intitulée -scaleImageTo:. Le but de -scaleImageTo: est de gérer les actions interface de plus haut niveau relatives à la mise à l’échelle, tandis que IAImageView s’ocupera du mécanisme de mise à l’échelle. -scaleImageTo: effectue aussi des contrôles d’erreur pour être sûr que le facteur de mise à l’échelle est positif et non nul, ce qui est un minimum en matière de zoom.
A part cette implémentation nous devons déclarer une autre variable d’instance dans IAWindowController.h qui stockera la valeur du zoom actuel :
float scale;
Cette variable servira à garder un zoom consistant entre plusieurs opérations appliquées à l’image (qui seront ajoutées dans de futurs articles). Jetons un oeil à l’implémentation de -scaleImageTo: avant de passer à -changeScale:.
- (void)scaleImageTo:(float)_scale {
if ( _scale > 0 ) scale = _scale;
[view scaleFrameBy:scale];
[zoomControl setFloatValue:(scale * 100)];
}
La première chose que nous faisons est de tester si la valeur de du facteur de zoom _scale est supérieure à 0 ; si tel est le cas alors le facteur de zoom est valide et nous positionnons la valeur de la variable d’instance à celle de l’argument. Sinon, alors l’échelle reste inchangée et le reste de la méthode est appliqué en utilisant l’ancienne valeur de zoom. Puis nous envoyons un message -scaleFrameBy: vers la vue avec “scale” en argument. Notez que -scaleFrameBy: est une méthode de notre conception, pas une méthode fournie par NSImageView.
Finalement, nous positionnons la valeur flottante du champ texte zoomControl à scale * 100. Nous multiplions par 100 parce que scale est un facteur pas un pourcentage, que le champ texte affiche. La raison pour laquelle nous donnons une valeur au champ texte—malgré le fait que l’échelle que nous réglons soit issue de ce champ texte et que rien de va changer—tient dans le fait qu’il puisse y avoir des situations où -scaleImageTo: est invoquée par quelque chose d’autre que l’action de l’utilisateur sur ce champ texte. Dans ces situations nous souhaitons conserver une cohérence entre l’affichage sur l’interface (le pourcentage de zoom) et l’état de l’application (la valeur de la variable d’instance scale).
Maintenant, de retour à l’action -changeScale:, nous avons l’implémentation suivante :
- (IBAction)changeScale:(id)sender {
[self scaleImageTo:([sender floatValue] / 100.0)];
}
Encore, nous divisons par 100 pour les mêmes raisons que nous avions multiplié par 100 au-dessus—le contrôle fournit un pourcentage et -scaleImageBy: attend un facteur.
La dernière chose que nous souhaitons faire dans IAWindowController est d’implémenter quelques formes de code d’initialisations. Dans les sous-classes NSWindowController, nous pouvons utiliser la méthode -windowDidLoad pour l’initialisation à peu près de la même manière que nous avons utilisé -awakeFromNib dans le but d’effectuer des initialisations.
Les contrôleurs de fenêtre sont créés quand les documents sont ouverts et que les données d’un document sont chargées. Donc, une chose que nouos souhaitons faire dans cette méthode consiste à récupérer l’image du document et l’afficher. Nous y arrivons en utilisant la méthode -activeImage de MyDocument, et en passant l’image retournée à la vue pour affichage. L’autre chose que nous souhaitons faire consiste à positionner le facteur initial du zoom sur les images quand elle sont affichées pour la première fois. Voici à quoi -windowDidLoad ressemble :
- (void)windowDidLoad {
NSImage *image = [[self document] activeImage];
[view setImage:image];
[self scaleImageTo:1.0];
}
Remarquez comment dans la première ligne nous pouvons accéder l’objet document qui détient le contrôleur de fenêtre en envoyant un message -document à self. Ensuite nous envoyons l’image nouvellement obtenue vers la vue image en utilisant la méthode -setImage:. Finalement, nous effectuons une opération de mise à l’échelle initiale pour configuer le zoom à 100%.
Pour le lecteur curieux, tout comme il y a -windowControllerDidLoadNib: et -windowControllerWillLoadNib: dans NSDocument, NSWindowController a une -windowWillLoad en complément de la méthode -windowDidLoad.
NSImageView—Tangente rapide
Maintenant que nos propos sur IAWindowController sont terminés, passons à une discussion d’IAImageView, avec un arrêt rapide pour jeter un oeil à sa classe parente, NSImageView. NSImageView est une sous-classe directe de NSControl, qui est une sous-classe de NSView. Donc, une vue d’image est à la fois une vue, dont on en connaît un peu à propos, et un contrôle, dont on en connaît aussi un peu. Avec ceci, nous sommes déjà en terrain familier puisque nous connaissons un peu NSControl et NSView.
NSImageView ajoute seulement 10 méthodes à NSControl. Le boulot de NSImageView est d’afficher NSImage à l’intérieur d’un cadre NSView (NSImage + NSView = NSImageView). En plus de ce job, NSImageView laisse le développeur définir comment l’image devra se mettre à l’échelle pour remplir le cadre de la vue, tout comme la manière dont l’image sera alignée à l’interieur du cadre. Le comportement par défaut fait que la taille de l’image sera proportionnelle à l’espace de la vue et que l’image sera alignée par le centre dans le cadre.
Une autre chose que nous obtenons avec NSImageView est la possibilité de faire glisser des images dans la vue pour qu’elles s’affichent. Ce comportement ne sera pas très utile dans notre cas, mais je n’ai aucun doute que ça le sera pour beaucoup de gens dans différentes applications. NSImageView nous permet de définir le style de bordure décorative autour de la vue image. On fait alors référence au style du cadre, mais je suis réfractaire au fait de l’appeler un cadre puisque nous allons parler un peu des rectangles de cadres pour les vues. Finalement, il y a des méthodes qui nous permettent de configurer l’image programmatiquement pour qu’elle s’affiche dans la vue, tout comme de récupérer cet objet image. Nous allons utiliser ces méthodes plus tard.
Il n’est pas fréquent que l’on ait un synopsis complet d’une classe, mais NSImageView est d’un abord direct de part les fonctionnalités qu’elle ajoute à ses classes parent. Voilà pour NSImageView.
Implémentation de IAImageView
Alors que nous progressons avec notre écriture de code nous arrivons au point d’implémenter IAImageView—si proche de la fin ! Nous savons déjà de part notre travail avec IAWindowController que IAImageView doit définir une méthode appelée -scaleFrameBy:. Comme déjà indiqué ailleurs, cette méthode contient le mécanisme de l’opération de mise à l’échelle (c’est en fait un mécanisme très simple).
Pour implémenter la mise à l’échelle nous exploitons le comportent correspondant de la vue image que nous avons configuré dans Interface Builder pour redimensionner automatiquement l’image pour qu’elle s’adapte au cadre de la vue. Donc si le cadre de la vue de l’image est plus large que la taille originelle de l’image, alors la vue image redimensionnera l’image pour qu’elle remplisse le cadre. Même chose pour un cadre plus petit que l’image. Ainsi, en contrôlant la taille du cadre de la vue de l’image, nous pouvons contrôler la taille de l’image affichée. Cela mène à l’implémentation aisée d’un comportement de mise à l’échelle. Tout ce que nous devons faire dans la méthode de mise à l’échelle consiste à redimensionner le cadre de la vue en fonction de la taille de l’image, et en fonction du facteur de zoom saisi.
Cela sera effectué en obtenant d’abord le NSSize de l’image, puis en appliquant une transformation affine de mise à l’échelle à cette taille. La taille redimensionnée est alors utilisée pour positionner la taille du cadre de la vue de l’image, et nous emballons le tout en indiquant à la vue de se redessiner. En d’autres mots :
- (void)scaleFrameBy:(float)scale {
NSSize imageSize = [[self image] size];
NSAffineTransform *at = [NSAffineTransform transform];
[at scaleBy:scale];
[self setFrameSize:[at transformSize:imageSize]];
[self setNeedsDisplay:YES];
}
Dans la première ligne nous créons une variable NSSize et positionnons sa valeur à la taille retournée par un -size message à la vue de l’image, qui est accédée en envoyant une méthode -image à self.
Ensuite, nous créons une instance de NSAffineTransform et lui envoyons un message -scaleBy: avec scale comme argument de cette méthode. La méthode -scaleFrameBy: se termine avec un message emboîté dans lequel nous transformons la NSSize imageSize en fonction de notre affine transform, en utilisant -transformSize:, puis nous nous servons de la valeur retournée par cette méthode—la taille redimensionnée—en tant qu’argument du message -setFrameSize: envoyé à self. Maintenant, la prochaine fois que la vue sera redessinée l’image sera affichée de manière à remplir le cadre redimensionné de la vue. Pour terminer, nous indiquons à la vue de redessiner son contenu de façon à ce que l’image soit affichée pour remplir le nouveau cadre.
Cette méthode aussi doit être déclarée dans le fichier en-tête d’IAImageView.
Configuration des Types de Fichier Document dans Project Builder
Avant d’avoir une application de visualisation d’images qui fonctionne nous devons indiquer à Project Builder quels types de fichier seront supportés par ImageApp. L’endroit où effectuer ces changements est placé sous l’onglet Targets, et dans Targets sous Application Settings. Dans cette vue, faites défiler vers le bas jusqu’à la section intitulée Document Types. Là, vous apercevez un tableau et différents champs de saisie d’informations relatives aux types de documents. Le tableau montre les types de document déjà supportés par l’application.
![]() Ici nous changeons les types de document supportés par l’application. |
La lecture de la documentation relative à NSImage révèle que cette classe supporte pas mal de formats de fichier image tels que : JPEG, GIF, PNG, TIFF, PICT, PDF, BMP, EPS, et les données image brut non balisées.
Séectionnez l’entrée par défaut située dans le tableau Document Types et effectuons quelques changements. Dans les champs situés en dessous nous voulons changer le nom en JPEG—le nom peut être ce que vous voulez, mais il est mieux d’en mettre un qui soit cohérent. Sous Extensions listez les extensions de fichier possibles qui peuvent être rencontrées pour un JPEG. Tapez “jpg jpeg JPG JPEG” (avec ou sans guillements, cela n’a pas d’importance). Un espace sépare les différentes extensions.
Maintenant, sous OS Types nous souhaitons placer les code type à quatre lettres de JPEG, qui est JPEG. Enfin, assurez-vous que la classe de document est MyDocument, et que le rôle est Viewer. Pour valider les changements, cliquez sur le bouton Change.
Nous allons maintenant saisir les autres types de fichier supportés, effectuez donc les changements illsutrés dans le tableau ci-dessous pour chaque type de fichier puis cliquez sur le bouton Add plutôt que sur le bouton Change. Le rôle de ces types sera Viewer et le classe de document sera, bien sûr, MyDocument.
| Nom | Extensions | OS Type |
| JPEG | jpg jpeg JPG JPEG | JPEG |
| GIF | GIF gif | GIFf |
| PNG | png PNG | PNGf |
| TIFF | tif tiff TIF TIFF | TIFF |
| PICT | pct pict PCT PICT | PICT |
| pdf PDF | “PDF “ | |
| BMP | bmp BMP | “BMP “ |
| EPS | eps EPS | “EPSF” |
Les deux types entre guillements sont ceux qui se terminent par un blanc. Les types sont codés sur quatre caractères et ceux relatifs à PDF et BMP ont un espace en guise de quatrième caractère. Nous n’allons pas ajouter le support des données image brut du fait que cela nécessiterait que l’utilisateur saisisse des informations relatives à l’image avant qu’elle puisse être affichée, et nous n’avons pas une interface propice à ce genre de chose.
A propos, si vous êtes curieux de savoir comment déterminer l’OS Type d’une fichier en particulier, Apple fournit avec l’installation des outils de développement un utilitaire à ligne de commande situé dans /Developer/Tools appelé GetFileInfo. Quand vous lancez cet outil en passant un nom de fichier comme argument, il affiche des informations se rapportant à ce fichier, y compris le code type—c’est ainsi que j’ai déterminé les codes type de la liste ci-dessus.
Quand Project Builder compile une application les informations contenues dans la table Document Types sont placées dans le fichier Info.plist, qui est contenu dans le répertoire Contents du bundle de votre application. Si, après compilation et lancement de votre application, vous constatez que vous ne pouvez pas ouvrir ces types de fichier, essayez d’effectuer une construction propre de votre projet en cliquant sur l’icône en forme de balai situé sur la barre d’outils puis en construisant de nouveau votre projet. Cela provoquera l’effacement de votre ancien Info.plist et la création d’un nouveau.
Finalement, nous voilà avec une application de visualisation d’image. Après un rapide compile and run vous devriez être en mesure d’ouvrir les formats de fichier indiqués, de zoomer et de faire défiler l’image.
Avant de fermer boutique, il y a une dernière chose que j’aimerais rajouter. Ce que nous allons faire maintenant consiste à ajouter une autre fonctionnalité à notre application qui rendra l’expérience utilisateur un peu plus appréciable. Nous allons ajouter quelques lignes de code qui effectuerons une sorte de validation sur les barres de défilement de la vue. Passons à ceci maintenant.
Les barres de défilement
Le truc avec les vue défilante c’est que par défaut les barres de défilement sont toujours présentes même si le contenu de la vue est significativement plus petit que la vue elle-même.
Ce que nous allons faire maintenant consiste à implémenter quelques lignes de code dont le but sera de comparer la taille de la vue à celle de la vue défilante et, en fonction de cette comparaison, masquer ou afficher les barres de défilement. Cette validation sera effectuée dans IAImageView, et le nom de cette méthode sera -validateScrollers. Nous allons inviquer cette méthode avant de faire quoi que ce soit dans -drawRect:. Donc, de retour à -drawRect: dans IAImageView, nous ajoutons :
- (void)drawRect:(NSRect)rect {
[self validateScrollers];
[super drawRect:rect];
}
Lorsque nous surpassons -drawRect: dans une sous-classe de NSImageView nous devons être sûr d’envoyer un message -drawRect: similaire vers super pour que le NSImagePart de IAImageView ait une chance d’effectuer le rendu de l’image.
Donc, les principes de -validateScrollers sont assez faciles. Si la taille du cadre de la vue image est plus grande que la largeur de la vue défilante, alors nous affichons la barre de défilement horizontale. Sinon, nous masquons cette barre. Même principe pour la barre de défilement verticale : Si la hauteur du cadre de la vue image est plus grande que la hauteur du cadre de la vue défilante, nous affichons la barre de défilement—sinon, nous masquons la barre. Nous pouvons déterminer que la vue défilante aura ou pas une barre de défilement horizontale ou veticale en envoyant des messages -setHasHorizontalScroller: ou -setHasVerticalScroller:, respectivement, qui prennent des arguments de type booléen. Jetons maintenant un oeil au code :
- (void)validateScrollers {
NSScrollView *scrollView = [[self superview] superview];
NSSize selfSize = [self frame].size;
NSSize sViewSize = [scrollView frame].size;
BOOL hFlag = selfSize.width > sViewSize.width;
BOOL vFlag = selfSize.height > sViewSize.height;
[scrollView setHasHorizontalScroller:hFlag];
[scrollView setHasVerticalScroller:vFlag];
}
Dans la première ligne de cette méthode, nous accédons à la vue défilante en envoyant un message superview à self (IAImageView), puis un autre message superview à la vue retournée par le premier message superview. Qu’est ce que superview, me demandez-vous ? Dans Cocoa, les vues placées dans des fenêtres sont arrangées selon une hiérarchie spaciale. En fait, une vue appartient à une superview, et détient une ou plusieurs subviews.
Dans notre application, IAImageView est située tout en bas de la hiérarchie, et la vue du contenu de la fenêtre est située en haut de la hiérarchie. La superview de IAImageView est une instance de NSClippingView, dont la superview est la vue défilante. La superview de la vue défilante est la vue du contenu de la fenêtre.
Mais là n’est pas toute l’histoire. Les NSScrollViews, en plus d’avoir une NSClipView en guise de subview, ont des NSScrollers, qui descendent de NSView. Donc, la vue défilante a trois subviews—les deux barres de défilement et la vue du contour. De la même façon, la vue du contenu de la fenêtre a deux ou plusieurs subviews en plus de la vue défilante ; ce sont les deux champs texte situés au bas de la fenêtre qui constituent notre contrôle du zoom.
L’image ci-dessous montre une représentation schématique de la hiérarchie de notre fenêtre document.
![]() La hiérarchie des vues de la fenêtre de notre application. |
Ainsi, en invoquant deux fois superview, nous passons deux vues au dessus de IAImageView dans la hiérarchie pour récupérer la vue défilante.
En poursuivant le code, nous récupérons la taille des cadres de la vue et de la vue défilante. Dans les deux lignes qui suivent nous avons deux variables booléennes, hFlag et vFlag, dont la valeur résulte de la comparaison montrée sur la partie droite de l’opération d’assignation.
De manière essentielle, si la largeur du cadre de la vue est plus grande que celle du cadre de la vue défilante, alors hFlag vaut YES. Même chose pour vFlag dans la direction verticale. Ces deux variables booléennes sont alors passées comme arguments aux méthodes -setHasHorizontalScroller: et -setHasVerticalScroller:. Et tout ce qu’il y a à faire.
En appelant [self validateScrollers] dans drawRect:, nous aurons des barres de défilement qui apparaissent et qui disparaissent en temps réel au moment où le contenu de la vue est redessiné.
Conclusion
L’article d’aujourd’hui est un long article. Voici le dossier du projet ImageApp pour vous aider à digérer cette masse d’informations. J’avais l’intention de vous en dire beaucoup plus, mais au fur et à mesure que j’écrivais je réalisais que j’avais créé un monstre. En fait, le concept qui m’a poussé à écrire cette application, et sur lequel je souhaitais écrire un article, ne verra maintenant pas le jour avant les deux autres à venir. Donc, bien que cet article présente—à mon avis—quelques intéressantes perles de sagesse, ce n’était qu’un point de départ pour d’autres choses à venir encore plus intéressantes.
Dans le prochain article, vous pouvez attendre avec impatience l’implémentation d’une assez chouette méthode de sauvegarde, plein de bonnes informations relatives aux images et aux réprésentations d’images. Nous implémenterons aussi quelques comportements intelligents de zoom sur fenêtre et même d’impression. A la prochaine.

Textes originaux en anglais sur O’Reilly : Manipuler les Images Bitmap ; Retour aux applications de gestion des documents par Mike Beam




Chargement
Commentaires récents