ブログトップ 記事一覧 ログイン 無料ブログ開設

プログラムdeタマゴ

2013/02/13

JavaFXの練習4:レイアウトがわからない

 さぁ、JavaFX2記事第4弾。ついに詰まりました。全然わかりません。誰か教えてください。

 今、nodamushiがわからないこと

  1. 別スレッドで処理した内容をsetTextでLabelの内容を変えようとするとスレッドがJavaFXのスレッドじゃないとエラーになる。SwingUtilities.invokeLater(Runnable)みたいなことはどうすればいいの?
  2. あるプロパティの値の変化でNodeの最適な大きさが変化したときに、どうすれば自動的に大きさを変更できるのか。(Swingでいうrevalidateとかみたいな)
  3. ぶっちゃけ、プロパティのbeanって何よ。豆って何よ。ていうか、プロパティってどれ使えばいいのよ。

 というわけで、誰か優しい人が教えてくれると期待しつつ(チラッ 今日やった内容を書き留めときます。

自作のレイアウトを作ってみる。

 簡単なレイアウトなら、既存のレイアウトパネル群を使えば、大体は出来るだろう。でも、やっぱ出来ないことだってあるじゃん。それに、今までSwing使ってきた私としては、自分でLayoutManagerを書いた方が何となくやりやすい。

 というわけで、練習として、今回は下の図の様に自分の子を円形に配置するレイアウトを作ってみることにしたよ。

f:id:nodamushi:20130213191940p:image:w320



 レイアウトのためのノードを作るには、javafx.scene.layout.Paneを継承したクラスを作ればいいっぽい。で、以下のメソッドをオーバーライドする。

  • layoutChildren():実際に子の配置を行うメソッド
  • computePreWidth(double):このノードの最適な幅を計算するメソッド
  • computePreHeight(double):このノードの最適な高さを計算するメソッド
  • computeMinWidth(double):このノードの最小の幅を計算するメソッド
  • computeMinHeight(double):このノードの最小の高さを計算するメソッド
  • computeMaxWidth(double):このノードの最大の幅を計算するメソッド
  • computeMaxHeight(double):このノードの最大の高さを計算するメソッド

 今回、最大最小は特に気にしないことにしたので、最初の三つをオーバーライドすることにした。なお、最小値のデフォルトはinsetの大きさ、最大はDouble.MaxValueを返すらしい。

 円形に配置したいので、高さの最適値は「円の直径+子の高さの中で最大のもの」、幅も同様に「円の直径+この幅の中で最大のもの」とした。今回はinsetは考えていない。

 と、いうわけで、これを元に実装するとこうなる。

package nodamushi.layout;

import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.layout.*;

public class CirclePane extends Pane{
  //半径
  private double radius;  
  public CirclePane(double r) {
    setRadius(r);
  }
  
  public void setRadius(double d){radiuse=d;}
  public double getRadius(){return radius;}
  
  private double getMaxChildWidth(double height){
    ObservableList<Node> nodes = getChildren();
    double maxWidth=0;
    for(Node n:nodes){
      double d = n.prefWidth(height);
      if(maxWidth < d)maxWidth = d;
    }
    return maxWidth;
  }
  private double  getMaxChildHeight(double width){
    ObservableList<Node> nodes = getChildren();
    double maxHeight=0;
    for(Node n:nodes){
      double d = n.prefHeight(width);
      if(maxHeight < d)maxHeight = d;
    }
    return maxHeight;
  }
  @Override protected double computePrefWidth(double height) {
    return getMaxChildWidth(height)+getRadius()*2;
  }
  
  @Override protected double computePrefHeight(double width) {
    return getMaxChildHeight(width)+getRadius()*2;
  }
  
  @Override protected void layoutChildren() {
    ObservableList<Node> nodes = getChildren();
    int length = nodes.size();
    if(length == 0)return;
    double step = 2*PI/length;//一つ配置する毎の回転角度
    double rad = 0;//現在の配置角度
    double width = getWidth();//このノードの大きさ
    double height = getHeight();
    double r = getRadius();//半径
    for(Node n:nodes){
      //配置するノードの中心座標を計算
      double x = Math.cos(rad)*r+width/2;
      double y = Math.sin(rad)*r+height/2;
      //配置するノードの左上の座標を計算
      double w = n.prefWidth(-1);
      double h = n.prefWidth(-1);
      x = x- w/2d;
      y = y- h/2d;
      //再配置
      n.resizeRelocate(x, y, w, h);
      //配置角度の更新
      rad+=step;
    }
  }
}

 このCirclePaneに適当にCircleを30個追加して表示した結果が先の図ってわけです。ひとまず、レイアウトをすることは出来たね。でも、せっかくなので、この半径を変更したり、配置の開始角度を変更したりしてアニメーションさせることが出来たら楽しそうだよね。

 というわけで、double radiusをDoublePropertyに変えてみよう………と思わなけりゃ良かった。

 とりあえず、radiusの値の変更があったら、自動で再レイアウトをしてもらわないといけない。何となく、それっぽい関数にrequestLayout()があったので、それを呼んでみた。

private DoubleProperty radius = new DoublePropertyBase() {
  @Override
  public void invalidated() {
    requestLayout();
  }
  
  @Override
  public String getName() {
    return "radius";
  }
  
  @Override
  public Object getBean() {
    return CirclePane.this;
  }
};
public final void setRadius(double d){radiuseProperty().setValue(d);}
public final double getRadius(){return radius.getValue();}

 とりあえず、これでアニメーションしてみた。

//importは略
public class Main  extends Application{
  public static void main(String[] args){launch(args);}
  public void start(Stage stage) throws Exception {
    CirclePane c = new CirclePane(200);
    ObservableList<Node> child = c.getChildren();
    for(int i=0;i<30;i++){
      Color color = new Color(Math.random(), Math.random(),Math.random(), 1);
      child.add(new Circle(20, color));
    }
    VBox vbox = new VBox();
    ObservableList<Node> vchild = vbox.getChildren();
    vchild.add(new Label("top"));
    vchild.add(c);
    vchild.add(new Label("bottom"));
    
    stage.setScene(new Scene(vbox));
    stage.show();
    
    new Timeline(new KeyFrame(new Duration(1500), new KeyValue(c.radiuseProperty(), 250))).play();
  }
}

 以下の内容は勘違いだと気がつきました。ちゃんと、レイアウトの大きさの変更は上のレイアウトに伝わっていました。最初っからがっつりウィンドウサイズを大きくしてから実行した例↓

f:id:nodamushi:20130213201828p:image

 きちんとアニメーションに従ってbottomの位置が変化しました。とりあえず、requestLayoutで良さそうです。


 で、結果というと

f:id:nodamushi:20130213191941p:image:w360

 ふ〜む………。いやね、ウィンドウサイズまで変化するとは私も思っていなかったんですよ。でも、CirclePaneを代入したVBoxはレイアウトを変更してくれる。つまり、上の図では小さくて見にくいと思いますが、topとbottomの文字が円に被らないように再配置されると思ったんですよ。しかし、結果はtopとbottomは動かないっ!再配置されない!

 で、わからんから、OpenJFXのソースコード読んでみたんですよ。そしたら、平気で、impl_markDrityとか意味わからん関数使っとるんす。意味わからねーよ。

 というわけで、私が目的とする動作をさせるためにはどうすればいいのか、誰か教えてください………(´Д⊂ヽ

欽ちゃん1号欽ちゃん1号 2013/02/14 18:09 他にも方法があるかもしれませんが一つ考えてみました。
やらないといけないことが2つあると考えました。
1 c.radiusの変化に応じてStageの大きさを変化させる
2 Stageの大きさの変化に応じてVBoxをresizeする

2についてはチュートリアルにありました
Example 3-5 Using Listeners to Resize an Embedded Application
http://docs.oracle.com/javafx/2/deployment/deploy_overview.htm

1についてはこんな感じで
(c.radius).addListener(
new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> ov,
Number value, Number new_value) {
dp_w.setValue((new_value.doubleValue() + nodeRadius * 2) * 2);
dp_h.setValue((new_value.doubleValue() + nodeRadius * 2) * 2);
owner.setMinWidth(dp_w.getValue());
owner.setMinHeight(dp_h.getValue());

}
});

