Window にスクリプトで部品を配置する

ここでは、ウィンドウにいろいろなものを配置していく。何ができるかのメモというより、作っていく過程のメモか。

ここで作るのは、こんな感じの iTunes とか Finder の検索なんかで、項目を増やしたり減らしたりするやつ。NSPredicateEditor を使えばもっと簡単にできそうなんだけど、使い方がわからないので、強引にスクリプトで作ってみた。

実際には、スクリプトで部品をおくことにしたんだけど、まずは、どこにおいたらいいかの代替の見当をつけるために、実際のものをおいてみたのが上の図。これで、Inspector の Size のところで、座標を見てそれを元にスクリプトで配置する。Apply ボタンは、単においてみただけ。実際にどう使うかとかは考えてない。

あくまでも、とりあえず思いついたことのメモなので、もっといい方法があればそれに変えていくつもり。

動作は、+ ボタンをクリックすると検索する項目が一番下に追加されていき、- ボタンをクリックするとその項目が消えるようにしたい。まあ、iTunes のスマートプレイリストを作るときのまね。Mail のジャンクフィルターのもそうだし、まあ、普通のやつということで。

作り方としては、NSView のサブビューを作り、それに部品を配置してパネルに追加していき、それとともにパネルの大きさを変える。これだと、消すときは部品の乗ってる NSView のオブジェクトを消してしまえば全部消える。また、それとともにパネルの大きさを変える。

部品の追加

まず、NSView.alloc.initWithFrame(frame) で NSView のオブジェクトを作る。何もおいてないパネルの横幅が 581 で、あとはだいたい。

view = NSView.alloc.initWithFrame([0,46,581,86])

で、Window 操作で書いたように、起点の座標が画面の左下にあり、パネルを下に広げるとそのままでは部品も下に引っ張られるので(下からの座標だから、左下の角からの位置がかわらないため)、それが起きないように固定する。それには、setAutoresizingMask(mask) を使う。mask のオプションについては Resizing Mask を参照。ここでは、サブビューの大きさは変えないでウィンドウは下に伸びるだけなので、下側を可変にする。

view.setAutoresizingMask(NSViewNotSizable)

view.setAutoresizingMask(NSViewMinYMargin)

後は、これに配置する部品を作っていく。Pop Up Button 2つ、Text Field 1つ、Round Button 2つ。

まずは、Pop Up Button から。NSPopUpButton.alloc.initWithFrame_pullsDown(rect,true/false) で作るんだけど、細かいことは、Save パネルのところで書いたので、そちらを参照。size は Interface Builder で部品をおいて調節してから、そのサイズをメモして frame に使ってる。addSubview で上で作った view にサブビューとして追加する。

popup = NSPopUpButton.alloc.initWithFrame_pullsDown([17,5,155,26],false)

items = ["item 1","item 2","item 3"]

popup.addItemsWithTitles(items)

view.addSubview(popup)

2つ作るので、もう一つは名前を変えて作る。もう一つの size は [174,5,114,26]。

Text Field は、NSTextField.alloc.initWithFrame(frame) で作る。これの frame も Interface Builder から。特別な設定はなし。これも、addSubview で上で作った view にサブビューとして追加する。

textField = NSTextField.alloc.initWithFrame([293,8,208,22])

view.addSubview(textField)

最後は、Round Button を NSButton.alloc.initWithFrame(frame) で作る。

button = NSButton.alloc.initWithFrame([504,3,32,32])

次に、ボタンのタイプを setBezelStyle(style) で設定する。オプションについては、Button Style を参照。

button.setBezelStyle(NSCircularBezelStyle)

フォントは普通に setFont(font) で、ボタンに配置するテキストは setTitle(string) で設定する。font は NSFont.fontWithName_size(name,size) か何らかの方法で設定する。ここでは、+- の2つ作るがボタンは一つずつ作る。

button.setFont(NSFont.fontWithName_size("Lucida Grande Bold", 18.0))

button.setTitle("+")

ボタンには、クリックしたときに何らかの動作を与えたい。それには setAction(action) を使うんだけど、その前に、setTarget(target) で action を送る先を設定する。ここではボタンを設定しているスクリプトにボタンの動作も書くので、それが結びつけてある NSObject クラスを指定するために target を self にする(AppController)。addItem という ib_action に結びつけるので、addItem を action にする。

button.setTarget(self)

button.setAction('addItem')

最後に、作ったボタンを上の view にサブビューとして追加する。

view.addSubview(button)

これで、部品の乗った NSView オブジェクトができたわけだけど、後で部品にアクセスするために、@subViews というクラスオブジェクトを作って、配列として入れておく。

@subViews << [view,popupMain,popupSub,textField]

つぎに、パネルのサイズを変更する。パネルのサイズは frame で得られるので、それを .to_a で配列にして変更する。配列の中身は [[x 座標,y 座標],[幅,高さ]] という具合に入っているので、y 座標を下げて、高さをその分増やす。ここでは、view が入る分だけ大きくしたいので、view の高さ分だけ変更している。frame は [x,y,w,h] でも [[x,y],[w,h]] でも受け付けてくれるので、中身を変更するだけでいい。

panelSize = @panel.frame.to_a

panelSize[0][1] = panelSize[0][1] - 46

panelSize[1][1] = panelSize[1][1] + 46

変更には setFrame_display_animate(frame,true/false,true/false) を使う。せっかくなのでアニメーションで伸びる変更にする。

@panel.setFrame_display_animate(panelSize,true,true)

最後に、大きくなった部分に view を追加する。view の大きさの分だけパネルが大きくなるので、そのまま追加すればいい。

@panelView.addSubview(view)

これを ib_action に設定すればいい。

def addItem(sender)

