2015年4月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
無料ブログはココログ

« 2012年6月 | トップページ | 2013年7月 »

2012年7月の4件の記事

2012年7月17日 (火)

やっぱり、コメントというものはそうそういただけるものではないですね。

このブログ、何に使うかと思いながら始めてみましたが、結局今のところ、wxRuby のブログになっています。

特に超絶なテクニックを提示できているわけでもなく、ブログにソースコードを全て載せているのは、これを見て学んでくださいというよりは、こんなコードしかかけないので、もっといい方法を教えてください、というつもりだったのですが、そうそう簡単には突っ込みはもらえないようです。

ためになるご指導をいただけるまでには、まだまだ修行を積まなければならないようです。

2012年7月 6日 (金)

wxRuby での例外処理のワナ

前回 (画像を貼り付ける(6) - ListItem のずれない identifier) で、大きな問題には対処しましたが、あれでも特定の条件下ではエラーが出ます。

それで思い出したのですが、ここまで説明を忘れていた大事なテクニックとして、例外処理(エラー処理)があります。といっても raise() を使えばよいのですが、これではダメなのです。ダメだということは、どこかで読んでいたのですが、実際にコードを書いているとやっぱりやってしまいました。

何が問題かというと、raise() という method 名が ruby と wxRuby で衝突しているのです!!!

Wx::Window#raise() という method があるので、普段 ruby で記述しているように、関数的 method として、地の文でいきなり raise() とすると、例外処理ではなく、Wx::Window#raise() が呼び出されてしまいます。通常通り例外処理に繋げるには、レシーバを明示して、

   Kernel.raise()

としなければなりません。ここ、試験に出ます :-p

去年、wxRuby の勉強を始めて web を漁っていたころ、どこかのサイトで、wxRuby の今後の version では、Wx::Window#raise() の方の method 名を変えるとかどうとか書いてあるのを見たような記憶があるのですが、いずれにしても、wxRuby で書くときには、Kernel.raise() としておいたほうが安全なようです。

2012年7月 5日 (木)

画像を貼り付ける(6) - ListItem のずれない identifier

ListItem#id は、ListCtrl の要素が削除されてしまったり、sort されてしまったりすると、identifier として使えないことがわかりました。では、代わりに何を使うのか。

今回に関してだけの話であれば、ListItem#data に、元画像の file 名を格納しているので、これが一意な identifier に使えますが、他の用途で ListCtrl を使うときには、そうはいかないかもしれない。また、今回書いてみたコードでは、identifier で検索をかけるしかないのですが、identifier が文字列だと、比較・検索のコストが高くなりそうなので、できれば整数値が使えるといいですね。

wxWidget レベルでの ListItem の instance の window id とか、wxRuby レベルでの ListItem object の object_id が使えれば楽チンですが、ListCtrl の中で ListItem そのものが格納される実装になっているか不明ですし、object の中身は同一でも、生成・破壊が繰り返されていて object_id は変わっているということも考えられるので、安易には使えません。

結局、自前で整数値の identifier を設定することにしました。ListItem にユーザーレベルで自由に使えるデータは ListItem#data にしか設定できませんが、これを identifier に使ってしまうと、画像 file 名を格納しておくところがなくなってしまいます。そこで、以下のような Struct を使って、ListItem#data に複数の値を格納することにしました。

ListItemData = Struct.new(:static_index, :path)
item.data = ListItemData.new( idx, img_file )

こうして、idx に identifier としての整数値を、img_file に画像 file 名としての文字列を渡せば ListCtrl#sort_item() をかけても ListItem#data.static_index は変わりません。

上記のギミックを組み入れて、修正したのが以下のコードです。


thumb_child2.rb


# encoding: UTF-8

require 'wx'

class MyApp < Wx::App
  def on_init
    @main_frame = ThumbnailsFrame.new
    @main_frame.show
  end
end