全体はこんな感じです
Main.java

public class Main extends Application implements ChangeListener<Number>{

private DoubleProperty dp_w = new SimpleDoubleProperty();
private DoubleProperty dp_h = new SimpleDoubleProperty();

private Scene scene;
private Stage owner;
private VBox vbox;
private double nodeRadius = 20;

public static void main(String[] args){launch(args);}

@Override
public void start(Stage stage) throws Exception {

owner = stage;
owner.widthProperty().addListener(this);
owner.heightProperty().addListener(this);
dp_w.setValue(500);
dp_h.setValue(500);

CirclePane c = new CirclePane(200, nodeRadius);

(c.radius).addListener(
new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> ov,
Number value, Number new_value) {
dp_w.setValue((new_value.doubleValue() + nodeRadius * 2) * 2);
dp_h.setValue((new_value.doubleValue() + nodeRadius * 2) * 2);
owner.setMinWidth(dp_w.getValue());
owner.setMinHeight(dp_h.getValue());

}
});

ObservableList<Node> child = c.getChildren();
for(int i=0;i<30;i++){
Color color = new Color(Math.random(), Math.random(),Math.random(), 1);
child.add(new Circle(nodeRadius, color));
}
vbox = new VBox();
vbox.setPadding(new Insets(10, 10, 10, 10));
vbox.setSpacing(10);
ObservableList<Node> vchild = vbox.getChildren();
vchild.add(new Label("top"));
vchild.add(c);
vchild.add(new Label("bottom"));
scene = new Scene(vbox, 500, 500);
stage.setScene(scene);
stage.show();

