Blog tech

MacRuby - Introduction à HotCocoa - Création de l'UI

Rédigé par Nicolas Cavigneaux | 25 janvier 2011

Dans notre dernier article concernant HotCocoa nous avons vu les bases de la création d’un projet en faisant le tour des fichiers disponibles et en expliquant leur fonctionnement.

Dans ce nouvel article nous allons mettre en place l’interface utilisateur et y associer des actions factices qui seront remplacées dans la troisième et dernière partie de cette série d’articles sur HotCocoa.

Code de l’UI

Voici le code complet qui sera utilisé dans notre exemple. Nous allons le détailler pas à pas. Comme pour le dernier article, l’ensemble du code source est disponible sur notre compte GitHub

require 'rubygems'
require 'hotcocoa'

class Application

  include HotCocoa

  def start
    application :name => "SynbiozFeeds" do |app|
      app.delegate = self
      window(:size => [640, 480], :center => true, :title => "Synbioz Feed", :view => :nolayout) do |win|
        win.will_close { exit }

        win.view = layout_view(:layout => {:expand => [:width, :height], :padding => 0, :margin => 0}) do |vert|
          vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal, :layout => {:padding => 0, :margin => 0, :start => false, :expand => [:width]}) do |horiz|
            horiz << label(:text => "Flux RSS", :layout => {:align => :center})
            horiz << @feed_field = text_field(:layout => {:expand => [:width]})
            horiz << button(:title => 'lire', :layout => {:align => :center}) do |b|
              b.on_action { load_feed }
            end
          end

          vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
            scroll.setAutohidesScrollers(true)
            scroll << @table = table_view(:columns => [column(:id => :data, :title => '')],
                                          :data => []) do |table|
               table.setUsesAlternatingRowBackgroundColors(true)
               table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)
            end
          end
        end
      end
    end
  end

  def load_feed
    str = @feed_field.stringValue
    unless str.nil? || str =~ /^\s*$/
      10.times do |i|
        title = str + " - article #{i.next}"
        @table.dataSource.data << {:data => title}
      end
      @table.reloadData
    end
  end
end

Application.new.start

Design et fonctionnement de l’UI

L’idée est de pouvoir entrer l’URL d’un flux RSS ou ATOM, valider notre saisie, récupérer les 10 dernières entrées de ce flux et les afficher dans un tableau par ordre chronologique inverse. Dans un premier temps nous allons simuler la récupération du flux et l’affichage de ses données pour se concentrer sur le développement de l’interface graphique.

Voici donc le résultat que l’on souhaite obtenir :

Fonctionnement du code

Nous commençons par créer la fenêtre principale de notre application :

window(:size => [640, 480], :center => true, :title => "Synbioz Feed", :view => :nolayout)

On précise sa taille, on force le centrage au lancement de l’application puis on définit le titre de cette fenêtre. Le dernier paramètre :view => :nolayout n’est pas obligatoire pour faire fonctionner notre application mais nous permet d’éviter du traitement qui serait inutile. En effet, à la création d’une fenêtre, un LayoutView par défaut sera instancié et appliqué à la fenêtre. Il s’avère que nous allons créer notre propre layout pour afficher nos résultats il est donc tout à fait inutile de laisser l’application créer le layout par défaut qui ne sera jamais utilisé.

Bien souvent vous créerez votre propre LayoutView. La classe LayoutView est la brique de base qui va vous permettre d’organiser la disposition des éléments dans votre fenêtre. Il faut savoir que LayoutView hérite de NSView, vous aurez donc accès à l’ensemble des méthodes de NSView depuis votre objet LayoutView.

Un objet LayoutView peut prendre de nombreux paramètres. Tout d’abord :frame qui se comporte comme pour un objet window et qui permet donc de définir la position et la taille de l’élément dans la vue. Nous utilisons également :mode, :margin, :spacing et :layout.

  • :mode permet de choisir si la disposition sera :vertical ou :horizontal. Par défaut, le mode est :vertical
  • :margin définie la taille de la marge pour le layout
  • :spacing définie l’espacement qui sera utilisé entre les éléments placés dans la vue.

Et finalement :layout que nous allons voir plus en détail.

Le hash :layout sera automatiquement transformé en un objet LayoutOptions. les clés disponibles pour ce hash sont :start, :expand, :padding, :left_padding, :right_padding, :bottom_padding, :top_padding et :align.

  • :start définie comment vont s’empiler les vues filles mais je dois avouer que même après des tests je n’ai pas vraiment compris la différence entre une valeur à true ou false. La valeur par défaut étant true
  • :expand définie comment la vue va s’étirer si la fenêtre est redimmensionnée. les options disponibles sont :height, :width, [:height, :width] et nil. Par défaut la valeur est nil
  • :_padding* définie le padding autour des vues.
  • :align définie l’alignement de la vue. les options disponibles sont :left, :center, :right, :top et :bottom

Dans notre cas d’exemple nous utiliserons deux layouts et une scroll view qui nous servira à afficher les éléments récupérés.

win.view = layout_view(:layout => {:expand => [:width, :height], :padding => 0}) do |vert|

Nous créons donc la vue principale que nous associons à la fenêtre. On prend soin au passage de supprimer le padding et de préciser que cette vue se redimmensionnera sur la hauteur et la largeur si la fenêtre est redimmensionnée. Aucun :mode n’ayant été spécifié, les éléments seront empilés verticalement.

Les objets layout_view peuvent prendre un bloc ce qui permet, entre autre, d’ajouter facilement des vues dans la vue nouvellement créée.

vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal, :layout => {:padding => 0, :start => false, :expand => [:width]}) do |horiz|

