『Silverlight2 と IronRuby で Flickr のクライアントを書く』を翻訳した。
http://d.hatena.ne.jp/ma2/20080815/p1の翻訳。
文章が口語調なんで,うまく訳せていないところがありますが許してください。ここで使っている画面イメージは僕のPCで,このエントリの通りに作ったものを動作させた結果です。
元ネタはJimmy Schementiさんのブログ(http://blog.jimmy.schementi.com/2008/08/walk-through-silverlight-flickr-client.html)。素晴らしいエントリをありがとう!
ウォークスルー: SilverlightとIronRubyでFlickrクライアントを作る
昨日,私は.NET Developers Association(NETDA)向けにIronRubyとSilverlightについて話をした。この記事では,私が作ったアプリの1つ,Flickrクライアントをお見せしよう。
実際のアプリへのリンク:http://jimmy.schementi.com/silverlight/photoviewer
事前準備
このウォークスルーはマックでもWindowsでも動作するが,Safariでは問題があるようだ。FirefoxとIEでは問題なく動作する。
始めるには,Silverlight Dynamic Languages SDK (Beta 2), 略して"sdl-sdk"のダウンロードと,Silverlight 2 Beta 2のインストールが必要だ。sdl-sdk.zipをsdl-sdkフォルダに展開すること。
またこのアプリが利用するイメージとライブラリを含んだphotoviewer-start.zipが必要だ。
新規プロジェクトの作成
Silverlightのアプリを作るために,SDKに"sl"というスクリプトがある。これは2つの引数をとる。言語(ruby,python,あるいはjscript)とアプリケーション名である。カレントディレクトリにアプリ名のフォルダが生成されて,その中にデフォルトアプリが生成される。ちょっと話しすぎたかな。実際にやってみよう。cmd.exeでもterminal.appでも開いて,以下のように入力する。
$ cd path/to/sdl-sdk
$ script/sl ruby photoviewer
【注意】もしWindowsを使っているのなら,パスの区切りにスラッシュ '/' ではなく '\' を使うこと。
これでたぶん"Your ruby Silverlight application was created in photoviewer"のようなメッセージが出るはずだ。同じようになったかな? 素晴らしい,先に進もう。では何が生成されたのだろうか。"photoviewer"フォルダの中には以下のようなものがある。
- index.html
- Ruby Silverlightの入れ物。何が生成されたのか気になるのなら,コメントに解説があるので読もう!
- reby/app.rb
- Silverlightのエントリポイント。このファイルはapp.xamlをレンダリングして,Rubyでちょっとしたテキストをセットしている。
- ruby/app.xaml
- アプリ用のXAML UI。実際には今回のアプリではXAMLは使わないので,後で削除する。
- ruby/silverlight.rb
- SilverlightApplicationクラスを定義し,既存のSilverlightのAPIをRubyにとってより使いやすいようにしている。これの新しいバージョンをphotoviewer-start.zipに入れてあるので,置き換えること。
- stylesheets/error.css
- まずいRubyのコードを書いて不幸なイベントが起きたとき,ブラウザに表示されるエラーメッセージを読みやすくするスタイルシート。
- javascripts/error.js
- Rubyのエラー通知機能をオフにした場合でも,このファイルが全てのエラーをキャッチして,ユーザが見苦しいアラートボックスを見なくてすむ。
新規に作ったRubyアプリを走らせる
喋りすぎたようだ。ちゃんと動作しているかな。試してみよう。
$ cd photoviewer
$ ../script/server /b:index.html
【注意】"/b"はWindowsでもMacでも同じようにスラッシュを使う。なぜならこれはパラメータの一部でありパスではないからである。
これはSilverlightの開発用のWEBサーバChironを起動して,デフォルトブラウザでindex.htmlを表示する。"/w"オプションでは,WEBサーバだけ起動してブラウザは起動しない。とにかく,このような表示が見られるはずだ(もちろんデフォルトブラウザ内に)。
外部ライブラリの追加
このアプリはいくつかの外部ライブラリに依存しているので,コーディングを始める前にプロジェクトに入れよう。私は大変いい人間なので,ダウンロードしてもらったphotoviewer-start.zipに,依存しているものを全て入れておいた。zipの中身を取り出して,プロジェクトディレクトリにコピーする。rubyやsilverlight.rbを置き換えるかと警告が出るかもしれないが,問題ないので置き換える。やった? 素晴らしい。さて何を追加したのだろうか?
- images/loading.gif
- 読み込み中のGIFイメージ。
- ruby/System.Json.dll
- JSONパーサ。Silverlight SDKに同梱のもの。
- ruby/json.rb
- System.Jsonへのモンキーパッチ。よりRubyっぽくするためのもの。
- ruby/silverlight.rb
- このアプリはsilverlight.rbのより新しいバージョンに依存しているので,生成したファイルをこれで置き換える。
- lightbox/
- イメージを美しく表示するjavascriptのライブラリ。大好き。
すごいコードを書く! UIとか
前述したように,このアプリはHTML UIをIronRubyが駆動する。そう,Silverlightを美麗なグラフィック抜きで使うことも可能なんだ。これを実現するために,Silverlightの描画画面を非表示にする必要がある。index.htmlを開いて,28行目のwidthとhightを"100%"から"1"にする。
<object data="data:application/x-silverlight," type="application/x-silverlight-2-b2" width="1" height="1">
width/hightを"1"にすると,コントロールを非表示にできるが,実際には存在しているのでロードはされる。賢いハックだろ?
我々のアプリにUIを与えよう。以下をindex.htmlの最後にある""タグの後ろに入力する。
<div class="search"> <form id="search" action="javascript:void(0)"> <input type="text" id="keyword" /> <input type="submit" id="submit_search" value="search" /> <img src="images/loading.gif" id="images_loading" /> </form> <div id="search_results"> <div id="search_images"></div> <div id="search_links"></div> </div> </div> <div class="clear"></div>
これは基本的なサーチ欄,サブミットボタン,見つかったイメージを表示する場所だ。スタイルをつけよう。stylesheets/screen.cssをオープンする。一番最初に「#silverlightControlHost」スタイルは,1×1のコントロールが100%の大きさになっては困るので不要だ。削除してくれ。
#silverlightControlHost { height: 100%; }
いま作った素のUIにスタイルをつけよう。
body { font-family: "Trebuchet MS" Verdana sans-serif; border: 0px; padding: 0px; margin: 0px; } div.clear { clear: both; } /* main search box */ .search { padding: 20px; margin: 20px; border: 10px solid gray; background-color: #ccc; } form#search #images_loading { width: 18px; height: 15px; display: none; } /* search results */ #search_results { display: none; } /* search images */ #search_images { padding-top: 10px; } #search_images .image, .image a, .image a img { float: left; padding: 0px; margin: 0px; border: 0px; } #search_images .image a:link, #search_images .image a:visited { background-color: white; padding: 5px; margin: 5px; background-color: white; border: 1px solid gray; } #search_images .image a:hover { background-color: #ff9966; } /* search links */ #search_links { clear: both; padding-top: 10px; } #search_links a { border: 1px solid #003344; margin: 2px; padding: 0px 5px; color: #003344; background-color: white; text-decoration: none; } #search_links a:hover, #search_links a.active { color: white; background-color: #003344; border: 1px solid white; } #search_links a.active { cursor: default; }
ブラウザ画面をリロードすれば,この画面が表示される。
IronRubyはDOMが好き
すごい。これでUIができたが,何もしない。Rubyの登場だ。ruby/app.rbを開いて,ハッキングを始めよう。
このアプリのポイントは,キーワードを入力してサーチボタンを押すと,そのキーワードに関連した画像をFlickrからダウンロードするところだ。なので,最初のステップとしてRubyにサーチボタンの押下を制御させることを提案したい。いいかな? よし。Appクラス全体を,以下のコードで置き換える。
def initialize document.submit_search.onclick do |s, e| puts "Search button pressed!" end end
これはサーチボタンを押すたびにページの一番下に「Search button pressed!」と表示する。おっと,コードが言う通りだ。
RubyはFlickrが大好き
ボタンクリックをRubyにフックする方法は分かったので,Flickrと話して検索結果のデータを取得するようにしよう。まず最初に,Flickrとの通信方法を知る必要がある。initializeを以下のように変える。
def initialize @url = "http://api.flickr.com/services/rest" @options = { :method => "flickr.photos.search", :format => "json", :nojsoncallback => "1", :api_key => "6dba7971b2abf352b9dcd48a2e5a5921", :sort => "relevance", :per_page => "30" } document.submit_search.onclick do |s, e| create(document.keyword.value, 1) end end
我々は分別のある大人なので,FlickrとはRESTで通信する。@optionsハッシュは,RESTの呼び出しに必要な様々なパーツを集めたものだ。我々は後でこれをURLにする関数を作る。重要なのは,我々がflickr.photos.searchを呼び出し,レスポンスとしてJSONを要求するということだ。onclickイベントは,神秘的な名前のcreateメソッドを呼び出す。これはまだ存在していないので,書くことにする。
def create(keyword, page) @options[:tags] = keyword @options[:page] = page request end def request make_url request = Net::WebClient.new request.download_string_completed do |sender, args| @response = args.result show end document.images_loading.style[:display] = "inline" request.download_string_async Uri.new(@url) end
createは再利用する必要がありそうだし,その引数はURIの一部としても必要なので,initializeの中で作った@optionsに追加しておく。そしてrequestメソッドを呼び出す。ここで「会話」が始まる。
最初に,@optionsからURLを作る(メソッドはすぐ作る)。そしてSystem::Windows::Net::WebClient(Silverlightの一部)のインスタンスに対して,Flickrに実際にリクエストを出すように通知している。images_loading.style[:display] = "inline" で,ロード中のインジケータが回り始める。レスポンスが帰ってきたら,showを呼び出す。これはなんか表示すべきだろうね。とりあえずレスポンスを出力して,ロード中インジケータを止める。
def show puts @response document.images_loading.style[:display] = "none" end
おっと。make_urlを作るちょうどいい頃合いだ。
def make_url first, separator = true, '?' @options.each do |key, value| separator = "&" unless first @url += "#{separator}#{key}=#{value}" first = false end end
特別なことは何も無くて,"?"と"&"と"="を正しい場所におくだけだ。まだこれだけじゃ動作しない。app.rbを保存して,検索文字を入力して,searchをクリックすると,わけの分からないものが表示される。
この気味悪いものを美しく
このFlickrのデータをどうすればいいのか ... うーん ... どう? このゴミの中のどこかに美しい写真があるのだから,絞り出すことにしよう。言い換えればJSONをパースしよう。app.rbの先頭に,以下の行を追加する。
require 'json'
これは最初にコピーしたjson.rbをロードする。これは単なるSystem::Json::JsonValueに対するモンキーパッチで,データへのアクセスを簡単にしてくれる。興味があるのなら,json.rbの該当部分はこれ。
require 'System.Json.dll' include System module System::Json class JsonValue def [](index) item = self.get_Item(index.to_clr_string) type = item.get_JsonType return item.to_string.to_s.to_f if type == JsonType.Number return item.to_string.to_s.split("\"").last if type == JsonType.String return System::Boolean.parse(item) if type == JsonType.Boolean item end def inspect to_string.to_s end end end
これはJsonValueに[]メソッドを追加して,foo = {'bar' : 'baz'}であるようなJSONの値にfoo['bar']とアクセスできるようにする。便利。それはともかく我々のコードに戻ろう。app.rbで,showメソッドを定義し直して,JSONをパースしてなんか役に立つことをするようにしよう。
def show @flickr = System::Json::JsonValue.parse(@response) render end def render @render = Render.new(@flickr, @options[:tags], @options[:page]) document.search_images[:innerHTML] = @render.generate_photos document.search_links[:innerHTML] = @render.generate_pages @render.hook_page_events('search_links') document.images_loading.style[:display] = "none" document.search_results.style[:display] = "block" end
showはFlickrのレスポンスに対してSystem::Json::JsonValue.parseを呼ぶだけになり,次にレンダリングを新しいクラスRender(あとで定義する)に委譲して,ロード中インジケータを止めるrenderという新しいメソッドを書いた。render.rbというファイルを作り,app.rbの中でrequireしよう。
require 'render'
render.rbを開き,写真のレンダリングにとりかかろう。
class Render def initialize(flickr, tags, current_page) @flickr = flickr @tags = tags @current_page = current_page end def generate_photos if @flickr['stat'] == "ok" && @flickr['photos']['total'].to_i > 0 tag(:div, :class => 'images') do @flickr['photos']['photo'].collect do |p| photo(p) end.join end else "No images found!" end end def photo(p) source = "http://farm#{p['farm'].to_i}.static.flickr.com/#{p['server']}/#{p['id']}_#{p['secret']}" thumb = "#{source}_s.jpg" img = "#{source}.jpg" tag(:div, :class => 'image') do tag(:a, :href => "#{img}") do tag(:img, :src => "#{thumb}") end end end def generate_pages "" end def hook_page_events(div) end private def tag(name, options, &block) output = "" output << "<#{name}" keyvalue = options.collect do |key, value| "#{key}=\"#{value}\"" end output << " #{keyvalue.join(" ")}" if keyvalue.size > 0 if block output << ">" output << yield output << "</#{name}>" else output << " />" end output end end
ちょっとばかりコードがあるが,tagの呼び出し以外は素直なものだ。プライベートメソッドのtagは,nameとoptionsに基づいてHTMLを生成すること以外の大半の仕事をする。HTMLをよりrubyらしく出力する(The private tag method does most of the work here, but generating HTML based on a name and options; it makes writing HTML more ruby-esk.)。またgenerate_pagesとhook_page_eventsが実装されていないことに気づいたことだろう。これらは今のところ実装できない。ブラウザをリロードして検索を行えば,イメージを取得できる。
Flickrの出力をページ化する
Flickrはリクエストの1ページだけを送ってくる。Flickrへのリクエストのoptionsを作ったときには,ページあたり30個のイメージを意味する:per_page=>30というエントリがあった。searchボタンをクリックしたときにcreate(document.keyword.value, 1)を呼び出すとき,2番目の引数はFlickrにクエストの1ページ目を要求している。だから任意のページを得るためには,createメソッドにその値を与えれば,該当のページを渡してくる。再利用するって言ったでしょ! generate_pagesを実装するには,そのページへのリンクをレンダリングすればよく,hook_page_eventsを実装するには,各ページリンクに指定のページでcreateを呼び出すようにフックする必要がある。簡単だね。
def generate_pages render = "" if @flickr['photos']['total'].to_i > 0 num_pages = @flickr['photos']['pages'].to_i > 10 ? 10 : @flickr['photos']['pages'].to_i num_pages.times { |i| render += page(i + 1) } if num_pages > 1 end render end def page(i) tag(:a, :href => 'javascript:void(0)', :id => "#{i}") { "#{i}" } end def hook_page_events(div) $app.document.get_element_by_id(div.to_s.to_clr_string).children.each do |child| if child.id.to_s.to_i == @current_page child.css_class = "active" else child.onclick { |s, args| $app.create(@tags, child.id.to_s.to_i) } end end end
あとは保存して,リロードして,検索して,歌って,踊って… きれいなページングアクションが見られるはずだ。
RubyはJavascriptと一緒にうまく遊べる
ブラウザの中でRubyが動くなんて本当にすごい。でも真実を語ろう。僕はJavascriptも好きだ。なんといってもJavascriptには死ぬほどライブラリがあるから。
"clicking on an image"をちゃんとしたい。いまは単純に新しいページをオープンするだけで,検索結果はなくなってしまうので「戻る」ボタンは意味が無い。イメージをクリックしたときにクールな視覚効果が使えたり,実際のFlickrのイメージへのリンクを持てたら素晴らしい。そんなライブラリがあればの話だけど…
http://www.huddletogether.com/projects/lightbox2/
前にも言ったとおり,Lightboxは素晴らしい。イメージをズームアップするにはLightboxを使う。まずindex.htmlに戻り,Lightboxを読み込む。
<script type="text/javascript" src="lightbox/js/prototype.js"></script> <script type="text/javascript" src="lightbox/js/scriptaculous.js?load=effects"></script> <script type="text/javascript" src="lightbox/js/lightbox.js"></script> <link rel="stylesheet" href="lightbox/css/lightbox.css" type="text/css" media="screen" />
Lightboxは,アンカータグの特殊なプロパティを使って,イメージがコレクションの一部かどうか,イメージのタイトルは何かなどの情報を格納している。Render#photoメソッドに戻って,:titleと:relの値を:aタグに追加しよう。
tag(:a)の呼び出しはこんな風になる。
tag(:a, { :href => "#{img}", :title => "<a href="http://www.flickr.com/photos/#{p['owner']}/#{p['id']}" target="_blank">#{p['title']}</a>", :rel => "lightbox[#{@tags}]" }) do tag(:img, :src => "#{thumb}") end
最後に,レスポンスを受信したときにLightboxを初期化する。app.rbのshowメソッドreder呼び出しの後ろに以下のコードを追加する。
if document.overlay && document.lightbox document.overlay.parent.remove_child document.overlay document.lightbox.parent.remove_child document.lightbox end HtmlPage.window.eval("initLightbox()")
最後の行は最高に美しいとは言えないが,イメージは全て美しい。
これでお終い!
最後までごくろうさん! 「おっぱい」とか検索して時間をつぶしてね。