new Timeline(new KeyFrame(new Duration(10000), new KeyValue(c.radius, 250)
)).play();

}
//relayout the application to match given size
private void resize(double width, double height) {

vbox.setMaxWidth(dp_w.getValue());
System.out.println("dp_w.getValue() = " + dp_w.getValue());

vbox.setMaxHeight(dp_h.getValue());
System.out.println("dp_h.getValue() = " + dp_h.getValue());

}

@Override
public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
resize(owner.getWidth(), owner.getHeight());
}

}

CirclePane.java

public class CirclePane extends Pane{

private double nodePref = 0;

public CirclePane(double r, double nodePref) {

setRadius(r);
this.nodePref = nodePref;

}

public DoubleProperty radius = new DoublePropertyBase() {
@Override
public void invalidated() {
requestLayout();
}

@Override
public String getName() {
return "radius";
}

@Override
public Object getBean() {
return CirclePane.this;
}
};
public final void setRadius(double d){radius.setValue(d);}

public final double getRadius(){return radius.getValue();}


private double getMaxChildWidth(double height){
ObservableList<Node> nodes = getChildren();
double maxWidth=0;
for(Node n:nodes){
double d = n.prefWidth(height);
if(maxWidth < d) {
maxWidth = d;
}
}
return maxWidth;
}
private double getMaxChildHeight(double width){
ObservableList<Node> nodes = getChildren();
double maxHeight=0;
for(Node n:nodes){
double d = n.prefHeight(width);
if(maxHeight < d) {
maxHeight = d;
}
}
return maxHeight;
}

@Override protected double computeMaxWidth(double height) {
return getMaxChildWidth(height)+getRadius()*2;
}

@Override protected double computeMaxHeight(double width) {
return getMaxChildHeight(width)+getRadius()*2;
}

@Override protected double computePrefWidth(double height) {
return getMaxChildWidth(height)+getRadius()*2;
}

@Override protected double computePrefHeight(double width) {
return getMaxChildHeight(width)+getRadius()*2;
}

@Override protected void layoutChildren() {
ObservableList<Node> nodes = getChildren();
int length = nodes.size();
if(length == 0) {
return;
}
double step = 2*PI/length;
double rad = 0;
double width = getWidth();
double height = getHeight();
double r = getRadius();
for(Node n:nodes){

double x = Math.cos(rad)*r+width/2;
double y = Math.sin(rad)*r+height/2;

double w = n.prefWidth(nodePref);
double h = n.prefWidth(nodePref);

x = x- w/2d;
y = y- h/2d;

n.resizeRelocate(x, y, w, h);

rad+=step;
}
}
}

nodamushinodamushi 2013/02/19 15:46 返事が遅れましたが、丁寧にありがとうございます。試したところ実行できました。

aoe-tkaoe-tk 2013/02/25 02:22 > SwingUtilities.invokeLater(Runnable)みたいなことはどうすればいいの?

javafx.application.Platform.runLater() を使います。

> あるプロパティの値の変化でNodeの最適な大きさが変化したときに、どうすれば自動的に大きさを変更できるのか。

JavaFXにはバインディングという仕組みがあって、コンポーネント間のプロパティの値を同期させることができます。以下のさくらばさんのITProの連載記事での説明が一番丁寧で分かり易いと思います。
http://itpro.nikkeibp.co.jp/article/COLUMN/20121219/445461/?ST=develop

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/nodamushi/20130213/1360753476
リンク元