JRubyでFilthyRichClients その3
Filthy Rich Clientsの第3章。
paintComponentやpaintをオーバーライドするときや、画像のgetGraphicsメソッドなどで
取得できるGraphicsオブジェクトの話。
Graphicsオブジェクトの状態を変化させて、レンダリングを操作できる。
以下の属性が、Filthy Rich Clientsについて、最も役にたつと書いてある。
属性を変えるためのセッター、ゲッターなど関連するメソッドも一緒に書くと
- Foreground Color
- setColor
- getColor
- Background Color
- setBackground
- getBackground
- Font
- setFont
- getFont
- Stroke
- setStroke
- getStroke
- RenderingHint
- setRenderingHint
- getRenderingHint
- addRenderingHint
- Clip
- clipRect
- clip
- setClip
- getClip
- getClipBounds
- Composite
- setComposite
- getComposite
- Paint
- setPaint
- getPaint
- Transform
- rotate
- scale
- translate
- transform
- setTransform
- getTransform
以下はTransformを色々と変更して、図形や文字を描画している。
include Java include_class %w( JFrame JComponent SwingUtilities ).map{|s| 'javax.swing.' + s} include_class %w( Color ).map{|s| 'java.awt.' + s} class RotateComponent < JComponent def paintComponent(g) g2d = g.create() # 背景の描画 g2d.setColor(Color::WHITE) g2d.fillRect(0, 0, getWidth, getHeight) # 四角形の描画 g2d.setColor(Color::GRAY.brighter()) g2d.fillRect(50, 50, 50, 50) g2d.rotate(degree_to_radian(45)) g2d.setColor(Color::GRAY.darker()) g2d.fillRect(50, 50, 50, 50) g2d = g.create() g2d.rotate(degree_to_radian(45), 75, 75) g2d.setColor(Color::BLACK) g2d.fillRect(50, 50, 50, 50) g2d.dispose() # 文字列の描画 g2d = g.create() g2d.translate(150, 150) total_degree = 0 4.times do g2d.drawString("rotate: " + total_degree.to_s + "-degree", 10, 0) g2d.rotate(degree_to_radian(90)) total_degree += 90 end g2d.dispose() end private def degree_to_radian(deg) Math::PI / 180.0 * deg end end SwingUtilities.invokeLater do f = JFrame.new f.add(RotateComponent.new) f.setSize(300, 300) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) f.setLocationRelativeTo(nil) f.setVisible(true) end
JRubyでFilthyRichClients その2
描画をカスタマイズするときは、普通はpaintComponentをオーバーライドするんだけど、
その部品の子供や境界線にも影響を与えたいときはpaintをオーバーライドしてもいい。
swing部品を半透明するなどのように、部品全てに影響を与えたいときはpaintのオーバーライドが使えるみたい。
下のコードでは、paintをオーバーライドしてJButtonを半透明にする。一度、BufferdImageに本来の絵を描いてから、
AlphaCompositeで半透明を設定して、BufferdImageの絵を描画している。
include Java include_class %w( JFrame JPanel JButton SwingUtilities ).map{|s| 'javax.swing.' + s} include_class %w( AlphaComposite Color ).map{|s| 'java.awt.' + s} class TranslucentButton < JButton def initialize(text) super(text) setOpaque(false) @btn_image = nil end def paint(g) if(@btn_image == nil || @btn_image.getWidth != getWidth || @btn_image.getHeight != getHeight) @btn_image = getGraphicsConfiguration().createCompatibleImage(getWidth, getHeight) end # super呼び出しで、@btn_imageにボタンの絵を描画する。 g_btn = @btn_image.getGraphics g_btn.setClip(g.getClip) super(g_btn) g_btn.dispose # AlphaCompositeを半透明(0.5)に設定して@btn_imageをグラフィクスに描画する。 g.setComposite(AlphaComposite.getInstance(AlphaComposite::SRC_OVER, 0.5)) g.drawImage(@btn_image, 0, 0, nil) end end class CheckBoard < JPanel CHECK_SIZE = 60.0 def paintComponent(g) # 背景を白で塗っておく g.setColor(Color.white) g.fillRect(0, 0, getWidth, getHeight) g.setColor(Color.black) #左上と右下のブロックを黒で塗る (getWidth / CHECK_SIZE).ceil.times do |i| (getHeight / CHECK_SIZE).ceil.times do |j| g.fillRect(CHECK_SIZE*i , CHECK_SIZE*j , CHECK_SIZE/2, CHECK_SIZE/2) g.fillRect(CHECK_SIZE*i + CHECK_SIZE/2, CHECK_SIZE*j + CHECK_SIZE/2, CHECK_SIZE/2, CHECK_SIZE/2) end end end end SwingUtilities.invokeLater do panel = CheckBoard.new panel.add(TranslucentButton.new("TranslucentButton")) f = JFrame.new("TranslucentButton") f.add(panel) f.setSize(300, 300) f.setVisible(true) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) end
重い処理のあつかい
重い処理をEDT上で行うと、画面がフリーズしてしまうので、EDT上では重い処理はやらない。
以下は良くない書き方。
include Java include_class %w( JFrame JButton SwingUtilities ).map{|s| 'javax.swing.' + s} class FreezeEDT < JFrame def initialize(title) super(title) freezer = JButton.new("Freeze") freezer.addActionListener do puts "重い処理 start" sleep 3 puts "重い処理 finish" end add(freezer) pack end end SwingUtilities.invokeLater do FreezeEDT.new("FreezeEDT").setVisible(true) end
実行結果
ボタンをクリックすると、addActionListenerに渡しているブロックが実行され、画面がフリーズする。
なので、
- 重い処理をするときは、EDT上でやらないで、スレッドを生成して処理させる。
- スレッドの中にSwing部品を更新するような処理を入れるときは、その処理をSwingUtilities.invokeLaterに渡す。
以下はこれをふまえたやり方。
include Java include_class %w( JFrame JLabel JButton SwingUtilities SwingConstants ).map{|s| 'javax.swing.' + s} include_class 'java.awt.BorderLayout' class SwingThreading < JFrame def initialize(title) super(title) count = 0 label = JLabel.new(count.to_s, SwingConstants::CENTER) add(label, BorderLayout::CENTER) btn = JButton.new("Increment") btn.addActionListener do Thread.start do sleep 3 # 重い処理 count += 1 SwingUtilities.invokeLater{ label.setText(count.to_s) } end end add(btn, BorderLayout::SOUTH) pack end end SwingUtilities.invokeLater do SwingThreading.new("SwingThreading").setVisible(true) end
JRubyでFilthyRichClients
半年ほど前にFilthyRichClientsを読んだけど、いろいろ忘れているので復習する。
せっかくなので、JRubyでサンプルコードを書きなおしながら勉強してみる。
まずは、chapter2の楕円とハイライト表示のコードを書いてみよう。
楕円を描く
require 'java' include_class 'java.awt.Color' include_class %w( JFrame JComponent SwingUtilities).map{|s| 'javax.swing.' + s} class OvalComponent < JComponent def paintComponent(g) g.setColor(getBackground()) g.fillRect(0, 0, getWidth(), getHeight()) g.setColor(Color::RED) g.fillOval(0, 0, getWidth(), getHeight()) end end SwingUtilities.invokeLater do f = JFrame.new f.setTitle("OvalComponent") f.add(OvalComponent.new) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE); f.setSize(200, 200); f.setVisible(true); end
実行結果
JComponentを継承したクラスをつくって、paintComponentをオーバーライドした。
setColorで赤色に設定して、fillOvalで円を描いている。赤い楕円が表示される。
ハイライト表示
JButtonのテキストにハイライトを付ける。
require 'java' include_class %w( AlphaComposite Color FlowLayout RadialGradientPaint ).map{|s| 'java.awt.' + s} include_class %w( JFrame JButton SwingUtilities ).map{|s| 'javax.swing.' + s} include_class 'java.awt.image.BufferedImage' include_class 'java.awt.geom.Point2D' class HighlightButton < JButton HIGHLIGHT_SIZE = 18 def initialize(text) super(text) @highlight_image = create_highlight_image() end def create_highlight_image img = BufferedImage.new(HIGHLIGHT_SIZE, HIGHLIGHT_SIZE, BufferedImage::TYPE_INT_ARGB) g = img.createGraphics() # 背景 g.setComposite(AlphaComposite::Clear) g.fillRect(0, 0, HIGHLIGHT_SIZE, HIGHLIGHT_SIZE) # グラデーションを作成 g.setComposite(AlphaComposite::SrcOver) center = Point2D::Float.new(HIGHLIGHT_SIZE/2, HIGHLIGHT_SIZE/2) radius = HIGHLIGHT_SIZE/2.0 dist = [0.0, 0.85].to_java(:float) colors = [Color.white, Color.new(255, 255, 255, 0)].to_java('java.awt.Color'.to_sym) paint = RadialGradientPaint.new(center, radius, dist, colors) g.setPaint(paint) g.fillOval(0, 0, HIGHLIGHT_SIZE, HIGHLIGHT_SIZE); g.dispose() img end def paintComponent(g) super(g) g.drawImage(@highlight_image, getWidth()/4, getHeight()/4, nil) end end SwingUtilities.invokeLater do f = JFrame.new f.setTitle("OvalComponent") f.getContentPane().setLayout(FlowLayout.new); f.add(JButton.new("standard")) f.add(HighlightButton.new("highlight")) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE); f.setSize(100, 100) f.setVisible(true); end
実行結果
JButtonを継承するクラスでpaintComponentをオーバーライドした。
create_highlight_imageの中がごちゃごちゃしてるけど、BufferedImageをつくって、そこにグラデーションを書いています。
paintComponentの中では、まずデフォルトの描画を行い(superの呼び出し)、
そのあとcreate_highlight_imageで作ったImageを描画しています。下のJButtonのほうに、微妙にハイライトが付いている。
RadialGradientPaintをつくるのに、色々とto_javaするのが大変。。。もっと上手な方法あるのかな。
nanocのソースを読む
nanocのソースを読んで、使えるコマンドなどを調べてみよう。
まずは、nanocのコマンドであるnanoc3ファイルを見てみる。
以下のソースはほぼすべて抜粋です。
nanoc3
require 'nanoc3' require 'nanoc3/cli' # Run base Nanoc3::CLI::Base.shared_base.run(ARGV)
requireして、引数をshared_base.runに渡している。
Nanoc3::CLI::Base.shared_base
def self.shared_base @shared_base ||= Nanoc3::CLI::Base.new end
=を使って最初の一回だけnewするようにしている。だからshared_baseなのかな。。 |
最初のrunは、Nanoc3::CLI::Base#runのようだ。
次はrunを見てみる。
Nanoc3::CLI::Base#run
def run(args) super(args) rescue Interrupt => e exit(1) rescue StandardError, ScriptError => e print_error(e) exit(1) end
super(args)となっているので、スーパークラスを見ればよいのか。
module Nanoc3::CLI class Base < Cri::Base
Cri::Base#runを見れば良いようだ。Criはnanoc3をインストールされたときに、
一緒にインストールされたgemのようです。Criがいつrequireされてたかというと、最初のrequire 'nanoc3/cli'を
したときにnanoc3/cliの中でrequireされていた。
Cri::Base#run
def run(args) # Check arguments if args.length == 0 @help_command.run([], []) exit 1 end # もっと続く。。。
何も引数を与えずにnanoc3すると、@help_command.runが呼ばれてexitする。
@help_commandは何かというと
#Cri::Base attr_accessor :help_command
#Nanoc3::CLI::Base self.help_command = Nanoc3::CLI::Commands::Help.new
このHelpクラスのようにCommandっぽいクラスは\nanoc3\cli\commandsディレクトリの下に
autocompile.rb compile.rb create_item.rb create_layout.rb create_site.rb debug.rb help.rb info.rb update.rb view.rb
とあって、Helpクラスもhelp.rbに書いてある。nanocのヘルプを呼ぶと、これらに対応するような
commandがある。
[saliy@localhost ~]$ nanoc3 nanoc, a static site compiler written in Ruby. Available commands: autocompile start the autocompiler compile compile items of this site create_item create a item create_layout create a layout create_site create a site debug show debug information for this site help show help for a command info show info about available plugins update update the data stored by the data source to a newer version view start the web server that serves static files
なので、"nanoc3 コマンド名"を実行すると、コマンドに対応するクラスのrunが呼ばれるのかな、と想像してみる。
Nanoc3::CLI::Commands::Help#run
def run(options, arguments) # Check arguments if arguments.size > 1 $stderr.puts "usage: #{usage}" exit 1 end if arguments.length == 0 # Build help text text = '' # Add title text << "nanoc, a static site compiler written in Ruby.\n" # もっと続く。。。
puts text
いまの場合、arguments.lengthは0。
textにnanocのヘルプを詰めてputsする。そして、呼び出し元に戻るとexit 1 して終了。
nanoc3を何も引数を付けずに呼び出すと、Nanoc3::CLI::Commands::Help#runで、ヘルプが出力されることが分かった。
もうちょっと読んでみよう。
nanocをつかう
HTMLジェネレータnanocをつかってみようと思います。
サイト作成
create_siteでサイトを作成します。
[saliy@localhost ~]$ nanoc3 create_site nanoc_test create config.yaml create Rakefile create Rules create content/index.html create content/stylesheet.css create layouts/default.html Created a blank nanoc site at 'nanoc_test'. Enjoy!
作成されたファイルが表示されます。
compile
先ほど作成されたファイルをもとにcompileでhtmlをつくります。
[saliy@localhost nanoc_test]$ nanoc3 compile Loading site data... Compiling site... create [0.01s] output/index.html create [0.00s] output/style.css Site compiled in 0.07s.
outputディレクトリに作成されました。
確認
作成されたhtmlをブラウザで確認します。
[saliy@localhost nanoc_test]$ nanoc3 view [2010-05-19 00:15:02] INFO WEBrick 1.3.1 [2010-05-19 00:15:02] INFO ruby 1.9.1 (2010-01-10) [i686-linux] [2010-05-19 00:15:03] INFO WEBrick::HTTPServer#start: pid=9750 port=3000
3000ポートでwebrickが動いているようなので、ブラウザで3000ポートを指定してアクセスします。
nanoc3 viewをするには、rack,adsfをインストールしている必要があるようなので、無ければインストールします。
[saliy@localhost nanoc_test]$ nanoc3 view [2010-05-19 00:15:02] INFO WEBrick 1.3.1 [2010-05-19 00:15:02] INFO ruby 1.9.1 (2010-01-10) [i686-linux] [2010-05-19 00:15:03] INFO WEBrick::HTTPServer#start: pid=9750 port=3000
とりあえず、こんな感じで動かしてみた。もうちょっと詳しく調べてみよう。
チェックボックスをつくる
イベントの勉強にチェックボックスを自作してみた。
イベントクラス
package my.events { import flash.events.Event; public class CheckBoxEvent extends Event { /** * The <code>CheckBoxEvent.CHANGE</code> constant defines the values of the <code>type</code> * property of the event object for a <code>checkBoxChange</code> event. * * @eventType CheckBoxChange */ public static const CHANGE:String = "checkBoxChange"; private var _isOn:Boolean; /** * Constructor. * * @param type The type of event to be dispatched. This should be <code>CheckBoxEvent.CHANGE</code>. * @param isOn The state of the check box. * * @example * <pre> * var event:CheckBoxEvent = new CheckBoxEvent(CheckBoxEvent.CHANGE, boolean); * dispatchEvent(event); * </pre> */ public function CheckBoxEvent(type:String, isOn:Boolean) { super(type); _isOn = isOn; } /** * @return The state of the check box. If value is true, check box was checked. */ public function get isOn():Boolean { return _isOn; } /** * @return The cloned event. */ override public function clone():Event { return new CheckBoxEvent(type, _isOn); } override public function toString():String { return formatToString("CheckBoxEvent", "type", "bubbles", "cancelable", "eventPhase", "_isOn"); } } }
チェックボックスのチェックマーク
チェックマークの絵を描く。
package my.display { import flash.display.Shape; public class CheckBoxMark extends Shape { public function CheckBoxMark() { draw(); } private function draw():void { graphics.beginFill(0x000000); graphics.lineStyle(1, 0x000000); graphics.moveTo(0, 10); graphics.lineTo(5, 15); graphics.lineTo(20, 0); graphics.lineTo(5, 20); graphics.lineTo(0, 10); graphics.endFill(); scaleX = 0.7; scaleY = 0.7; } }
チェックボックス
メインとなるクラス
package my.display { import my.events.CheckBoxEvent; import flash.display.Sprite; import flash.display.Shape; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFormat; public class CheckBox extends Sprite { private const SIZE:uint = 20; private const TEXT_FORMAT:TextFormat = new TextFormat(null, 20); private var _mark:Shape; private var _isOn:Boolean; public function CheckBox(isOn:Boolean=false, text:String="") { _isOn = isOn; drawBox(); createMark(); createTextField(text); addEventListener(MouseEvent.MOUSE_DOWN, onMouseDownListener); } private function drawBox():void { graphics.beginFill(0xFFFFFF); graphics.lineStyle(1); graphics.lineTo(SIZE, 0); graphics.lineTo(SIZE, SIZE); graphics.lineTo(0, SIZE); graphics.lineTo(0, 0); graphics.endFill(); } private function createMark():void { _mark = new CheckBoxMark(); _mark.x = (width - _mark.width) / 2; _mark.y = (height - _mark.height) / 2; addChild(_mark); _mark.visible = _isOn; } private function createTextField(text:String):void { var field:TextField = new TextField(); field.text = text; field.setTextFormat(TEXT_FORMAT); field.selectable = false; field.x = width; field.y = (height - field.textHeight) / 2 ; addChild(field); } private function onMouseDownListener(e:MouseEvent):void { _isOn = !_isOn; dispatchEvent(new CheckBoxEvent(CheckBoxEvent.CHANGE, _isOn)); _mark.visible = _isOn; } } }
動作確認
ちゃんと動いているようだ。
package { import my.display.CheckBox; import my.events.CheckBoxEvent; import flash.display.Sprite; public class Main extends Sprite{ public function Main() { var c:CheckBox = new CheckBox(true, "test"); c.x = c.y = 100; addChild(c); c.addEventListener(CheckBoxEvent.CHANGE, changeListener); } private function changeListener(e:CheckBoxEvent):void { trace(e.isOn); } } }
スライダーをつくる
カスタムイベントの勉強にスライダーを作ってみた。
とりあえず
- イベントクラス
- スライダーのつまみ部分
- スライダー
の3つのクラスで作ってみた。
イベントクラス
- 発生するイベント
- SliderEvent.START => スライダーのドラッグを開始したときに発生
- SliderEvent.CHANGE => スライダーをドラッグ中に発生
- SliderEvent.END => ドラッグ終了時に発生
- コンストラクタのpercentは、ドラッグしているスライダーの割合
package { import flash.events.Event; public class SliderEvent extends Event { public static const START:String = "SliderStart"; public static const CHANGE:String = "SliderChange"; public static const END:String = "SliderEnd"; private var _percent:Number; public function SliderEvent(type:String, percent:Number) { super(type); _percent = percent; } public function percent():Number { return _percent; } override public function clone():Event { return new SliderEvent(type, _percent); } override public function toString():String { return formatToString("SliderEvent", "type", "bubbles", "cancelable", "eventPhase", "_percent"); } } }
スライダーのつまみ部分
- つまみの絵を描くだけ。
- コンストラクタ
- widthとheightでつまみの大きさを指定。fillColorで何色で塗るか指定。
package { import flash.display.Sprite; internal class SliderHandle extends Sprite { public function SliderHandle(width:Number, height:Number, fillColor:uint = 0xFFFFFF) { draw(width, height, fillColor); } private function draw(width:Number, height:Number, fillColor:uint):void { graphics.beginFill(fillColor); graphics.lineStyle(1, 0x000000); graphics.lineTo(width / 2, height); graphics.lineTo( -width / 2, height); graphics.lineTo(0, 0); graphics.endFill(); } } }
スライダー
- コンストラクタ
- widthはバーの長さ、percentは初期値(0〜100を指定)
- イベントの送出
- getPercentで、現在のスライダーバーの割合を計算して、SliderEventをdispatchEventする
startDragにスライダーのつまみを動かせる範囲を指定できたので簡単にかけた。
package { import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Rectangle; public class Slider extends Sprite { private const BAR_HEIGHT:uint = 10; private const SLIDER_HANDLE_WIDTH:uint = 10; private const SLIDER_HANDLE_HEIGHT:uint = 20; private var _handle:SliderHandle; private var _bar_width:uint; public function Slider(width:uint, percent:uint) { _bar_width = width; drawSlideLine(); createSliderHandle(); initStartPosition(percent); } private function drawSlideLine():void { graphics.beginFill(0xFFFFFF); graphics.lineStyle(1); graphics.lineTo(_bar_width, 0); graphics.lineTo(_bar_width, BAR_HEIGHT); graphics.lineTo(0, BAR_HEIGHT); graphics.lineTo(0, 0); graphics.endFill(); } private function createSliderHandle():void { _handle = new SliderHandle(SLIDER_HANDLE_WIDTH, SLIDER_HANDLE_HEIGHT); _handle.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownListener); _handle.y = BAR_HEIGHT / 2; addChild(_handle); } private function initStartPosition(percent:uint):void { if ( 0 > percent || percent > 100) { throw Error("percent mast range 0 to 100:" + percent); } _handle.x = _bar_width * percent / 100; } private function draggableArea():Rectangle { return new Rectangle(0, BAR_HEIGHT / 2, _bar_width, 0); } private function getPercent():Number { return _handle.x / _bar_width * 100; } private function mouseDownListener(e:MouseEvent):void { stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveListener); stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpListener); dispatchEvent(new SliderEvent(SliderEvent.START, getPercent())); _handle.startDrag(false, draggableArea()); } private function mouseMoveListener(e:MouseEvent):void { dispatchEvent(new SliderEvent(SliderEvent.CHANGE, getPercent())); } private function mouseUpListener(e:MouseEvent):void { stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveListener); stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpListener); dispatchEvent(new SliderEvent(SliderEvent.END, getPercent())); _handle.stopDrag(); } } }
使い方
下のようにして使う。
package { import flash.display.Sprite; public class Main extends Sprite { public function Main() { var slider:Slider = new Slider(500, 50); addChild(slider); slider.addEventListener(SliderEvent.START, f1); slider.addEventListener(SliderEvent.CHANGE, f2); slider.addEventListener(SliderEvent.END, f3); } private function f1(e:SliderEvent):void { trace("start:" + e.percent()); } private function f2(e:SliderEvent):void { trace("change:" + e.percent()); } private function f3(e:SliderEvent):void { trace("end:" + e.percent()); } } }