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年1月 | トップページ | 2012年3月 »

2012年2月の2件の記事

2012年2月 9日 (木)

ようやく1本書き上げました(10) - 一応完結

主だった技術的な問題が整理できたので、残りの機能を実装します。

Img_10_01

[file 出力] と [debug log] のタブに、[Browse] ボタンがありますが、これは、処理結果を出力するファイル名を、キーボードからの直接入力でなく、ファイルダイアログから選択するボタンです。直ぐ上の MyPanelOutOrScan の [Browse] ボタンで、出力先のフォルダを選ぶのと同じです。

この機能は、[file 出力]タブ、[debug log]タブそれぞれの中身を表示している panel に共通になるように、wxFormBuilder を使う際に名前空間をそろえるように配慮しておきました。そのため、共通の親クラスである、MyPanelAbstractOrLogBase に実装すれば、両方のパネルで動作します。

class MyPanelAbstractOrLogBase < MyPanel
  def initialize( *args )
    xrc = args.shift
    super( *args )
    load_subclass( xrc )
    @text_ctrl_file = find_by_name( 'text_ctrl_file' )
    @text_ctrl_view = find_by_name( 'text_ctrl_view' )
    evt_button( find_by_name( 'btn_file_browse' ) ) { |e| browse_file( e ) }

  end
  attr :text_ctrl_file, :text_ctrl_view

  def browse_file( evt )
    dlg = Wx::FileDialog.new( self, 'file 選択', Wx.get_app.get_ui_data( :dir ) )
    if ( dlg.show_modal == Wx::ID_OK )
      @text_ctrl_file.clear
      @text_ctrl_file.set_value( File.basename( dlg.get_path ) )
    end
  end

end

XRC の 'btn_file_browse' という名前の Button を拾い出して、クリックされると browse_file( evt ) にイベントを渡します。MyApp のサービスルーチン MyApp#get_ui_data() 経由で、MyPanelOutOrScan で指定されたフォルダ名を取得し、これを引数としてfile 選択ダイアログを開き、戻り値を TextCtrl に設定しています。

[file 出力]タブ、[debug log]タブには、もうひとつ、[file を出力] というボタンがあります。これらの機能そのものは、アプリケーションの全体的な機能として実装したいので、MyApp#scan_web() と同様に、MyApp# out_abstract_or_log() として実装します。そして、AppGlobalEvent でイベントを受けて実行する形にします。

class MyApp < Wx::App
  def on_init
    XrcUtil.load_resource_relative( 'MyProject2.xrc' )
    @frame_main = FrameMain.new
    evt_app_global( MyEventID::ScanOnly ) { |evt| scan_web( evt ) }
    evt_app_global( MyEventID::OutAbstractSpecified ){ out_abstract_or_log( :abstract ) }
    evt_app_global( MyEventID::OutLogSpecified ){ out_abstract_or_log( :log ) }
    @frame_main.show
  end

  def scan_web( evt )
