プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

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

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

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

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


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

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




 レイアウトのためのノードを作るには、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();
  }
}

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

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


 で、結果というと


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


 で、わからんから、OpenJFXのソースコード読んでみたんですよ。そしたら、平気で、impl_markDrityとか意味わからん関数使っとるんす。意味わからねーよ。
 というわけで、私が目的とする動作をさせるためにはどうすればいいのか、誰か教えてください………(´Д⊂ヽ