新規作成  編集  Ruby-GNOME2 Project Website  ページ一覧  検索  更新履歴  編集履歴  RSS  ログイン

libglade2-tut-create-src

テンプレートからソースコード(Rubyスクリプト)へ

さて、後はテンプレートを編集していきます。というのは半分正解で半分不正解です。

前章でも

「"xxxx is not implemented yet."と表示されるようになっていますので自分で実装したいところから書き換えて行くイメージ」

と書きました。実際それで動きますし、Ruby-GNOME2-0.9.1までは確かにその方法が一般的(?)でした。 でも、その方法だと「機能追加などでGUIを変更したとき、hwedit_glade.rbを毎回作り直す(バックアップを取っておいてシグナルハンドラ部分を書き直す」というような「手動マージ作業」を行う必要があります。 もっと複雑なこと、たとえば自前の定数を書いてみたり、メソッドを追加してみたりするとなおさらマージ作業が大変になりますし、Ruby/Libglade2がバージョンアップしたときに自動的に追加されるメソッドが増えるかもしれません。 このように、ruby-glade-create-templateは最初の1回のテンプレート生成だけは使えるのですが、2度目以降はあまり使えないツールになってしまい、とたんに生産性が落ちてしまいます。

そこで、ここでは、hwedit_glade.rbに触れることなく別ファイルにソースコードを書いておくようにして、少しでもその辺の面倒くささを回避する方法を推奨します(Ruby-GNOME2-0.10.0以降)。

まずはコピペ

一番最初にhwedit_glade.rbを生成したとき、これをコピーして、(この例では)hwedit.rbという名前にします。 次に、このファイルを編集します。以下に例を示します。(変更部分のみ)

require 'hwedit_glade'

class Hwedit < HweditGlade 
#  include GetText

#  attr :glade

冒頭を、hwedit_glade.rbを読み込んで実行しHweditGladeのサブクラスを定義する形に書き換えます。コメントアウトした部分は不要なので削除します。

def initialize(path_or_data, root = nil, domain = nil, localedir = nil, flag = GladeXML::FILE)
  super(path_or_data, root, domain, localedir, flag)
end

ここは親クラスのinitializeメソッドを呼び出すように書き換えるだけでOKです。

def on_main_window_delete_event(widget, arg0)
  puts "on_main_window_delete_event"		# 必要なくなったら消す
  false
end
def on_main_window_destroy(widget)
  puts "on_main_window_destroy"
  Gtk.main_quit
end

プライマリ(メイン)ウィンドウを作る」のページで説明したように、プログラムのウィンドウのクローズボタンをクリックしたときに正しくプログラムを終了させるのに最低限必要な処理を書きます。(今の時点ではon_main_window_delete_eventは変更しなくても動作しますが)

要はinitializeとシグナルハンドラを上書きするようなサブクラスを作るわけです。 この部分だけ、きっと手間に感じると思いますが、一度作れば後は楽チンなこと間違いなしです。

end

# Main program
if __FILE__ == $0
  # Set values as your own application. 
  PROG_PATH = "hwedit.glade"
  PROG_NAME = "hwedit"
  Hwedit.new(PROG_PATH, nil, PROG_NAME)
  Gtk.main
end

PROG_PATHとPROG_NAMEは適切な値に書き直します。また、HweditGlade.newとなっている行もHwedit.newに変更します。

PROG_PATHはhwedit.gladeを置くPATHです。上記例ではこのツールを実行するディレクトリに置いているのでそのままにしてありますが、実際にアプリケーションとして公開する場合はちょっと考えなければなりません。 一般的にはMS Windowsであればc:\ruby\share\hwedit\glade\配下、Linux/FreeBSDであれば、/usr/share/hwedit/gladeに置けば良いでしょう。

これらの値は以下のようにすれば取得できます。

require 'rbconfig'
datadir = Config::CONFIG["datadir"]

したがって、先のPROG_PATHはこれを指定してしまうというのも一案でしょう。 私はこの辺の設定周りは別ファイル化(config.rbとか)にして、インストーラを起動するときに自動的に生成するようにしています。

PROG_NAMEは地域化用データのファイル名(拡張子を除く)として扱われます。ここではプログラム名を代入しています。

シグナルハンドラの実装

さて、後は気の向くままにシグナルハンドラたちを実装しましょう。@glade["textview1"]というような形でそれぞれウィジェットのインスタンスを呼び出すことができます。"textview1"はGlade-2上で設定した(あるいはデフォルトのままではウィジェット + 番号という形になる)ウィジェット名です。 ただ、いちいちそのように書くのも手間なので良く使うウィジェットはインスタンス変数に代入しておくと便利です。

例えばこんな感じです。

def initialize(path_or_data, root = nil, domain = nil, localedir = nil, flag = GladeXML::FILE)
  super(path_or_data, root, domain, localedir, flag)

  @window = @glade['main_window']
  @editor = @glade['textview1']
  @filedialog = @glade['filechooser']
  @aboutdialog = @glade['aboutdialog']
end

以下、シグナルハンドラの実装例です。

"Edit"メニュー

まずは簡単な所で、"Cut(切り取り)"、"Copy(コピー)"、"Paste(貼り付け)"コマンドを実装してみます。

def on_paste1_activate(widget)
  puts "on_paste1_activate"
  @editor.paste_clipboard
end

def on_copy1_activate(widget)
  puts "on_copy1_activate"
  @editor.copy_clipboard
end

def on_cut1_activate(widget)
  puts "on_cut1_activate"
  @editor.cut_clipboard
end

以上(笑)。Gtk::TextViewのおかげです。ちなみにTextViewウィジェットの右クリック(コンテキスト)メニュー内のコマンドは実装する必要すらありません。デフォルトで動作します。

"Help"メニュー

次は、"About"コマンドを選択した時に、Gladeで作っておいたアバウトダイアログを表示する例です。

def on_about1_activate(widget)
  puts "on_about1_activate"
  @aboutdialog.run do |response|
    case response
    when Gtk::Dialog::RESPONSE_DELETE_EVENT
      puts "RESPONSE_DELETE_EVENT"
    when Gtk::Dialog::RESPONSE_CLOSE
      puts "RESPONSE_CLOSE"
    end
  end
  @aboutdialog.hide
end

Gtk::Dialog#runは、ダイアログ上で何らかの"response" Signalが発生するまで待機し、Signalを受け取ると関連付けられたブロックを実行してメソッドを抜けます。"response" Signalとは、"response ID"が設定されたボタンが押されるか、またはダイアログ枠のクローズボタン(これを押したというイベントにも"response ID"が設定されています)が押された時に発生するSignalです。"response ID"はブロック引数として渡されるので、ブロック内でこの値に応じた処理を行うことができます。

コード中の定数は事前定義されたIDです。「Gtk2チュートリアル」の「ダイアログ」のページなどに一覧があります。Gtk::Dialog::RESPONSE_CLOSEはGlade上でボタンのプロパティに表示されるものと違いますが、あれをそのまま使うと、定義されていないというエラーが出てしまうので、Gtk::Dialog::RESPONSE_??の方を使うのが簡単です。個々のIDの対応はすぐわかると思います。*1

ただアバウトダイアログの場合、"response ID"の値によって処理を分ける必要はないので、以下のようなコードで済ませてしまうのが普通です。

def on_about1_activate(widget)
  puts "on_about1_activate"
  @aboutdialog.run
  @aboutdialog.hide
end

Gtk::Dialog#runは、どのボタンを押してもダイアログが閉じる処理の場合には便利ですが、そうでない場合、他のウィジェットと同じ方法でシグナルハンドラを設定することもできます。詳しくは「Gtk2チュートリアル」の「ダイアログ」のページを見て下さい。


以後、"File"メニュー内のコマンドを実装していきます。

"Quit(終了)"コマンド

def on_quit1_activate(widget)
  puts "on_quit1_activate"
  unless @window.signal_emit('delete_event', nil)
    @window.signal_emit('destroy')
  end
end

"Quit(終了)"コマンドが選択された場合、ウィンドウのクローズボタンが押された時に自動で行われる処理を明示的に実行します。まず、"delete_event"シグナルを自分で発生させ、その戻り値がfalseの場合さらに"destroy"シグナルを発生させます。

ウィンドウを閉じようとした時やプログラムの終了時に行いたいことは、"on_main_window_delete_event"メソッドや"on_main_window_destroy"メソッド内で実行します。

"New(新規)"コマンド

require 'hwedit_glade'
require 'kconv'			# 追加

class Hwedit < HweditGlade

  DEFAULT_FILECHARSET = Kconv::SJIS	# 追加


def on_new1_activate(widget)
  puts "on_new1_activate"
  @filename = nil			# 編集中テキストのファイルパス情報をクリア
  @filecharset = DEFAULT_FILECHARSET	# 編集中テキストの文字コード情報をデフォルトに戻す
  @editor.buffer.text = ""		# TextViewのデータをクリア
  @window.title = 'Hello World Editor - ' + 'untitled'	# ウィンドウタイトル更新
end

"New(新規)"コマンドでは、読み込み/保存ファイルに関する情報とTextViewの初期化、およびウィンドウタイトルの更新を行います。

"DEFAULT_FILECHARSET"は新規文書を保存するときのデフォルト文字コードです。値には、ファイル入出力の時などに利用する(予定の)Kconvモジュールの定数を使っています。チュートリアル用プログラムの動作確認をMS Windowsで行っているのでShift-JISを選択しました。プログラムのユーザが自分の環境に合わせて適宜変更するという想定です(起動時のコマンドラインオプションで指定できるようにすると便利かも)。スクリプト冒頭にKconvモジュールを読み込むコードも追加します。

"@filecharset"はファイル保存時の文字コードです。Gtk::TextViewはUTF-8のテキストしか受け付けないので、このような変数を用意して新規作成時やファイル読み込み時に記録しておく必要があります。

このサンプルでは、直前に編集していたテキストを保存するかどうか、ユーザに尋ねる処理は省略しています。(保存しません)

"New(新規)"コマンド(改)

処理としては上記の通りでいいのですが、"on_new1_activate"メソッド内のコードは、プログラムの起動時にも実行するものなので、別メソッドとして切り出すことにします。またウィンドウタイトルの更新は上記2つの場所以外でも実行されることが容易に想像できるので、独立したメソッドにします。そのように書き換えたコードが以下です。

def initialize(path_or_data, root = nil, domain = nil, localedir = nil, flag = GladeXML::FILE)
  super(path_or_data, root, domain, localedir, flag)

  @window = @glade['main_window']
  @editor = @glade['textview1']
  @filedialog = @glade['filechooser']
  @aboutdialog = @glade['aboutdialog']

  initialize_editor	# 追加
end

def on_new1_activate(widget)
  puts "on_new1_activate"
  initialize_editor	# 置き換え
end

def initialize_editor	# 新規作成
  @filename = nil
  @filecharset = DEFAULT_FILECHARSET
  @editor.buffer.text = ""
  update_window_title
end

def update_window_title	# 新規作成
  @window.title = 'Hello World Editor - ' + File.basename(@filename || 'untitled')
end

ウィンドウタイトルにはファイルパスではなく、ファイル名のみ表示するようにしてみました。

"Open(開く)"コマンド

処理の流れとしては、Gladeで作っておいたGtk::FileChooserDialogを表示してユーザにファイルを選択させる、そのファイルをまるごと読み込んでTextViewに表示、ファイル名を使ってウィンドウタイトルを更新、ダイアログを隠す、という形になります。

def on_open1_activate(widget)
  puts "on_open1_activate"
  show_opendialog		# 追加
end

def show_opendialog	# 新規作成
  @filedialog.action = Gtk::FileChooser::ACTION_OPEN	# ダイアログをオープン用に設定
  @filedialog.title  = 'Open Dialog'
  if @filedialog.run == Gtk::Dialog::RESPONSE_OK
    if File.exist?(get_platform_filename(@filedialog.filename))
      @filename = @filedialog.filename	# ファイルパスを記録
      read_file(@filename)			# 選択されたファイルを読み込んでTextViewに表示
      update_window_title
    end
  end
  @filedialog.hide
end

def get_platform_filename(filename)	# 新規作成
  if RUBY_PLATFORM.include?('mswin32')
    return Kconv.tosjis(filename)
  else
    return Kconv.toutf8(filename)
  end
end

このチュートリアルのプログラムでは、一つのGtk::FileChooserDialogをオープン時、保存時両方で使い回しますので、まず"show_opendialog"メソッドの冒頭でオープン用の設定をしています。その後ダイアログを表示して、"OK"ボタンで閉じられ、かつ取得したファイルパスが存在する時だけファイルの読み込みと表示を行います。

アバウトダイアログの例と同じようにrunメソッドを使っていますが、ブロックは付けずに戻り値から"OK"ボタンが押されたかどうかを判定しています。

選択されたファイルの存在チェックでは"get_platform_filename"メソッドを経由したパスを指定しています。これはダイアログで取得できるパスの文字コード(UTF-8)と"File.exist"メソッドが受け付けるパスの文字コードが異なる場合があるためです。例えば、MS Windowsでは日本語などを含むパスはShift-JISでなければなりません。"get_platform_filename"メソッドでプラットフォームに応じた文字コードに変換しています。このメソッドはMS Windows以外の場合の処理は適当です。ご注意ください。

"update_window_title"メソッドは"New(新規)"コマンドの実装時に作ったものです。

下に"read_file"メソッドのコードを挙げます。

def read_file(filename)	# 新規作成
  text = ""
  File.open(get_platform_filename(filename)) do |f|
    text = f.readlines.join		# まるごと読み込む
  end
  @filecharset = Kconv.guess(text)	# ファイル保存時の文字コードを記録
  if @filecharset == Kconv::UNKNOWN
    @filecharset = DEFAULT_FILECHARSET
  end
  @editor.buffer.text = Kconv.kconv(text, Kconv::UTF8, @filecharset)	# TextViewに表示
  @editor.move_cursor(Gtk::MOVEMENT_BUFFER_ENDS, -1, false)		# カーソルを先頭に移動
end

"File.open"メソッドでも文字コードを変換したパスを指定しています。またファイルから読み込んだデータについても必要な処理をしています。上でも書きましたが、Gtk::TextViewがUTF-8のテキストしか正常に表示できないためです。

Gtk::TextView#move_cursorは、移動単位と移動量を指定してカーソルを移動するメソッドです。移動量はマイナス値も指定できます。3番目の引数は、移動前の位置から移動後の位置までのテキストを選択状態にするかどうかを指定します。

"Save(保存)"コマンド

保存ファイルパスが既に決まっている場合(既存ファイルを読み込んだ、もしくは保存済み)、そのまま保存し、そうでなければダイアログを表示してユーザに指定してもらいます。"save_file"メソッドは"Save As(別名で保存)"コマンドでも使います。"show_savedialog"メソッドは"Save As(別名で保存)"コマンドで説明します。

def on_save1_activate(widget)
  puts "on_save1_activate"
  if @filename
    save_file(@filename)
  else
    show_savedialog
  end
end

def save_file(filename)	# 新規作成
  File.open(get_platform_filename(filename), 'w') do |f|
    f.write(Kconv.kconv(@editor.buffer.text, @filecharset, Kconv::UTF8))
  end
end

"Save As(別名で保存)"コマンド

def on_save_as1_activate(widget)
  puts "on_save_as1_activate"
  show_savedialog
end

常にダイアログを表示してユーザにファイル名を入力してもらいます。

def show_savedialog	# 新規作成
  @filedialog.action = Gtk::FileChooser::ACTION_SAVE	# ダイアログを保存用に設定
  @filedialog.title  = 'Save Dialog'
  loop do
    if @filedialog.run == Gtk::Dialog::RESPONSE_OK
      next unless @filedialog.filename			# ファイル名が空
      if File.exist?(get_platform_filename(@filedialog.filename))
        next unless overwrite_file?(@filedialog.filename)	# 上書き拒否
      else
        next unless filename_valid?(@filedialog.filename)	# ファイル名が不正
      end
      @filename = @filedialog.filename
      save_file(@filename)
      update_window_title
    end
    break
  end
  @filedialog.hide
end

"show_savedialog"メソッドでは、"Open(開く)"コマンドでも使ったGtk::FileChooserDialogを保存用に使い、入力されたファイル名が不正でなければ保存します。Gtk::Dialog#runメソッドでレスポンスを捕捉してその値をチェックするのは"show_opendialog"メソッドと同じですが、そのブロックを無限ループで挟んで、"OK"ボタンが押され、かつ上書きが拒否されたり入力されたファイル名が不正である場合、そのままファイル保存ダイアログが表示され続けるようにしています。

def overwrite_file?(filename)	# 新規作成
  dialog = Gtk::MessageDialog.new(
          @filedialog, Gtk::Dialog::MODAL,
          Gtk::MessageDialog::QUESTION,
          Gtk::MessageDialog::BUTTONS_OK_CANCEL,
          filename + "\n already exists. Do you overwrite it?")
  result = dialog.run
  dialog.destroy
  result == Gtk::Dialog::RESPONSE_OK
end

"overwrite_file?"メソッドでは、呼び出される度に、ファイルを上書きするかどうか確認するダイアログ(Gtk::MessageDialog)を新規に作成して表示します。"OK"ボタンが押されたかどうかを戻り値として返します。

def filename_valid?(filename)	# 新規作成
  begin
    File.open(get_platform_filename(filename), 'w') do |f| end
  rescue Errno::EINVAL => err
    p err
    dialog = Gtk::MessageDialog.new(
            @filedialog, Gtk::Dialog::MODAL,
            Gtk::MessageDialog::ERROR,
            Gtk::MessageDialog::BUTTONS_CLOSE,
            File.basename(filename) + " is an invalid file name.")
    dialog.run
    dialog.destroy
    return false
  end
  true
end

"filename_valid?"メソッドでは、ファイルパスをチェックして不都合があればダイアログで表示してfalseを返します。このメソッドはファイル保存処理専用に作ったので、試しにファイルを作ってみてパスの有効性を確認しています(ファイルができてしまっても問題がない)。パスに問題がなければtrueを返します。

サンプルスクリプト

このページで作成したhwedit.rbの全体をダウンロードできます。実行するにはこれまでのチュートリアルで作成した"glade_hwedit.rb"と"hwedit.glade"が必要です。

>> hwedit.rb


*1"response ID"の実体は整数ですが、事前定義されたものはすべて負の整数です。もしユーザ定義のものを使う場合には正の整数にする必要があります。

更新日時:2008/10/27 23:06:42
キーワード:
参照:[libglade2-tut-dialogs] [libglade2-tut]