-中略-

  end

  # note の abstract 或いは log のページに表示された内容(に相当するもの)
  # を UTF-8 変換される前の状態で、指定された file 名で出力する。
  def out_abstract_or_log( type )
    specified_file = get_ui_data(
     (type == :abstract)? :abstract_specified : :log_specified
    )

    if ( specified_file.empty? )
      Wx::MessageDialog.new( @frame_main, "出力 file 名を指定してください.",
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      return
    end

    full_path = if ( File.dirname( specified_file ) == '.' )
      add_file_to_dir( get_ui_data( :dir ), specified_file )
    else
      specified_file
    end

    begin
      File.open( full_path, "w:binary" ){ |f|
        f.print (type == :rd)? @abstract_output : @log
      }
      Wx::MessageDialog.new( @frame_main, "#{specified_file} を出力しました",
        "Message", Wx::OK | Wx::ICON_INFORMATION ).show_modal
    rescue
      Wx::MessageDialog.new( @frame_main, "#{full_path} の出力に失敗しました",
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      raise if ( $DEBUG )
    end
  end


  def get_ui_data( *args ); @frame_main.get_ui_data( *args ); end
  def get_ui_ctrl( *args ); @frame_main.get_ui_ctrl( *args ); end

private
  # path delimiter として '\' と '/' の混在を考慮し
  # directory 名と file 名を連結して、delimiter をすべて '/' に
  # 統一して返す。

  def add_file_to_dir( dir, file )
    if ( dir.empty? )
      file
    else
      dir = dir.gsub( /\\/, '/' ) if dir.include?( '\\' )
      ( dir[-1] == '/' )? dir + file : dir + '/' + file
    end
  end

end

これで、イベントを受ける側はできましたから、後は panel 側からイベントを投げられるようにするだけです。

class MyPanelAbstract < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_abstract', *args )
    evt_button( find_by_name( 'btn_out_file' ) ){
      AppGlobalEvent.new( MyEventID::OutAbstractSpecified ).throw_from( self )
    }

  end
end

class MyPanelLog < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_log', *args )
    evt_button( find_by_name( 'btn_out_file' ) ){
      AppGlobalEvent.new( MyEventID::OutLogSpecified ).throw_from( self )
    }

  end
end

最後に残ったボタンは、MyPanelOutOrScan の [Scan → Default file で出力] です。一気に、このボタンにもイベントを割り付けましょう。

class MyApp < Wx::App
  def on_init
    XrcUtil.load_resource_relative( 'MyProject2.xrc' )
    @frame_main = FrameMain.new
    evt_app_global( MyEventID::ScanOnly ) { |evt| scan_web( evt ) }
    evt_app_global( MyEventID::OutAbstractSpecified ){ out_abstract_or_log( :abstract ) }
    evt_app_global( MyEventID::OutLogSpecified ){ out_abstract_or_log( :log ) }
    evt_app_global( MyEventID::ScanAndOutAbstractDefault ){ |evt|
      is_success = scan_web( evt )
      out_abstract_or_log( :abstract ) if ( is_success )
    }


    @frame_main.show
  end
-中略-

end

class MyPanelOutOrScan < MyPanel
  def initialize( *args )
    super
    load_subclass( 'panel_out_or_scan' )
    @text_ctrl_dir = find_by_name( 'text_ctrl_dir' )
    evt_button( find_by_name( 'btn_browse_dir' ) ) { |e| browse_dir( e ) }
    evt_button( find_by_name( 'btn_scan_only' ) ) { |e|
      AppGlobalEvent.new( MyEventID::ScanOnly ).throw_from( self )
    }
    evt_button( find_by_name( 'btn_out_default_file' ) ) { |e|
      AppGlobalEvent.new( MyEventID::ScanAndOutAbstractDefault ).throw_from( self )
    }

  end
-中略-

end

これで、 [Scan → Default file で出力] をクリックすると、web を scan して、すぐに abstract file を出力します。

以下に全ソースを掲載します。これで下請けの sub_a.rb さえ作りこめば出版社の web site から好きに情報を取り出せます。(もちろん、この下請けの作りこみの方がずっと面倒なのですが)


GUI_10a.rb


# encoding: utf-8

require "wx"

SUBs = Dir.glob( File.dirname( __FILE__ ) + '/sub*.rb' )
SUBs.each{ |s| require_relative s }
Journals = SUBs.map{ |s| File.basename( s, '.*' ).upcase }.map{ |j| self.class.const_get(j).new }

# XmlResource を利用するための function
module XrcUtil
module_function
  # Wx::App で最初に xrc file を取り込む function.
  # この function を実行する source file からの相対 path で指定する。
  def load_resource_relative( xrc )
    Wx::XmlResource.get.load( File.dirname( __FILE__ ) + '/' + xrc )
  end

  # Wx::Window を継承した object で、xrc 内で定義されているものを
  # 'name' field の値で識別して返す。
  def find_by_name( name )
    Wx::Window.find_window_by_name( name, self )
  end

  # もともとの load_subclass method が、load する window の種類に
  # よって使い分けを要求されるため、自動判別するために wrap する。
  def load_subclass( name, parent = nil )
    case self
    when Wx::Dialog
      Wx::XmlResource.get.load_dialog_subclass( self, parent, name )
    when Wx::Frame
      Wx::XmlResource.get.load_frame_subclass( self, parent, name )
    when Wx::Wizard
      Wx::XmlResource.get.load_wizard_subclass( self, parent, name )
    when Wx::Panel
      Wx::XmlResource.get.load_panel_subclass( self, parent, name )
    end
  end
end

class AppGlobalEvent < Wx::CommandEvent
  EVT_TYPE = Wx::EvtHandler.register_class(self, nil, 'evt_app_global', 1)

  def initialize( evt_id )
    super(EVT_TYPE)
    self.id = evt_id
  end

  def throw_from( me )
    me.event_handler.process_event(self)
  end
end

module MyEventID
  (<<-_TYPE).split(' ').each_with_index{ |e,i| const_set( e, i + 1 ) }
    ScanAndOutAbstractDefault ScanOnly OutAbstractSpecified OutLogSpecified
  _TYPE
end

class MyApp < Wx::App
  def on_init
    XrcUtil.load_resource_relative( 'MyProject2.xrc' )
    @frame_main = FrameMain.new
    evt_app_global( MyEventID::ScanOnly ) { |evt| scan_web( evt ) }
    evt_app_global( MyEventID::OutAbstractSpecified ){ out_abstract_or_log( :abstract ) }
    evt_app_global( MyEventID::OutLogSpecified ){ out_abstract_or_log( :log ) }
    evt_app_global( MyEventID::ScanAndOutAbstractDefault ){ |evt|
      is_success = scan_web( evt )
      out_abstract_or_log( :abstract ) if ( is_success )
    }

    @frame_main.show
  end

  # 指定された url を scan し abstract file と、debug log を取得。
  # 取得内容を note のそれぞれのページに、UTF-8 変換して表示。
  # abstract および log の出力 file 名入力欄に dafault file 名を設定。
  def scan_web( evt )
    journal = Journals[ get_ui_data( :select_journal_index ) ]
    url = get_ui_data( :url )
    if ( url.empty? )
      Wx::MessageDialog.new( @frame_main, "url を指定してください.", "Error",
        Wx::OK | Wx::ICON_ERROR ).show_modal
      return( false )
    end

    begin
      journal.scan_web_page( url )
    rescue
      Wx::MessageDialog.new( @frame_main, $!.inspect,
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      raise if ( $DEBUG )
      get_ui_ctrl( :text_view_abstract ).clear
      get_ui_ctrl( :text_specify_abstract ).clear
      get_ui_ctrl( :text_view_log ).clear
      get_ui_ctrl( :text_specify_log ).clear
      return( false )
    end

    @abstract_output = journal.abstract
    abstract = get_ui_ctrl( :text_view_abstract )
    abstract.clear; abstract.change_value( NKF.nkf( '-w', @abstract_output ) )
    get_ui_ctrl( :text_specify_abstract ).set_value( journal.default_out_file_name )

    @log = journal.log
    log = get_ui_ctrl( :text_view_log )
    log.clear; log.change_value( NKF.nkf( '-w', @log ) )
    get_ui_ctrl( :text_specify_log ).set_value( 'scan.log' )
    true
  end

  # note の abstract 或いは log のページに表示された内容(に相当するもの)
  # を UTF-8 変換される前の状態で、指定された file 名で出力する。
  def out_abstract_or_log( type )
    specified_file = get_ui_data(
     (type == :abstract)? :abstract_specified : :log_specified
    )

    if ( specified_file.empty? )
      Wx::MessageDialog.new( @frame_main, "出力 file 名を指定してください.",
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      return
    end

    full_path = if ( File.dirname( specified_file ) == '.' )
      add_file_to_dir( get_ui_data( :dir ), specified_file )
    else
      specified_file
    end

    begin
      File.open( full_path, "w:binary" ){ |f|
        f.print (type == :rd)? @abstract_output : @log
      }
      Wx::MessageDialog.new( @frame_main, "#{specified_file} を出力しました",
        "Message", Wx::OK | Wx::ICON_INFORMATION ).show_modal
    rescue
      Wx::MessageDialog.new( @frame_main, "#{full_path} の出力に失敗しました",
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      raise if ( $DEBUG )
    end
  end

  def get_ui_data( *args ); @frame_main.get_ui_data( *args ); end
  def get_ui_ctrl( *args ); @frame_main.get_ui_ctrl( *args ); end

private
  # path delimiter として '\' と '/' の混在を考慮し
  # directory 名と file 名を連結して、delimiter をすべて '/' に
  # 統一して返す。
  def add_file_to_dir( dir, file )
    if ( dir.empty? )
      file
    else
      dir = dir.gsub( /\\/, '/' ) if dir.include?( '\\' )
      ( dir[-1] == '/' )? dir + file : dir + '/' + file
    end
  end
end

class FrameMain < Wx::Frame
  def initialize
    super( nil, -1, $0, :size => [450,500] )
    set_background_colour( Wx::Colour.new( 240, 240, 255 ) )
    sizer_top = Wx::BoxSizer.new( Wx::VERTICAL )
    set_sizer( sizer_top )

    sizer_top.add( @panel_url = MyPanelUrl.new( self ), 0, Wx::EXPAND )
    sizer_top.add( @rbox_select_journal = make_rbox, 0,
      Wx::LEFT | Wx::RIGHT | Wx::EXPAND, 5 )
    sizer_top.add( @panel_out_or_scan = MyPanelOutOrScan.new( self ), 0, Wx::EXPAND )
    sizer_top.add( make_view_note, 1, Wx::EXPAND )
  end

  def make_rbox
    Wx::RadioBox.new( self, -1, 'Select Journal',
      :choices => Journals.map{ |j| j.name.encode(__ENCODING__) },
      :style => Wx::RA_SPECIFY_COLS, :major_dimension => 2
    )
  end

  def make_view_note
    note = Wx::Notebook.new( self, -1 )
    note.add_page( @panel_abstract = MyPanelAbstract.new( note, -1 ), 'file 出力', true )
    note.add_page( @panel_log = MyPanelLog.new( note, -1 ), 'debug log', false )
    note
  end

  # Frame 自身の control 或いは、Frame に貼り付けた Panel などの
  # 下部構造に属する contol のうち、外部から読み出しを要するものに
  # 対する access を集約する method. 書き込みはできない。
  def get_ui_data( tag )
    case tag
    when :url; @panel_url.text_ctrl_url.get_value

    when :select_journal_name; @rbox_select_journal.get_string_selection
    when :select_journal_index; @rbox_select_journal.get_selection

    when :dir; @panel_out_or_scan.text_ctrl_dir.get_value

    when :abstract_specified; @panel_abstract.text_ctrl_file.get_value
    when :log_specified; @panel_log.text_ctrl_file.get_value
    else; nil
    end
  end

  # Frame 自身の control 或いは、Frame に貼り付けた Panel などの
  # 下部構造に属する contol のうち、外部から書き込みを要するものに
  # 対する access を集約する method.
  # 読み出しのみでよいものは、get_ui_data で。
  def get_ui_ctrl( tag )
    case tag
    when :text_specify_abstract; @panel_abstract.text_ctrl_file
    when :text_specify_log; @panel_log.text_ctrl_file
    when :text_view_abstract; @panel_abstract.text_ctrl_view
    when :text_view_log; @panel_log.text_ctrl_view
    else; nil
    end
  end
end

class MyPanel < Wx::Panel
  include XrcUtil
end

class MyPanelUrl < MyPanel
  def initialize( *args )
    super
    load_subclass( 'panel_url' )
    @text_ctrl_url = find_by_name( 'text_ctrl_url' )

    evt_button( find_by_name( 'btn_url_paste' ) ){ |evt|
      @text_ctrl_url.clear
      @text_ctrl_url.paste
    }
  end
  attr :text_ctrl_url
end

class MyPanelOutOrScan < MyPanel
  def initialize( *args )
    super
    load_subclass( 'panel_out_or_scan' )
    @text_ctrl_dir = find_by_name( 'text_ctrl_dir' )
    evt_button( find_by_name( 'btn_browse_dir' ) ) { |e| browse_dir( e ) }
    evt_button( find_by_name( 'btn_scan_only' ) ) { |e|
      AppGlobalEvent.new( MyEventID::ScanOnly ).throw_from( self )
    }
    evt_button( find_by_name( 'btn_out_default_file' ) ) { |e|
      AppGlobalEvent.new( MyEventID::ScanAndOutAbstractDefault ).throw_from( self )
    }
  end
  attr :text_ctrl_dir

  def browse_dir( evt )
    dlg = Wx::DirDialog.new( self, 'Directory 選択', @text_ctrl_dir.get_value )
    if ( dlg.show_modal == Wx::ID_OK )
      @text_ctrl_dir.clear
      @text_ctrl_dir.set_value( dlg.get_path )
    end
  end
end

# note の abstract および log 表示ページ用の Panel
# 処理内容が殆ど共通なので、それぞれのページで異なる
# 部分のみを、ここから継承して記述を加える。
class MyPanelAbstractOrLogBase < MyPanel
  def initialize( *args )
    xrc = args.shift
    super( *args )
    load_subclass( xrc )
    @text_ctrl_file = find_by_name( 'text_ctrl_file' )
    @text_ctrl_view = find_by_name( 'text_ctrl_view' )
    evt_button( find_by_name( 'btn_file_browse' ) ) { |e| browse_file( e ) }
  end
  attr :text_ctrl_file, :text_ctrl_view

  def browse_file( evt )
    dlg = Wx::FileDialog.new( self, 'file 選択', Wx.get_app.get_ui_data( :dir ) )
    if ( dlg.show_modal == Wx::ID_OK )
      @text_ctrl_file.clear
      @text_ctrl_file.set_value( File.basename( dlg.get_path ) )
    end
  end
end

class MyPanelAbstract < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_abstract', *args )
    evt_button( find_by_name( 'btn_out_file' ) ){
      AppGlobalEvent.new( MyEventID::OutAbstractSpecified ).throw_from( self )
    }
  end
end

class MyPanelLog < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_log', *args )
    evt_button( find_by_name( 'btn_out_file' ) ){
      AppGlobalEvent.new( MyEventID::OutLogSpecified ).throw_from( self )
    }
  end
end

my_app = MyApp.new
my_app.main_loop

これで、ようやく1本書き上げました。

2012年2月 5日 (日)

ようやく1本書き上げました(9) - application 全体に関係するイベントの別案

前回は、application 全体に関与する処理をグローバル変数あるいは Wx.get_app() を経由して、MyApp 内の担当 method を直接呼び出す形でイベントハンドリングしました。

しかし、なんとなくかっこよくありません。もう少しエレガントな方法(にこだわりだすと大抵はまってしまうのですが)がないものかと考えてみました。

いつも参考にさせていただいている「wxRubyでGUIプログラミング」のこちらのページ が、ひとつのよい例です。この例では、application の終了方法が 2 つ用意されています。ひとつは window 上隅の close ボタン、もうひとつはメニューから 'QUIT' を選ぶ方法です。

close ボタンをクリックすると、システムレベルで EVT_CLOSE イベントが発生します。メニューから 'QUIT' を選ぶと、メニューからイベントが発生し、そのイベントのハンドラ内からユーザーレベルで EVT_CLOSE イベントを発生させます。どちらの EVT_CLOSE イベントも MyApp の中で拾い上げられて処理ルーチンが動き出します。

やっぱり、複数の場所から共通して呼び出すルーチンは、このようなイベント - ハンドラシステムで駆動したいものです。システムレベルで、このようなインターフェイスを用意していれば、楽にイベントを管理できます。ボタンやメニューなどシステムに組み込まれているイベント処理が、CPU のハードウェア割り込みに相当するとすれば、ユーザーレベルのイベントシステムを経由した処理ルーチンの呼び出しは、ソフトウェア割り込みのようなものです。

wxRuby のドキュメント をあちこち調べると、どうやら Wx::CommandEvent を継承して、新しいイベントクラスを作るのがよさそうです。既存のイベントクラスを流用できないかとも考えたのですが、ID の重複を避けるのが簡単ではないようなので、新しいものを作ることにしました。BwxRuby を install したときに、lib の下の方にサンプルスクリプトが install されていますが、そのうちの samples/event/event.rb が大変参考になります。これを踏まえて、以下のようなクラスを作りました。

class AppGlobalEvent < Wx::CommandEvent
  EVT_TYPE = Wx::EvtHandler.register_class(self, nil, 'evt_app_global', 1)

  def initialize( evt_id )
    super(EVT_TYPE)
    self.id = evt_id
  end

  def throw_from( me )
    me.event_handler.process_event(self)
  end
end

イベント処理の内部では、ボタンやメニュー、マウスなど、それぞれのイベントの種類の区別を、それぞれ固有の整数値で区別しているようで、Event.new_event_type() で未使用の整数値を確保して使うようです。これを、上のコードの Wx::EvtHandler.register_class() の第二引数に与えるのですが、nil にしておくと、内部で自動的に Event.new_event_type() が call されます。第三引数は、イベントを拾い上げる側の method 名を文字列で与えます。この名前を使って、evt_button( ID ){ ... } のように、このユーザー定義イベントを拾うことができます。

このクラスを使ってイベント処理をすると、前回のソースコードはこのようになります。


GUI_09a.rb


# encoding: utf-8

require "wx"

SUBs = Dir.glob( File.dirname( __FILE__ ) + '/sub*.rb' )
SUBs.each{ |s| require_relative s }
Journals = SUBs.map{ |s| File.basename( s, '.*' ).upcase }.map{ |j| self.class.const_get(j).new }

# XmlResource を利用するための function
module XrcUtil
module_function
  # Wx::App で最初に xrc file を取り込む function.
  # この function を実行する source file からの相対 path で指定する。
  def load_resource_relative( xrc )
    Wx::XmlResource.get.load( File.dirname( __FILE__ ) + '/' + xrc )
  end

  # Wx::Window を継承した object で、xrc 内で定義されているものを
  # 'name' field の値で識別して返す。
  def find_by_name( name )
    Wx::Window.find_window_by_name( name, self )
  end

  # もともとの load_subclass method が、load する window の種類に
  # よって使い分けを要求されるため、自動判別するために wrap する。
  def load_subclass( name, parent = nil )
    case self
    when Wx::Dialog
      Wx::XmlResource.get.load_dialog_subclass( self, parent, name )
    when Wx::Frame
      Wx::XmlResource.get.load_frame_subclass( self, parent, name )
    when Wx::Wizard
      Wx::XmlResource.get.load_wizard_subclass( self, parent, name )
    when Wx::Panel
      Wx::XmlResource.get.load_panel_subclass( self, parent, name )
    end
  end
end

class AppGlobalEvent < Wx::CommandEvent
  EVT_TYPE = Wx::EvtHandler.register_class(self, nil, 'evt_app_global', 1)

  def initialize( evt_id )
    super(EVT_TYPE)
    self.id = evt_id
  end

  def throw_from( me )
    me.event_handler.process_event(self)
  end
end

module MyEventID
  (<<-_TYPE).split(' ').each_with_index{ |e,i| const_set( e, i + 1 ) }
    ScanAndOutAbstractDefault ScanOnly OutAbstractSpecified OutLogSpecified
  _TYPE
end


class MyApp < Wx::App
  def on_init
    XrcUtil.load_resource_relative( 'MyProject2.xrc' )
    @frame_main = FrameMain.new
    evt_app_global( MyEventID::ScanOnly ) { |evt| scan_web( evt ) }

    @frame_main.show
  end

  # 指定された url を scan し、abstract file と、debug log を取得。
  # 取得内容を note のそれぞれのページに、UTF-8 変換して表示。
  # abstract および log の出力 file 名入力欄に dafault file 名を設定。
  def scan_web( evt )
    journal = Journals[ get_ui_data( :select_journal_index ) ]
    url = get_ui_data( :url )
    if ( url.empty? )
      Wx::MessageDialog.new( @frame_main, "url を指定してください.", "Error",
        Wx::OK | Wx::ICON_ERROR ).show_modal
      return( false )
    end

    begin
      journal.scan_web_page( url )
    rescue
      Wx::MessageDialog.new( @frame_main, $!.inspect,
        "Error", Wx::OK | Wx::ICON_ERROR ).show_modal
      raise if ( $DEBUG )
      get_ui_ctrl( :text_view_rd ).clear
      get_ui_ctrl( :text_specify_rd ).clear
      get_ui_ctrl( :text_view_log ).clear
      get_ui_ctrl( :text_specify_log ).clear
      return( false )
    end

    @abstract_output = journal.abstract

    abstract = get_ui_ctrl( :text_view_abstract )
    abstract.clear; abstract.change_value( NKF.nkf( '-w', @abstract_output ) )
    get_ui_ctrl( :text_specify_abstract ).set_value( journal.default_out_file_name )

    log = get_ui_ctrl( :text_view_log )
    log.clear; log.change_value( NKF.nkf( '-w', journal.log ) )
    get_ui_ctrl( :text_specify_log ).set_value( 'scan.log' )
    true
  end

  def get_ui_data( *args ); @frame_main.get_ui_data( *args ); end
  def get_ui_ctrl( *args ); @frame_main.get_ui_ctrl( *args ); end
end

class FrameMain < Wx::Frame
  def initialize
    super( nil, -1, $0, :size => [450,500] )
    set_background_colour( Wx::Colour.new( 240, 240, 255 ) )
    sizer_top = Wx::BoxSizer.new( Wx::VERTICAL )
    set_sizer( sizer_top )

    sizer_top.add( @panel_url = MyPanelUrl.new( self ), 0, Wx::EXPAND )
    sizer_top.add( @rbox_select_journal = make_rbox, 0,
      Wx::LEFT | Wx::RIGHT | Wx::EXPAND, 5 )
    sizer_top.add( @panel_out_or_scan = MyPanelOutOrScan.new( self ), 0, Wx::EXPAND )
    sizer_top.add( make_view_note, 1, Wx::EXPAND )
  end

  def make_rbox
    Wx::RadioBox.new( self, -1, 'Select Journal',
      :choices => Journals.map{ |j| j.name.encode(__ENCODING__) },
      :style => Wx::RA_SPECIFY_COLS, :major_dimension => 2
    )
  end

  def make_view_note
    note = Wx::Notebook.new( self, -1 )
    note.add_page( @panel_abstract = MyPanelAbstract.new( note, -1 ), 'file 出力', true )
    note.add_page( @panel_log = MyPanelLog.new( note, -1 ), 'debug log', false )
    note
  end

  # Frame 自身の control 或いは、Frame に貼り付けた Panel などの
  # 下部構造に属する contol のうち、外部から読み出しを要するものに
  # 対する access を集約する method. 書き込みはできない。
  def get_ui_data( tag )
    case tag
    when :url; @panel_url.text_ctrl_url.get_value

    when :select_journal_name; @rbox_select_journal.get_string_selection
    when :select_journal_index; @rbox_select_journal.get_selection

    when :dir; @panel_out_or_scan.text_ctrl_dir.get_value

    when :abstract_specified; @panel_abstract.text_ctrl_file.get_value
    when :log_specified; @panel_log.text_ctrl_file.get_value
    else; nil
    end
  end

  # Frame 自身の control 或いは、Frame に貼り付けた Panel などの
  # 下部構造に属する contol のうち、外部から書き込みを要するものに
  # 対する access を集約する method.
  # 読み出しのみでよいものは、get_ui_data で。
  def get_ui_ctrl( tag )
    case tag
    when :text_specify_abstract; @panel_abstract.text_ctrl_file
    when :text_specify_log; @panel_log.text_ctrl_file
    when :text_view_abstract; @panel_abstract.text_ctrl_view
    when :text_view_log; @panel_log.text_ctrl_view
    else; nil
    end
  end
end

class MyPanel < Wx::Panel
  include XrcUtil
end

class MyPanelUrl < MyPanel
  def initialize( *args )
    super
    load_subclass( 'panel_url' )
    @text_ctrl_url = find_by_name( 'text_ctrl_url' )

    evt_button( find_by_name( 'btn_url_paste' ) ){ |evt|
      @text_ctrl_url.clear
      @text_ctrl_url.paste
    }
  end
  attr :text_ctrl_url
end

class MyPanelOutOrScan < MyPanel
  def initialize( *args )
    super
    load_subclass( 'panel_out_or_scan' )
    @text_ctrl_dir = find_by_name( 'text_ctrl_dir' )
    evt_button( find_by_name( 'btn_browse_dir' ) ) { |e| browse_dir( e ) }
    evt_button( find_by_name( 'btn_scan_only' ) ) { |e|
      AppGlobalEvent.new( MyEventID::ScanOnly ).throw_from( self )

    }
  end
  attr :text_ctrl_dir

  def browse_dir( evt )
    dlg = Wx::DirDialog.new( self, 'Directory 選択', @text_ctrl_dir.get_value )
    if ( dlg.show_modal == Wx::ID_OK )
      @text_ctrl_dir.clear
      @text_ctrl_dir.set_value( dlg.get_path )
    end
  end
end

# note の abstract および log 表示ページ用の Panel
# 処理内容が殆ど共通なので、それぞれのページで異なる
# 部分のみを、ここから継承して記述を加える。
class MyPanelAbstractOrLogBase < MyPanel
  def initialize( *args )
    xrc = args.shift
    super( *args )
    load_subclass( xrc )
    @text_ctrl_file = find_by_name( 'text_ctrl_file' )
    @text_ctrl_view = find_by_name( 'text_ctrl_view' )
  end
  attr :text_ctrl_file, :text_ctrl_view
end

class MyPanelAbstract < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_abstract', *args )
  end
end

class MyPanelLog < MyPanelAbstractOrLogBase
  def initialize( *args )
    super( 'panel_log', *args )
  end
end

my_app = MyApp.new
my_app.main_loop

MyPanelOutOrScan 内で 'btn_scan_only' ボタンのクリックイベントを受け取ると、AppGlobalEvent.new() で、そのイベント ID がMyEventID::ScanOnly であるユーザーイベントを発生させます。AppGlobalEvent はユーザー定義イベントなので、システム側で勝手に発生することは有り得ず、そのためたまたまシステムが発生させたイベント ID が MyEventID::ScanOnly と重なる AppGlobalEvent を引っ掛けてしまう恐れもありません。ユーザー側で、使いたいイベントに(固有でありさえすれば)好きな整数値を割り付けることができます。

発生させたイベントをシステムに投げるには、イベントを発生させたオブジェクトの中でevent_handler.process_event( イベント ) を実行します。長ったらしいので、AppGlobalEvent に throw_from() という wrapper method を作りましたが、引数として呼び出し元の self を渡さないとならないので、あまり楽になった気はしません。(ある method を呼び出した側のオブジェクトを知る方法がわからなかったので…、呼び出し側 method なら caller() でわかるのですが)

throw_from() で投げた AppGlobalEvent は、MyApp#initialize() 内の、evt_app_global( MyEventID::ScanOnly ){ } で拾い上げて、MyApp#scan_web() につなげます。

前回の最後にも書きましたが、こんな手の込んだことをしなくても、get_app() 経由で直接 scan_web() を呼び出せば十分なのですが、CommandEvent を継承した Event 経由での呼び出しには、ひとつ利点がありそうです。

Event handing overview のページに書いてありますが、CommandEvent を発生させてシステムに投げると、そのイベントは、Wx::オブジェクトの親子関係を遡って伝播していきます。親子関係は、Frame や Panel, Button などを new するときに、第一引数として親を知らせる形で木構造を構築します。

MyPanelOutOrScan で発生した AppGlobalEvent は、MyPanelOutOrScan 自身にまず伝わり、次に FrameMain に伝わり、最後に top の application である MyApp に伝わります。この伝播順の途中でイベントを拾い上げればよいので、今回は MyApp#initialize() に evt_app_global() を記述しましたが、FrameMain#initialize() に evt_app_global() を記述してもよいし、(意味はないですが) MyPanelOutOrScan に evt_app_global() を記述してもイベントは拾えます。

この仕組みが便利に使えそうだと思うのは、たとえば、TopApp の下に、同じ画面デザインの Frame_A と Frame_B がぶら下がるようなアプリケーションを書いた場合です。Frame_A/B 両方で、Event_1 と Event_2 が発生するとして、Event_1 は Frame_A/B に共通の処理を、Event_2 は Frame_A/B で別の処理をしたい場合、evt_app_global( Event_1 ) は TopApp 内に、evt_app_global( Event_2 )は、Frame_A と Frame_B の内部にそれぞれ別に記述すれば、Event を発生させる部分の記述には注意を要しません。或いは、Event_2 も TopApp で拾うように記述しておいて、Frame_A だけ、Frame_A 内に evt_app_global( Event_2 ) を追加すれば、イベントの伝播を intercept できます。呼び出し元の記述を一切変更せずに、比較的に柔軟な変更ができそうです。

今回作っているアプリケーションでは、ここまで複雑なことは必要としませんが、このまま AppGlobalEvent を使った形で実装を進めて行きたいと思います。

« 2012年1月 | トップページ | 2012年3月 »