class ThumbnailsFrame < Wx::Frame
  THUMB_W = 160; THUMB_H = 120
  THUMB_SIZE = Wx::Size.new( THUMB_W, THUMB_H )

  def initialize
    super(nil, :title => "Thumbnails", :size => [720, 480])
    set_sizer(Wx::BoxSizer.new( Wx::VERTICAL ))
    create_status_bar

    load_button = Wx::Button.new(self, :label => "Load...")
    evt_button( load_button ) { |evt| on_load }
    get_sizer.add(load_button, 0, Wx::ALIGN_CENTER_HORIZONTAL)

    @thumb_list = Wx::ListCtrl.new(self, :style => Wx::LC_ICON)
    get_sizer.add_item(@thumb_list, :proportion => 1, :flag => Wx::EXPAND)

    evt_list_item_activated( @thumb_list ) { |e| thumb_activated( e ) }
    @thumb_list.evt_size { |e| refresh_thumb_list }
  end

  private
  def on_load
    dlg = Wx::DirDialog.new(self)
    if ( dlg.show_modal == Wx::ID_OK )
      set_status_text("サムネイルを作成中...")
      load_thumbnails(dlg.get_path)
      set_status_text("")
    end
  end

  ListItemData = Struct.new(:static_index, :path)
  def load_thumbnails(dir)
    image_list = Wx::ImageList.new(THUMB_W, THUMB_H)
    @thumb_list.set_image_list(image_list, Wx::IMAGE_LIST_NORMAL)

    item = Wx::ListItem.new
    glob_pat = %w[jpg png].map{|x| "#{dir}/*.#{x}".gsub(/\\/, '/')}.join("\0")
    Dir.glob( glob_pat ).sort.each do |img_file|
      idx = image_list.add(thumbnail_bitmap(img_file))
      unless ( idx < 0 )
        item.id = idx; item.image = idx
        item.data = ListItemData.new( idx, img_file )
        @thumb_list.insert_item( item )
      end
    end
  end

  def thumbnail_bitmap(file)
    img = Wx::Image.new(file)
    img_w, img_h = img.get_width, img.get_height
    ratio_w, ratio_h = THUMB_W.to_f/img_w, THUMB_H.to_f/img_h

    if ( ratio_w < ratio_h )
      new_h = (img_h * ratio_w).to_i
      img.rescale(THUMB_W, new_h)
      img.resize(THUMB_SIZE, Wx::Point.new( 0,(THUMB_H - new_h)/2 ))
    else
      new_w = (img_w * ratio_h).to_i
      img.rescale(new_w, THUMB_H)
      img.resize(THUMB_SIZE, Wx::Point.new( (THUMB_W - new_w)/2,0 ))
    end
    Wx::Bitmap.from_image(img)
  end

  def thumb_activated( evt )
    @service ||= ->( item_static_index, service_tag ) {
      target_index = @thumb_list.find { |idx|
        @thumb_list.item( idx ).data.static_index == item_static_index
      }
      unless ( target_index )
        Kernel.raise( "Item with static index : #{item_static_index} dose not exist.\n" )
      end

      target_item = @thumb_list.item( target_index )
      case service_tag
      when :path; target_item.data.path
      when :delete
        path = target_item.data.path
        @thumb_list.delete_item( target_index )
        File.delete( path )
        refresh_thumb_list
      when :id; target_item.id
      when :static_index; item_static_index
      else
        Kernel.raise( "Undefined service tag \"#{service_tag}\" is specified.\n" )
      end
    }
    ViewFrame.new( self, @service.curry[ evt.item.data.static_index ] ).show
  end

  def refresh_thumb_list
    @thumb_list.each do |i|
      item = @thumb_list.item(i)
      item.text = "dynamic = #{item.id} : static = #{item.data.static_index}"
      @thumb_list.set_item( item )
    end
    @thumb_list.sort_items{ |a,b| a.static_index<=>b.static_index }
  end
end

class ViewFrame < Wx::Frame
  def initialize( parent, service )
    @parent_service = service
    path = @parent_service[ :path ]
    title = "dynamic on fork = #{@parent_service[ :id ]} : #{path}"
    super( parent, -1, title, :size => [450,500] )

    sizer_top = Wx::BoxSizer.new( Wx::VERTICAL )
    @sizer_button = Wx::BoxSizer.new( Wx::HORIZONTAL )
    @sizer_button.add_stretch_spacer
    @sizer_button.add( make_resize_button(), 0, Wx::ALIGN_CENTER_HORIZONTAL )
    @sizer_button.add( make_delete_button(), 0, Wx::ALIGN_CENTER_HORIZONTAL )
    @sizer_button.add_stretch_spacer

    set_sizer( sizer_top )
    sizer_top.add( @sizer_button, 0, Wx::EXPAND )
    sizer_top.add( @panel = ImagePanel.new( self ), 1, Wx::EXPAND )
    @panel.load_image( path )
  end

  def make_resize_button
    button = Wx::Button.new(self, -1, "Original Size" )
    evt_button( button ) { |e| resize_to_original( e ) }
    button
  end

  def make_delete_button
    button = Wx::Button.new(self, -1, "Delete Me" )
    evt_button( button ) { |e| delete_me( e ) }
    button
  end

  def resize_to_original( evt )
    size = @panel.get_original_size
    size.height += @sizer_button.size.height
    self.set_client_size( size )
  end

  def delete_me( evt )
    dlg = Wx::MessageDialog.new(self, "本当に削除してよろしいですか ?", "Confirm" )
    if dlg.show_modal == Wx::ID_OK
      @parent_service[ :delete ]
      self.close
    end
  end
end