スクリプト

end

ib_action :addItem

部品の削除

追加した部品やビューを削除するには、削除したい部品に対して removeFromSuperview を使う。

view.removeFromSuperview

これをどう使うかなんだけど、とりあえず何でもいいから動けばいい、という感じで書いたのがのが次のスクリプト。これを deleteItem とかのメソッドにして ib_action に設定する。

まず、全部削除するとボタンがなくなるので、一つになったら削除できなくする。= 1 でいいと思うんだけど念のため。

return if @subViews.length <= 1

次に部品の情報が入ってる配列から view の情報を取り出して、それを削除する。@subViews に対して each_with_index でブロックにする。view はそれぞれの要素のさらに最初の要素なので、それと削除ボタンが入ってる view の情報を superview で取り出して .to_s で Ruby の String オブジェクトにして比較する。なぜ直接の比較じゃ行けないのかは、未だによくわからず。他の入れ物に入ってるのと比べるとだめなのか、とは思うけど。一致したら、removeFromSuperView でパネルから取り除く。次に、取り除いた view から下の view を上に引っ張り上げないといけないので、@subView の取り除いた view の次の要素から最後の要素までをブロックにして、それぞれの view から frame で座標を取り出して y 座標を setFrame で変更する。変更したら、取り除いた view の入っていた要素を @subViews から delete_at(index) で取り除くと同時にブロックを抜ける。これをまとめたのが次のスクリプト。

@subViews.each_with_index do |eachSubview,idx|

if eachSubview[0].to_s == sender.superview.to_s

eachSubview[0].removeFromSuperview

@subViews[idx+1..@subViews.length].each do |eachView|

viewsize = eachView[0].frame.to_a

viewsize[0][1]=viewsize[0][1]+46

eachView[0].setFrame(viewsize)

end

@subViews.delete_at(idx)

break

end

end

これで、項目を1つ取り除いたので、パネルのサイズを変更する。上の方でやったのの逆にする。

panelSize = @panel.frame.to_a

panelSize[0][1]=panelSize[0][1]+46

panelSize[1][1]=panelSize[1][1]-46

@panel.setFrame_display_animate(panelSize,true,true)

これで、Interface Builder でパネルからボタン等を取り除き、パネルの大きさをたてに小さくしてからアプリケーションを実行すると、ちゃんと + をクリックすると項目が増えて、- をクリックすると消えるはず。

実行できればこんな感じになる。

非常に見苦しいスクリプトだけど、自分の参照用にここにのせておく。イメージなのでコピペはできません。コピペできるてきるようにテキストも加えました。名前付けは適当。このスクリプトでもあるけど、パネルを開いたときに view が一つもないときだけ一つ追加するようにしてある。そうしないとパネルを閉じて開くたびに一つずつ view が追加されていく。もっといい方法が思いついたら、また書き直すかもしれない。2度と手を付けないかもしれないけど。

def initialize

@subViews = Array.new

end

def openTagPanel(sender)

@tagPanel.makeKeyAndOrderFront(self)

addItem(sel) if @subViews == 0

end

ib_action :openTagPanel

def addItem(sender)

view = NSView.alloc.initWithFrame([0,46,581,86])

view.setAutoresizingMask(NSViewNotSizable)

view.setAutoresizingMask(NSViewMinYMargin)


popupMain = NSPopUpButton.alloc.initWithFrame_pullsDown([17,5,155,26],false)

itemsMain = ["item 1","item 2","item 3"]

popupMain.addItemsWithTitles(itemsMain)

view.addSubview(popupMain)


popupSub = NSPopUpButton.alloc.initWithFrame_pullsDown([174,5,114,26],false)

itemsSub = ["is","includes","is not","not include"]

popupSub.addItemsWithTitles(itemsSub)

view.addSubview(popupSub)

textField = NSTextField.alloc.initWithFrame([293,8,208,22])

view.addSubview(textField)


plusButton = NSButton.alloc.initWithFrame([504,3,32,32])

plusButton.setBezelStyle(NSCircularBezelStyle)

plusButton.setFont(NSFont.fontWithName_size("Lucida Grande Bold", 18.0))

plusButton.setTitle("+")

plusButton.setTarget(self)

plusButton.setAction('addItem:')

view.addSubview(plusButton)


minusButton = NSButton.alloc.initWithFrame([531,3,32,32])

minusButton.setBezelStyle(NSCircularBezelStyle)

minusButton.setFont(NSFont.fontWithName_size("Lucida Grande Bold", 18.0))

minusButton.setTitle("-")

minusButton.setTarget(self)

minusButton.setAction('deleteItem:')

view.addSubview(minusButton)

@subViews << [view,popupMain,popupSub,textField]


windowSize = @tagPanel.frame.to_a

windowSize[0][1] = windowSize[0][1] - 46

windowSize[1][1] = windowSize[1][1] + 46

@tagPanel.setFrame_display_animate(windowSize,true,true)

@tagPanelView.addSubview(view)

end

ib_action :addItem

def deleteItem(sender)

return if @subViews.length <= 1

@subViews.each_with_index do |eachSubview,idx|

if eachSubview[0].to_s == sender.superview.to_s

eachSubview[0].removeFromSuperview

@subViews[idx+1..@subViews.length].each do |eachView|

viewsize = eachView[0].frame.to_a

viewsize[0][1] = viewsize[0][1] + 46

eachView[0].setFrame(viewsize)

end

@subViews.delete_at(idx)

break

end

end

windowSize = @tagPanel.frame.to_a

windowSize[0][1] = windowSize[0][1] + 46

windowSize[1][1] = windowSize[1][1] - 46

@tagPanel.setFrame_display_animate(windowSize,true,true)

end

ib_action :deleteItem