Une Introduction à RubyCocoa, 2ème Partie
Créer le squelette du code
La première chose que nous devons faire avant de coder notre application est d’ajouter un nouveau fichier Ruby.
Nous devons ajouter une classe qui hérite de la classe NSObject.
Pour cela, control-cliquez sur le répertoire “Classes” en dessous de “Groups & Files” dans Xcode.
Sélectionnez “Add->New File…” depuis le menu popup et, ensuite, sélectionnez la sous-classe NSObject Ruby en-dessous de Ruby dans la boîte de dialogue “New File”.
Cliquez sur “Next”, et intitulez votre nouvelle classe Ruby Controller.rb, ou le nom que vous avez donné dans Interface Builder (IB).
Puis ajoutez la nouvelle classe dans le projet en cours et la target, puis cliquez sur “Finish”.
Une fois cela effectué, vous devriez avoir le squelette d’une classe Ruby qui hérite de la classe NSObject que l’on trouve dans le module RubyCocoa OSX.
Ensuite, nous allons ajouter nos outlets à la nouvelle classe.
Nous ajoutons les outlets à la classe Ruby en appelant la méthode ns_outlets et en lui passant la liste des symboles représentant les noms des outlets que nous souhaitons créer.
Cela ressemble à l’utilisation de la méthode attr_writer utilisée en Ruby pour créer les méthodes d’initialisation des variables d’instance.
Vous pourriez remplacer ns_outlets par attr_writer et l’application pourrait fonctionner sans incident.
Il est également possible d’utiliser l’alias ib_outlets comme je le fais dans mon code.
Après avoir ajouté les cinq outlets à notre classe Ruby, nous devons ajouter les méthodes qui correspondent aux actions que nous avons créées dans IB.
Nous le faisons en créant une méthode pour chaque action en utilisant le même nom que l’action que nous avons créée et qui prend un paramètre pour l’expéditeur du message.
Une fois que nous avons ajouté les cinq actions, nous devrions avoir un squelette de code qui ressemble à ce qui suit :
require 'osx/cocoa'
class Controller < OSX::NSObject
include OSX
ib_outlets :archiveFile, :fileTypeView,
:fileTableView, :fileType, :mainWindow
def addFile(sender)
puts "addFile method"
end
def removeFile(sender)
puts "removeFile method"
end
def browseForArchive(sender)
puts "browseForArchive method"
end
def createArchive(sender)
puts "createArchive method"
end
def extractArchive(sender)
puts "extractArchive method"
end
end
Vous devriez remarquer deux choses, dans le code ci-dessus, qui sont différentes de ce que je vous ai déjà dit d’ajouter dans votre nouveau fichier.
Premièrement, j’ai ajouté une ligne à chaque méthode qui écrit dans la console.
C’est uniquement pour nos tests.
Si vous le voulez, vous pouvez lancer votre application et essayer chaque bouton pour vous assurer que chacun fonctionne correctement.
Ensuite, assurez-vous de supprimer chaque instruction puts pour qu’une fois terminée, notre application n’écrive plus ces informations de test sur la console.
Ensuite, vous remarquerez que j’ai ajouté une ligne au début de la définition de la classe qui inclut le module OSX du framework RubyCocoa.
Si vous vous rappelez le premier article, j’ai indiqué que toutes les classes du framework RubyCocoa se trouvent dans le module OSX.
Ce que j’ai fait ici, c’est l’inclure à notre classe pour intégrer (mix-in est la façon dont Ruby résoud le problème de l’héritage multiple) toutes ces classes, méthodes et variables à notre classe, nous donnant un accès direct à tout ce que le framework offre.
Vous avez donc testé votre nouvelle application RubyCocoa et tout semble fonctionner correctement jusque-là n’est-ce pas ?
Et vous avez également supprimé du code les commandes puts.
Bien, vous avez tout ce qu’il faut pour la dernière partie dans laquelle nous allons ajouter le reste de la logique de la classe pour obtenir une application fonctionnellement complète.
Ajoutons le corps
Nous commencerons par mettre en place les méthodes qui seront utilisées pour manipuler notre objet NSTableView.
Les deux méthodes que nous avons besoin d’implémenter pour cela sont addFile et removeFile.
Cependant, avant que ces méthodes ne puissent fonctionner correctement, nous devons mettre en place un objet qui jouera le rôle de notre source de données pour l’instance de NSTableView.
Notre application est suffisamment simple pour que nous n’ayons pas besoin de créer une structure de données séparée pour être la source de données de notre NSTableView.
Au lieu de cela, notre Controller sera utilisé en tant que tel.
Pour cela, nous devons implémenter un minimum de deux méthode dans notre classe Controller pour lui permettre de jouer ce rôle.
Ces deux méthode sont numberOfRowsInTableView qui retourne le nombre de fichiers dans notre table et tableView_objectValueForTableColumn_row qui retourne la valeur trouvée dans la cellule en cours de sélection dans notre table.
L’implémentation pour ces deux méthodes de trouve ci-dessous.
###
# numberOfRowsInTableView
# Returns the number of records in the table.
# This must be implemented by whatever class
# acts as the data source for the NSTableView
# class.
###
def numberOfRowsInTableView(afileTable)
@files.length
end
###
# tableView_objectValueForTableColumn_row
# Returns the value corresponding to the cell
# (row and column intersection) the user has
# currently selected. This must be
# implemented by whatever class acts as the
# data source for the NSTableView class.
###
def tableView_objectValueForTableColumn_row(
afileTable, aTableColumn, rowIndex)
@files[rowIndex]
end
Ces deux méthodes sont assez simples pour notre application.
La première retourne simplement le nombre d’éléments du tableau @files (dont nous allons parler dans une seconde).
La seconde méthode prend en général l’intersection entre la colonne et la ligne en cours de sélection et retourne la valeur de la cellule ainsi désignée.
Cependant, notre application ne contient qu’une seul colonne, ce qui rend l’indice de la colonne inutile.
Tout ce dont nous avons besoin de faire est de retourner l’élément trouvé à l’index de la ligne sélectionnée, et c’est ce que nous faisons dans le code ci-dessus.
Maintenant, nous devons simplement créer une instance de la classe Array et lui assigner notre variable d’instance @files.
Notre tableau @files que nous utilisons pour contenir les noms des fichiers pour notre l’objet NSTableView de notre application doit être créé avant l’appel d’une des deux méthodes.
La meilleure façon de s’en assurer et d’ajouter une méthode initialize (le constructeur en Ruby) et d’y créer une instance de la classe Array, comme le montre le code ci-dessous.
###
# initialize
# This is the Ruby constructor for a class.
# We use it to create a new
# instance of the Array class to hold records
# for our files table.
###
def initialize
@files = Array.new
end
Nous avons maintenant toutes les méthodes nécessaires pour que Controller joue le rôle de source de données pour l’objet NSTableView et nous avons créé le tableau qui contiendra les informations qui seront affichées dans cette table, mais il nous reste à implémenter les méthodes addFile et removeFile qui manipulent cette table.
Faisons-le maintenant.
Dabord, l’implémentation de la méthode addFile
###
# addFile
# Displays an instance of the NSOpenPanel and
# gets the name and location of one or more
# files that the user wishes to add to the
# archive.
###
def addFile(sender)
oPanel = NSOpenPanel.openPanel
oPanel.setAllowsMultipleSelection(true)
buttonClicked = oPanel.runModal
if buttonClicked == NSOKButton
files = oPanel.filenames
count = files.count
for i in 0..count - 1
@files.push(
files.objectAtIndex(i))
end
@fileTableView.reloadData
end
end
Cette méthode affiche un NSOpenPanel qui permet à l’utilisateur de sélectionner plusieurs fichiers qui seront placés dans la nouvelle archive.
Ainsi, la première chose que nous devons faire est de créer une instance de la classe NSOpenPanel.
C’est exactement ce que nous faisons à la première ligne de la méthode.
Cette ligne nous montre étalement comment invoquer une méthode Cocoa en Ruby.
Dans cette ligne nous appelons la méthode openPanel de la classe NSOpenPanel qui se trouve dans le module OSX du framework RubyCocoa.
Puisque nous avons inclus ce module dans notre classe, nous n’avons pas besoin de préfixer la classe NSOpenPanel avec le nom du module (c.à.d. OSX::NSOpenPanel).
Au lieu de cela, toutes les classes et méthodes du module OSX sont intégrées à notre classe Controller.
La ligne suivante indique à la boîte de dialogue d’accepter les sélections multiples, permettant à l’utilisateur de sélectionner autant de fichiers qu’il ou elle le souhaite.
Après cela, nous affichons notre boîte de dialogue nouvellement créée et attendons la réponse de l’utilisateur.
Une fois que l’utilisateur a fermé la boîte de dialogue, nous vérifions quel bouton a été cliqué et nous nous assurons que le bouton “OK” a été cliqué avant de poursuivre.
Si c’est le cas, nous récupérons les noms des fichiers sélectionnés et nous réactualisons les données dans la table.
Ceci affichera tous les éléments actuellement dans notre tableau @files.
La méthode removeFile est aussi simple que la méthode addFile comme vous pouvez le voir dans le code ci-dessous :
###
# removeFile
# Removes the selected file(s) from the list
# of files for the archive.
###
def removeFile(sender)
# Get all of the selected indices from the
# table, and convert it to a Ruby array
indices = @fileTableView.
selectedRowEnumerator.allObjects.to_a
# Get the number of indices in the array
count = indices.length - 1
# Reverse the order of the array before
# removing the elements to avoid changing
# the index number of the elements we wish
# to remove. (e.g., if we have an array
# [a,b,c] and we are removing elements at
# indexes 1 and 2 we run into a problem.
# Once element b is removed element c
# shifts to position 1 and is not removed
# at the second call to delete_at.)
indices = indices.reverse
# Remove all of the selected files
for i in 0..count
@files.delete_at(
Integer(indices[i]))
end
@fileTableView.reloadData
end
La méthode removeFile commence par récupérer un tableau des indices des éléments sélectionnés dans le tableau que l’utilisateur a choisi de supprimer.
Ensuite on passe en revue la liste des indices, supprimant l’élément du tableau pointé par l’indice.
Le tableau @files inverse l’ordre des indices, et donc nous commençons à supprimer les éléments depuis la fin du tableau en progressant vers le début.
Nous faisons cela pour éviter d’altérer l’indexation des éléments en supprimant un autre élément le précédent et poussant l’élément précédent vers un index inférieur.
Si nous supprimons depuis le dernier index, nous déplaçons les éléments après avoir supprimé ceux que nous voulons, en évitant ainsi la suppression d’index d’éléments encore à supprimer.
Maintenant, nous avons tout le code nécessaire pour permettre à l’utilisateur de manipuler le tableau des fichiers de notre application.
Si vous le souhaitez, testez l’application pour vous assurer que les boutons “+” et “-” fonctionnent correctement.
Cependant, vous allez vous apercevoir d’un problème : il ne se passe rien.
C’est parce que nous n’avons pas indiqué à l’objet NSTableView que la classe Controller est sa source de données.
Nous le faisons très simplement en revenant à IB pour quelques secondes. Double-cliquez sur NSTableView dans l’application jusqu’à voir une ligne épaisse l’entourer.
Puis effectuez un control-clic sur la table et glissez la ligne de connexion vers l’instance de la classe Controller dans la fenêtre “MainMenu.nib”.
Dans la fenêtre “Info”, sélectionnez “dataSource” dans l’onglet “Outlets” du panneau “Connections” et cliquez sur le bouton “Connect”.
Enfin, sauvegardez le fichier nib et revenez à Xcode puis essayez d’exécuter l’application de nouveau.
Cette fois, vous devriez pouvoir ajouter et supprimer les fichiers de votre tableau sans aucun problème.
La fonctionnalité d’ajout et suppression d’éléments du tableau de fichiers fonctionne correctement, et il ne nous reste plus qu’une méthode à implémenter dans l’onglet “Create”.
Nous devons implémenter la méthode qui va créer l’archive du fichier et éventuellement la compresser si tel était le choix de l’utilisateur.
La méthode createArchive doit commencer par afficher une boîte de dialogue NSSavePanel qui permettra à l’utilisateur de choisir un nom et un emplacement pour la nouvelle archive.
Après avoir récupéré cette information, notre méthode createArchive va créer un répertoire temporaire et y copier tous les fichiers sélectionnés.
Ensuite, le répertoire temporaire sera taré, compressé et copié à l’emplacement choisi.
Enfin, notre méthode createArchive effectuera un peu de nettoyage en supprimant le répertoire temporaire et tout son contenu.
Si tout se passe bien, l’utilisateur devrait recevoir un message NSAlert indiquant que tout s’est bien passé.
Le code suivant effectue tout cela et doit être ajouté au code de votre application.
###
# createArchive
# Displays an instance of the NSSavePanel and
# gets a filename and location for the
# archive file being created.
###
def createArchive(sender)
sPanel = NSSavePanel.savePanel
sPanel.setExtensionHidden true
sPanel.setAccessoryView @fileTypeView
filename = NSString.alloc.initWithString(
"Untitled")
buttonClicked = sPanel.runModal
if buttonClicked == NSOKButton
filetype =
@fileType.titleOfSelectedItem.to_s
directory = sPanel.filenames.
objectAtIndex(0).to_s.split('/');
filename = directory.pop
directory = directory.join("/")
# Call to_s to turn an NSString into a
# Ruby string before performing a case
# statement on the value
case filetype.to_s
when "tgz"
tarCommand = "tar -cvzf " +
"#{filename}.tgz #{filename}"
when "bz2"
tarCommand = "tar -cvjf " +
"#{filename}.bz2 #{filename}"
when "Z"
tarCommand = "tar -cvZf " +
"#{filename}.Z #{filename}"
when "tar"
tarCommand = "tar -cvf " +
"#{filename}.tar #{filename}"
end
# Create an alert dialog to display
# the outcome of the tar command
alert = NSAlert.alloc.init
alert.setMessageText(
"Creation Successful")
alert.setInformativeText(
"The tar file was successfully " +
"created")
alert.setAlertStyle(
NSInformationalAlertStyle)
alert.addButtonWithTitle("Ok")
begin
# Create a temporary directory to
# hold the files for the archive
system("mkdir #{filename}")
# Copy all of the selected files
# to the archive directory
@files.each do |file|
system("cp '#{file.to_s}' " +
"'#{filename}'")
end
# Execute the tar command
system(tarCommand)
# Copy the tar file to the chosen
# directory
system("mv " +
"'#{filename}.#{filetype}' " +
"'#{directory}/#{filename}." +
"#{filetype}'")
# Remove the temporary directory
system("rm -rf '#{filename}'")
rescue
alert.setMessageText(
"Creation Unsuccessful")
alert.setInformativeText(
"An error occurred while " +
"creating the archive file." +
"nMake sure you have the " +
"correct permissions and " +
"try again.")
alert.setAlertStyle(
NSCriticalAlertStyle)
end
# Display an alert box
alert.beginSheetModalForWindow(
@mainWindow,
:modalDelegate, self,
:didEndSelector, nil,
:contextInfo, nil)
end
end
Vous avez remarqué que dans la première section de cette méthode ressemble à la méthode addFile que nous avons créée un peu avant.
C’est parce que nous créons une instance de NSSavePanel qui est proche de NSOpenPanel.
Une différence importante que vous aurez noté ici est que nous appelons une méthode nommée setAccessoryView à qui nous passons une vue que nous avons créée dans IB.
Cette méthode ajoute notre vue au NSSavePanel ce qui permettra à l’utilisateur de choisir le type d’archive qu’il veut créer au moment du choix du nom et de l’emplacement de l’archive.
Si les utilisateurs choisissent le bouton “OK” du NSSavePanel, nous créons l’archive.
Nous commençons ce processus en récupérant toutes les informations depuis NSSavePanel — comme le type d’archive, le nom du fichier et le répertoire.
Ensuite, nous utilisons ces informations pour construire la commande tar qui créera l’archive et la compressera selon le type de fichier choisi.
Après cela, nous créons une instance de la classe NSAlert et l’initialisons avec un message de succès à afficher à l’utilisateur pour lui indiquer que tout s’est bien déroulé.
Ensuite nous démarrons notre processus de création d’archive.
Nous utilisons la méthode system qui se trouve dans le module Kernel pour exécuter les commandes shell qui vont créer un répertoire temporaire, y copier les fichiers, et utiliser tar et la compression sur ce répertoire, copier les fichiers tarés vers le répertoire choisi et enfin, supprimer le répertoire temporaire et son contenu.
Si tout se déroule correctement, la NSAlert que nous avons créée auparavant sera affichée ; sinon, nous modifierons cette NSAlert pour lui faire afficher un message d’erreur.
Si vous avez tout copié correctement dans votre projet, et que vous avez les permissions de le faire, vous devriez pouvoir exécuter l’application et créer un nouveau fichier archive.
En supposant que tout se déroule bien, nous avons implémenté la moitié des fonctionnalités de notre application.
Alors dépêchons-nous et travaillons sur l’autre moitié.
La partie suivante de cette section ajoutera la possibilité d’extraire les fichiers d’une archive existante.
La méthode extractArchive est très semblable à la méthode createArchive.
D’abord, nous devons créer une instance de la classe NSOpenPanel qui permettra à l’utilisateur de choisir le répertoire où se situe l’archive.
Ensuite, nous créons une commande tar pour décompresser le fichier choisi.
Enfin, si tout se passe bien, l’application affichera une alerte pour indiquer à l’utilisateur que l’extraction a réussi, sinon une erreur lui indiquera le contraire.
Copiez le code suivant dans votre méthode extractArchive :
###
# extractArchive
# Displays an instance of the NSOpenPanel and
# gets the directory in which the extracted
# files will reside. Then, it extracts the
# archived file to that directory.
###
def extractArchive(sender)
oPanel = NSOpenPanel.openPanel
oPanel.setCanChooseFiles(false)
oPanel.setCanChooseDirectories(true)
buttonClicked = oPanel.runModal
if buttonClicked == NSOKButton
filename = @archiveFile.stringValue.to_s
filetype = filename.split('.')[1]
directory = oPanel.filenames.objectAtIndex(0)
# Create the tar extraction command
case filetype
when "tgz"
tarCommand = "tar -xvzf " + "#{filename} -C #{directory}"
when "bz2"
tarCommand = "tar -xvjf " + "#{filename} -C #{directory}"
when "Z"
tarCommand = "tar -xvZf " + "#{filename} -C #{directory}"
when "tar"
tarCommand = "tar -xvf " + "#{filename} -C #{directory}"
end
# Create an alert dialog to display
# the outcome of the tar command
alert = NSAlert.alloc.init
alert.setMessageText(
"Extraction Successful")
alert.setInformativeText(
"The tar file was successfully extracted")
alert.setAlertStyle(
NSInformationalAlertStyle)
alert.addButtonWithTitle("Ok")
# Extract the files from the archive
begin
system(tarCommand)
rescue
alert.setMessageText(
"Extraction Unsuccessful")
alert.setInformativeText(
"An error occurred while " +
"extracting the archived " +
"file.nMake sure you have " +
"the correct permissions " +
"and the chosen file exists.")
alert.setAlertStyle(
NSCriticalAlertStyle)
end
alert.beginSheetModalForWindow(
@mainWindow,
:modalDelegate, self,
:didEndSelector, nil,
:contextInfo, nil)
end
end
Dans la méthode extractArchive, la première chose que vous remarquerez est que nous créons une instance de la classe NSOpenPanel ; cependant, cette instance est légèrement différente de celle que nous avons créée dans la méthode addFile.
Nous initialisons cette instance de telle sorte que des répertoires — pas des fichiers — puissent être choisis, et nous ne permettons qu’une seule sélection, par défaut.
Après cela, nous voyons quelque chose de bien semblable à la méthode createArchive.
Nous créons une commande tar pour extraire les fichiers de l’archive choisie vers le répertoire qui a été selectionné dans NSOpenPanel.
Ensuite nous créons une instance de la classe NSAlert et nous appelons la commande system pour exécuter la commande tar que nous venons de créer.
Enfin, nous affichons un message d’alerte pour indiquer à l’utilisateur la réussite de l’opération.
Si la commande system rencontre des problèmes, nous les interceptons avec la clause rescue et nous affichons une message d’erreur à la place.
Il manque encore une chose bien pratique à notre application.
Plutôt que d’obliger l’utilisateur à taper les noms des fichiers qu’il souhaite extraire, nous devons implémenter une méthode qui permettra aux utilisateurs de sélectionner le fichier dans NSOpenPanel.
C’est exactement ce que fait notre méthode browseForArchive, dont l’implémentation se trouve ci-dessous :
###
# browseForArchive
# Pops up an instance of the NSOpenPanel and
# allows the user to select the archive they
# wish to open.
###
def browseForArchive(sender)
# Create an array of the file types
# allowed in the NSOpenPanel
filetypes = ["tgz", "bz2", "Z", "tar"]
# Create the NSOpenPanel and get the
# archive file to extract
oPanel = NSOpenPanel.openPanel
oPanel.setAllowsMultipleSelection(false)
buttonClicked =
oPanel.runModalForDirectory(nil,
:file, nil,
:types, filetypes)
if buttonClicked == NSOKButton
file = oPanel.filenames.objectAtIndex(0)
@archiveFile.setStringValue(file)
end
end
Là encore, nous nous retrouvons en terrain familier avec juste quelques différences que nous avons vues précédemment.
La première chose que nous remarquons est que nous avons encore créé un objet NSOpenPanel.
Mais cette fois, nous laissons juste l’utilisateur choisir le type des fichiers.
Ces types de fichiers sont les quatre que nous avons choisi de proposer dans notre application et qui se trouve dans le tableau fileTypes que nous avons créé juste avant l’instance de NSOpenPanel.
Une fois que nous avons affiché la boîte de dialogue d’ouverture de fichiers, nous nous assurons que l’utilisateur clique sur le bouton “OK”, puis nous initialisons archiveFileNSTextField pour afficher le nom du fichier que l’utilisateur vient de choisir.
Ceci permet à notre application d’être un peu plus rapide et moins sensible aux erreurs utilisateurs devant taper le nom des fichiers qu’il souhaite extraire.
Conclusion
Et voilà c’est fini.
Vous devriez avoir une application RubyCocoa parfaitement au point.
J’espère que vous avez apprécié ce développement et, avec un peu de chance, c’est une application que vous utiliserez peut-être.
Juste au cas où vous auriez rencontré des problèmes au cours de ce développement — ou si vous avez juste lu l’article sans coder — j’ai mis à disposition une copie du projet Xcode ici.
Faites-en ce que vous voulez. Vous pouvez même y ajouter les suggestions que je mentionne ci-dessous.
Il est évident que de nombreux aspects de notre application peut-être amélioré.
Un premier point pourrait être le nombre d’extensions fichier supportées.
Actuellement, seulement quatre sont supportées, mais il en existe beaucoup d’autres ; de même que des techniques de compression pourraient être ajoutées pour rendre cette application plus robuste.
Vous pourriez également changer toutes les boîtes de dialogues de fichiers en panneau jaillissant.
Une autre fonctionnalité intéressante serait de pouvoir sélectionner une archive existante pour lui ajouter et pour supprimer des fichiers.
Toutes ces fonctionnalités sont des moyens d’améliorer cette application et de la transformer en quelque chose que vous utiliseriez tous les jours.
Pour ceux qui ont apprécié l’article, je pense que vous avez du pain sur la planche qui vous occupera pour un temps.
Si vous ajoutez des fonctionnalités intéressantes, envoyez-les moi, et je verrai si je peux trouver un endroit pour que d’autres personnes puissent les obtenir ; assurez-vous de bien m’envoyer tout le code.
Une dernière chose que je voulais faire partager à ceux qui ont apprécié les articles jusque-là.
J’ai vraiment apprécié écrire ces tutoriels, et je réfléchis déjà à d’autres articles pour le futur proche.
Alors si vous avez aimé cette série sur la programmation Ruby, attendez-vous à d’autres articles de ma part sous peu.

Textes originaux en anglais sur O’Reilly : An Introduction to RubyCocoa, Part 2 par Christopher Roach
Chargement
Commentaires récents