class ImagePanel < Wx::Panel
  OriginalImage = Struct.new( :image, :width, :height )

  def initialize( *args )
    super
    evt_size { |evt| draw_image }
    evt_paint { |evt| draw_image }
  end

  def load_image( file_name )
    image = Wx::Image.new( file_name )
    @org_img = OriginalImage.new( image, image.width, image.height )
    draw_image
  end

  def get_original_size
    Wx::Size.new( @org_img.width, @org_img.height )
  end

  def draw_image
    return unless( @org_img )

    width = self.get_client_size.width
    height = self.get_client_size.height
    ratio_w, ratio_h = width.to_f/@org_img.width, height.to_f/@org_img.height

    new_scale = if ( ratio_w < ratio_h )
      [ width, (@org_img.height * ratio_w).to_i ]
    else
      [ (@org_img.width * ratio_h).to_i, height ]
    end
    bitmap = @org_img.image.copy.rescale( *new_scale ).convert_to_bitmap
    position = [ (width - new_scale.first)/2, (height - new_scale.last)/2 ]
    paint do |dc|
      dc.clear
      dc.draw_bitmap( bitmap, position.first, position.last, true )
    end
  end
end

MyApp.new.main_loop

まず、@thumb_list に要素を追加・登録する段階で、ListItem#data に固有の ID を設定します。

  ListItemData = Struct.new(:static_index, :path)
  def load_thumbnails(dir)
    image_list = Wx::ImageList.new(THUMB_W, THUMB_H)
    @thumb_list.set_image_list(image_list, Wx::IMAGE_LIST_NORMAL)

    item = Wx::ListItem.new
    glob_pat = %w[jpg png].map{|x| "#{dir}/*.#{x}".gsub(/\\/, '/')}.join("\0")
    Dir.glob( glob_pat ).sort.each do |img_file|
      idx = image_list.add(thumbnail_bitmap(img_file))
      unless ( idx < 0 )
        item.id = idx; item.image = idx
        item.data = ListItemData.new( idx, img_file )
        @thumb_list.insert_item( item )
      end
    end
  end

ID を割り振る段階では、item.id も item.date = ListItemDate.new() も、同じ整数値を設定しますが、前回示したように、item.id は要素を削除すれば自動的に変化します。一方 ListItemData#static_index は明示的に変更しない限り変化しません。

これで、ListItem に、勝手にシステムが書き換えない ID を割り振ることができるようになりました。ViewFrame.new() するときに、この、書き換わらない ID を渡すようにすれば解決なのですが、改めて考えるとここで ID を渡すのもなんだか不恰好です。ViewFrame の方では、自分が表示している画像が、ListCtrl で管理されている ListItem に対応しているなんてコテコテした情報は扱いたくありません。せっかくのオブジェクト指向なので、不要な情報は隠蔽した方が実装も安定します。

そこで、今回は、コテコテした情報操作は全て親に当たる ThumbnailsFrame の方で担当して、ViewFrame の方からは、与えられた call back 関数を呼び出せば、なんだかよくわからないけれどうまく処理される、といった実装にしてみました。つまり、ViewFrame.new() には、親子関係を繋ぐための情報としての ThumbnailsFrame と、call back 関数だけを渡します。ruby ですので、手続き型オブジェクトを call back として渡すのですが、ここで、ViewFrame の instance を作るごとに、手渡す ListItem の instance に専用の手続き型オブジェクトを作って渡します。そうすると、ViewFrame からは、何も考えずに、call back すれば、自分の扱っている ListItem をちゃんと同定して処理してもらえるわけです。

この、「各 ViewFrame に、それぞれの ListItem#data.static_index に対応する専用の手続きオブジェクトを作る」という作業に、関数のカリー化を使用します。

カリー化というのは、関数型言語の世界の言葉 (?) のようですが、n 個の引数を取る関数をベースにして、一つ目の引数の値を決め打ちで固定して、残りの n-1 個の可変引数を取る別の関数を生成する (?) 概念です。

ruby では Proc に、そのものズバリ、Proc#curry という method があります。これを ThumbnailsFrame#thumb_activated() で使用しています。

  def thumb_activated( evt )
    @service ||= ->( item_static_index, service_tag ) {
      target_index = @thumb_list.find { |idx|
        @thumb_list.item( idx ).data.static_index == item_static_index
      }
      unless ( target_index )
        Kernel.raise( "Item with static index : #{item_static_index} dose not exist.\n" )
      end

      target_item = @thumb_list.item( target_index )
      case service_tag
      when :path; target_item.data.path
      when :delete
        path = target_item.data.path
        @thumb_list.delete_item( target_index )
        File.delete( path )
        refresh_thumb_list
      when :id; target_item.id
      when :static_index; item_static_index
      else
        Kernel.raise( "Undefined service tag \"#{service_tag}\" is specified.\n" )
      end
    }
    ViewFrame.new( self, @service.curry[ evt.item.data.static_index ] ).show
  end