Nous ajoutons ensuite une vue horizontale dans laquelle nous pourrons placer un label, une zone de texte ainsi qu’un bouton de validation. Cette vue pourra être étirée sur sa largeur (:expand => [:width]) alors que sa hauteur sera maintenue à celle spécifiée dans le paramètre :frame.

horiz << label(:text => "Flux RSS", :layout => {:align => :center})
horiz << @feed_field = text_field(:layout => {:expand => [:width]})
horiz << button(:title => 'lire', :layout => {:align => :center}) do |b|
  b.on_action { load_feed }
end

Nous pouvons maintenant ajouter les éléments précédemment cités. Pour le label et le bouton nous précisons un alignement centré :align => :center pour qu’ils soient correctement alignés avec le champ texte. Seul ce champ texte aura une propriété expand ce qui permettra de l’étirer si on agrandit la fenêtre, le label et le bouton quand à eux auront une position et une taille fixe.

En créant le bouton, nous utilisons un bloc pour définir le comportement lorsque l’évènement on_action sera déclenché. Cet événement est déclenché à chaque pression du bouton. Nous appellerons la méthode load_feed qui est définie plus loin.

vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
  scroll.setAutohidesScrollers(true)

Nous ajoutons ensuite une scroll_view à notre vue verticale. Cette scroll_view sera extensible en largeur et en hauteur. Nous passons ensuite une propriété à true pour faire en sorte que la scrollbar de notre vue soit masquée automatiquement si elle n’est pas utile.

scroll << @table = table_view(:columns => [column(:id => :data, :title => '')], :data => []) do |table|
  table.setUsesAlternatingRowBackgroundColors(true)
  table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)
end

Nous pouvons maintenant ajouter une table dans notre scroll_view. Cette table est destinée à accueillir les données récupérées dans le flux RSS. Au passage on définit deux propriétés de la table, une couleur alternative pour toutes les lignes impaires ainsi que le style de la grille de lignes.

En ce qui concerne la création de la table en elle même, il y a quelques détails qui différent par rapport aux autres éléments de vue que nous avons utilisés jusque là. Les deux paramètres qui nous intéressent sont les colonnes que comportera notre table et la source d’information à utiliser pour remplir la table.

Le paramètre :columns attend en paramètre un tableau d’objet column. Le paramètre :title de l’objet column définie ce qui sera affiché au dessus de la colonne. Nous passons ici une chaîne vide car la valeur par défaut pour le titre est ‘Column’. Le paramètre :id de l’objet column représente l’identifiant qui sera utilisé lorsque nous souhaiterons fournir des données à la table.

Il est possible de fournir les données de deux façons. Vous pouvez soit passer un tableau ou fournir votre propre objet qui servira de source. Si vous décidez d’utiliser votre propre source de donnée, elle doit répondre aux méthodes numberOfRowsInTable(tableView) qui retourne un entier correspondant au nombre d’enregistrements à afficher dans le tableau ainsi qu’à la méthode tableView(view, objectValueForTableColumn:column, row:i) qui retourne une chaîne correspondant à la donnée à afficher pour une colonne et une ligne donnée.

Si vous utilisez la source de données par défaut vous passerez alors un tableau de hashes. Les clés dans le hash devront alors correspondre aux :id que vous avez définis à la création des colonnes.

Voici un petit exemple pour clarifier les choses :

@table = table_view(:columns => [column(:id => :name, :title => 'Nom'),
                                 column(:id => :age, :title => 'Age'),
                                 column(:id => :sex, :title => 'Sexe')],
                    :data => [{ :name => 'Nicolas Cavigneaux', :age => 29, :sex => 'M' },
                              { :name => 'Martin Catty', :age => 26, :sex => 'M' },
                              { :name => 'Jean-Rémi Laisne', :age => 25, :sex => 'M'}])

Maintenant que la mise en place de la table est faite, il ne nous reste plus qu’à définir la méthode load_feed qui servira à charger les données lors d’un clic sur le bouton. Pour cet article, nous utiliserons des données factices qui seront basées sur la chaîne entrée par l’utilisateur dans le champ texte.

def load_feed
  str = @feed_field.stringValue
  unless str.nil? || str =~ /^\s*$/
    10.times do |i|
      title = str + " - article #{i.next}"
      @table.dataSource.data << {:data => title}
    end
    @table.reloadData
  end
end

Nous récupérons la valeur entrée par l’utilisateur dans le champ texte via la méthode Cocoa stringValue. Nous prenons soin de ne pas tenir compte des chaînes vides puisque ce ne peut pas être une valeur valide pour notre utilisation.

Une fois la chaîne récupérée, nous bouclons dix fois pour créer dix lignes de résultat contenant la chaîne entrée par l’utilisateur suivie d’un nom d’article fictif avec un index.

Pour ajouter le résultat à la table, nous appelons la méthode data sur la dataSource de notre table et nous lui passons un hash ayant une clé qui correspond à l’id que nous avions défini en déclarant la table. Une fois nos données à jour, il nous faut recharger les données de la table pour que cette dernière affiche son contenu.

Notre application est maintenant fonctionnelle, on peut donc la tester, entrer un texte, valider avec le boutons et voir les 10 lignes apparaitre dans le tableau. Si vous recommencez, les nouvelles lignes vont s’ajouter et vous finirez par voir apparaître une scrollbar. Dans la prochaine version, la table sera vidée entre chaque demande.

Conclusion

Cet article nous a permis de voir les bases de la création d’une interface graphique Cocoa via HotCocoa et l’association d’actions sur des événements donnés. Notre prochain article concernant HotCoca aura pour but de mettre en place le code effectif qui lit l’URL entrée, rapatrie les données du flux RSS et affiche le titres des 10 derniers articles par ordre chronologique inverse.

L’équipe Synbioz.

Libres d’être ensemble.