読者です 読者をやめる 読者になる 読者になる

プログラムdeタマゴ

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

relocateはsetLayoutX,setLayoutYの代わりではない

JavaFX

 朝から叫んでおりました内容が解決したので、記事にしておきます。
 まずは、以下のようなクラスを用意します。一辺100の正方形の中心に直径60の円と長さ100の横棒を重ねたような図を表示します。

import javafx.scene.Group;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
public class ShapeGroup extends Group{
  private Circle circle;
  private Line line;
  
  @Override
  protected void layoutChildren(){
    if(circle == null){
      circle = new Circle(50,50,30);
      line = new Line(0,50,100,50);
      getChildren().addAll(circle,line);
    }
  }
  @Override
  public double prefHeight(double width){
    return 100d;
  }
  @Override
  public double prefWidth(double height){
    return 100d;
  }
}



 ちなみに、Groupを継承する場合はcomputePrefWidthとかじゃなくて、prefWidthをオーバーライドします。今日初めて知りました。
 で、下図のようにこいつの棒の端っこに文字をくっつけたいと思います。

import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;


public class ClassA extends Region{
  private String str;
  private Text text;
  private ShapeGroup shape;
  
  public ClassA(String str){
    this.str=str;
  }
  
  private void initInstance(){
    if(text==null){
      text = new Text(str);
      shape = new ShapeGroup();
      getChildren().addAll(text,shape);
    }
  }
  
  @Override
  protected void layoutChildren(){
    initInstance();
    Insets inset = getInsets();
    double il = inset.getLeft();
    double it = inset.getTop();
    double textWidth = text.prefWidth(-1);
    double textHeight = text.prefHeight(textWidth);

    double shapeHeight = shape.prefHeight(-1);
    
    //ラベルのy座標はちょうど
    //shapeの中央に来るようにする
    text.resize(textWidth, textHeight);
    text.relocate(il, it + shapeHeight*0.5-textHeight*0.5);
    
    shape.relocate(textWidth+il, it);
  }
  
  
  @Override
  protected double computePrefHeight(double width){
    initInstance();
    Insets inset = getInsets();
    double shapeHeight = shape.prefHeight(-1);
    return shapeHeight+inset.getTop()+inset.getBottom();
  }
  
  @Override
  protected double computePrefWidth(double height){
    initInstance();
    Insets inset = getInsets();
    double textWidth = text.prefWidth(-1);
    double shapeWidth = shape.prefWidth(-1);
    return textWidth+shapeWidth
        +inset.getLeft()+inset.getRight();
  }
}

実行結果



 あるぇええええええ?




 これの原因が全然分からなくって、Textの高さが指定したものと異なる、もしくは、描画位置が異なるのかとか勝手に思って、勝手にドツボにハマっていました。

 原因はTextではなく、こいつです。

shape.relocate(textWidth+il, it);
ShapeGroup extends Group



 javafx.scene.Node のrelocateには

Sets the node's layoutX and layoutY translation properties in order to relocate this node to the x,y location in the parent.

と書いてある。というわけで、私はsetLayoutXやsetLayoutYではなく、もっぱらrelocateを配置する際に使っていた。しかし、このrelocateは単にsetLayoutXやsetLayoutYの代用メソッドではなくて、割といらないことしてくれる。

//javafx.scene.Nodeのrelocate
    public void relocate(double x, double y) {
        setLayoutX(x - getLayoutBounds().getMinX());
        setLayoutY(y - getLayoutBounds().getMinY());

        PlatformLogger logger = Logging.getLayoutLogger();
        if (logger.isLoggable(PlatformLogger.FINER)) {
            logger.finer(this.toString()+" moved to ("+x+","+y+")");
        }
    }

 というように、単にsetLayoutY(y)とするのではなく、getLayoutBounds().getMinY()という値を引いてしまっている。
 そして、今回、ShapeGroupは高さ100の正方形の中心に直径60の円を描くという前提でプログラムを書いた。すなわち、ShapeGroupの上下にはそれぞれ長さ20の空白がある。getLayoutBounds().getMinY()はこの20を返す。従って、高さ100の正方形をy座標が0の位置に表示したつもりでも、高さ100の正方形をy座標が-20の位置に表示することになる。これが表示しているテキストの位置と横棒の位置がずれた原因である。
 relocateをsetLayoutX,setLayoutYに置き換える、もしくはGroupを継承するのではなく、RegionやPaneなどを継承するように変更すると、私が意図したような正しい表示がされる。




 というわけで、Groupを使うときは要注意と言うことと、relocate(またはresizeRelocate)はsetLayoutXで設定するのと結果が異なる場合があると知っていないといけない、というのが今日の教訓でした。