@service が、ベースになる Proc オブジェクトです。ViewFrame に渡す ListItem の ID と、ViewFrame から ThumbnailsFrame に要求する機能の tag を引数として受け取ります。これをカリー化して、一番目の引数を、該当する ListItem の ID で固定した手続きオブジェクトを ViewFrame.new() に渡します。こうすると、ViewFrame 側から call back するだけで、該当する ListItem を操作できるし、ViewFrame からは実装を隠蔽できるわけです。

上記のような点を修正したことで、子画面を複数開いてから全部削除していっても、意図通りの画像が削除できるようになりました。

2012年7月 1日 (日)

画像を貼り付ける(5) -削除するとListCtrl がずれる

前回 のコードのままだと、どんな不具合が生じるのか。わかりやすいように画面のキャプチャー画像を用意しました。

まず、script を起動して、image_0 ~ image_9 まで が含まれるフォルダを選択します。(この image_0.jpg ~ image_9.jpg までは、適当に用意したものです) そうすると、このようなサムネイル一覧画面になります。

Thumbnails

画像の下に表示されている文字列は、ThumbnailsFrame#refresh_thumb_list() の中で ListItem#text の値として設定しているものです。

  def refresh_thumb_list
    @thumb_list.each do |i|
      item = @thumb_list.item(i)
      item.text = "current index = #{item.id}"
      @thumb_list.set_item( item )
    end
    @thumb_list.sort_items{ |a,b| a <=> b }
  end

ListItem.id には ListCtrl 内で、その ListItem が何番目であるかという整数値が格納されているので、これを設定しているだけです。

ここで、0 と 4 と 6 のサムネイルをダブルクリックすると、子画面が開きます。それぞれ自由に拡大縮小できます。(ひとつだけ、拡大表示にしてあります)

Num_0 Num_4Num_6_2

各子画面のタイトルバーに、"forked index = " という表示があります。これは、ThumbnailsFrame#thumb_activated() で、子画面クラス ViewFrame.new() を実行するときに、evt.index として渡しているものです。

  def thumb_activated( evt )
    ViewFrame.new( self, evt.index, @thumb_list.item_data( evt.index ) ).show
  end

evt は、サムネイルがダブルクリックされたという情報が渡される ListEvent の instance です。evt.index からは、その ListCtrl の何番目の ListItem がダブルクリックされたかという情報が得られますので、結局、その ListItem#id と同じ情報が入ります。ですから、当然、サムネイル画面の "current index = " という情報と数値は一致します。

ここで、"4" を表示している子画面の [Delete Me] ボタンをクリックすると、確認ダイアログを出した上で、該当する画像ファイルとサムネイルを削除します。ThumbnailsFrane#refresh_thumb_list() を経由して、サムネイル一覧画面も更新されるので、以下のようになります。

Thumbs_new

ThumbnailsFrane#refresh_thumb_list() の中で、item.text を更新していますので、ちゃんと current index も先頭から順にならんでいます。その結果、最初のサムネイル一覧画面では "6" の画像を表示するサムネイルには "current index = 6" と表示されていたものが、削除後には "current index = 5" と表示されています。

ところが、まだ閉じられずに残っている子画面の "6" では、タイトルバーに "forked index = 6; ..." と表示されているのです。これは当然といえば当然です。"6" の子画面を表示している ViewFrame の instance には、自分が動き出してから "4" の画像が削除され、ListCtrl の中身が詰められてしまったという情報が何も伝わっていないからです。

ためしにこの段階で "8" のサムネイルをダブルクリックしてみると、次の子画面が開きます。

Num_8

ViewFrame.new() で、現在の "8" 画像に対する index が正しく伝わっているので、タイトルバーには "forked index = 7" と表示されます。

さて、この状態で、まだ閉じずに残してある "6" の子画面で、[Delete Me] ボタンをクリックすると、どの画像が削除されるでしょうか?

子画面側は、ListCtrl の管理を親である ThumbnailsFrame にまかせているので、親に、index = 6 の要素を削除する依頼を出します。これを受ける ThubnailsFrame#delete_item() では、現在の @thumb_list における index = 6 の要素を削除します。

  def delete_item( index )
    path = @thumb_list.item( index ).data
    @thumb_list.delete_item( index )
    File.delete( path )
    refresh_thumb_list
  end

ご想像の通り、これで実際に削除されるのは "7" の画像になります。

結局、子画面が自分を示すための identifier として使っている ListItem#id は、途中でどんどん動的に変わっていくもので、identifier としては適当でなかったということです。では、何を identifier にすればよいのか、いろいろ考えてとりあえず動かせるようになったコードを、次回掲載します。

« 2012年6月 | トップページ | 2013年7月 »