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

プログラムdeタマゴ

世界の端っこ

JavaFXのWebViewがマウスイベントでJVMごとクラッシュする

 いろんな訳あってブレークポイントをつけれるエディタを内蔵したいのだ。でも、JavaFXには良いテキストエディタがない。(だいたい、いつになったら全角文字がなんか変な色がつくの直るんだ?)
 
 Threadをまたぐのは正直面倒くさいけど、背に腹は代えられない。SwingNodeでつっこんでやるから、Swingでもいいや、と思ったのだが意外にもSwingも全然ねぇ。


 なら、JavaScriptでもいいよ!ということで、検索してみたら見つけました素晴らしいエディタ。その名もAce

 ブレークポイントあり、Emacsキーバインドあり(超高評価)の素晴らしいエディタです。これだ!これを突っ込もう。


 というわけで、突っ込んでみました。

f:id:nodamushi:20150714231705p:plain

import java.nio.file.Path;
import java.nio.file.Paths;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
import javafx.stage.Stage;


public class AceEditor extends Application{
  public static void main(final String[] args){
    launch(args);
  }


  @Override
  public void start(final Stage s) throws Exception{
    final WebView v = new WebView();
    final Path p = Paths.get("ace","editor.html");
    s.setScene(new Scene(v));
    v.getEngine().load(p.toUri().toURL().toString());
    s.show();
  }

}

 一見問題なさそうに見えます。が、コピー&ペーストができません。
 そして、何よりも問題なのは、マウスクリックしていると、突然不意に落ちること。

f:id:nodamushi:20150714231938p:plain


 残されたログを見ても、やっぱりマウスイベントで落ちているようです。
f:id:nodamushi:20150714232108p:plain


 JRE8u45でも試してみましたが、結果は同じでした。


 むろん、これをFirefoxやChromeで動かしても特に問題は起こりません。
 コピペが出来ないだけだったら、JavaScriptを駆使して何とかなるかなとか思ってたんですが、JVMごとクラッシュするとなると流石にどうもならない。





 これがAceだけに限る問題なのか、WebViewそのものの問題なのか、ちょっと判断がつきませんが、WebViewもまだまだ不安定なんだなと思い知らされました。
 バグレポート?めんどくせ



 一応検索用にエラーコード貼り付け。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000000000000, pid=7052, tid=6804
#
# JRE version: Java(TM) SE Runtime Environment (8.0_40-b25) (build 1.8.0_40-b25)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  0x0000000000000000
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#

Graphicを設定したRadioButtonをSkinで使うと表示がおかしくなる

 ControlのSkinにRadioButtonを使う。RadioButtonには画像も表示させたい………。そんなに特別な場面ではないだろう。ところが、おそらくJavaFX8のバグと思われる妙な挙動をしたので、ここで報告しておく。すでにバグとして登録されているかはよく知らない。

 問題となるソースコードは次のようなもの。基本的には、TilePaneを拡張し、子要素に二つのRadioButtonを並べたSkin2というクラスを4つ表示しているだけだ。
 ただし、この二つのRadioButtonには、GraphicとしてAというラベルとBというラベルを設定している。

//import は略
public class Test extends Application{

  @Override
  public void start(final Stage stage) throws Exception{
    //-fx-skinでSkin2をskinに指定
    final Control2 c2 = new Control2();
    //createDefaultSkinでskinを指定
    final Control3 c3 = new Control3();
    //直接Skin2を表示
    final Skin2 s = new Skin2(null);
    //もう一回表示
    final Control2 c22 = new Control2();

    final VBox v = new VBox();
    v.setPadding(new Insets(50));
    v.getChildren().addAll(c2,c3,s,c22);
    stage.setScene(new Scene(v));
    stage.show();
  }

  public static class Control2 extends Control{
    public Control2(){
      setStyle("-fx-skin:'"+Skin2.class.getName()+"'");
    }
  }

  public static class Control3 extends Control{
    @Override
    protected Skin<?> createDefaultSkin(){
      return new Skin2(this);
    }
  }

  public static class Skin2 extends TilePane implements Skin<Control>{
    Control c ;
    public Skin2(final Control c){
      this.c = c;
      final RadioButton
      r1 = new RadioButton("a"),
      r2 = new RadioButton("b");
      r1.setGraphic(new Label("A"));
      r2.setGraphic(new Label("B"));
      getChildren().addAll(r1,r2);
      r1.setSelected(true);
    }

    @Override
    public Control getSkinnable(){return c;}
    @Override
    public Node getNode(){return this;}
    @Override
    public void dispose(){}
  }

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

これを実行すると次のようになる。
f:id:nodamushi:20150302185844p:plain

最初と最後が明らかにおかしい。ボタンはないし、●は変なところに表示されているし、aと表示されていない。しかし、二つ目の「○B b」は正しく表示されている。

Skinで表示すると問題なのかと思ったが、createDefaultSkinを用いると、あら不思議、二列目のように正しく表示されている。ということは、リフレクション関連のバグ?

というわけで、どーいうことなんですかねぇ、これ。

ToggleButtonの集合を簡単に扱えるようにしてみた

 はてなにお布施の時期が過ぎていたので、ついでにはてなブログに移行してみました。プロの値段…高いよ………。

 さて、そんなこんなで、今日もJavaFXネタ。
 RadioButtonって複数のRadioButtonを並べて表示するって使い方が普通の場面だと思う。でも、これをするには

  1. 複数のRadioButtonを用意して
  2. ToggleGroupを用意して
  3. ToggleGroupにRadioButtonを追加して
  4. レイアウト用PaneにRadioButtonを代入して
  5. ToggleGroupのselectedToggleプロパティを監視して必要な値に変換させる

 ってな感じで面倒くさい

 というわけで、もうどっかにあるんだろうけど、この手順を簡略化するクラスを作ってみました。(長いので続きに。もしくはToggleButtons.java)

 で、それを使うと以下のように短く書けます。

//importは略
public class Test extends Application{

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

  @Override
  public void start(final Stage primaryStage) throws Exception{
    final String[] fileList={"None Selected",
        "Lenna","Mandrill","Aerial","Earth","Girl","Parrots","Pepper","Siboat"};
    final ToggleButtons buttons = new ToggleButtons();
    //画像付きRadioButtonを追加
    for(int i=1;i<fileList.length;i++){
      final String fname = fileList[i];
      buttons.createItem(fname,Test.class.getResourceAsStream(fname+".jpg"));
    }
    //選択されているToggleButtonは数値になっている
    buttons.selectedProperty().addListener(new InvalidationListener(){
      @Override
      public void invalidated(final Observable observable){
        System.out.println(fileList[buttons.getSelected()+1]);
      }
    });

    primaryStage.setScene(new Scene(buttons.asFlowPane()));
    primaryStage.show();
  }
}

 実行するとこんな感じ
 f:id:nodamushi:20150301174913p:plain

続きを読む

JavaFXのLineChartがわりと高性能になった件

 ども、一ヶ月ぐらい前に作った自作のLineChartをちまちま更新していたら、何か結構高性能になったんじゃね?ってことで、動画にとって見ましたのでご紹介。
 機能としては、「データを拡大して、スライドさせることが出来る」「マウスポイントに追従してデータの位置を取得できる」「複数のLineChartの端を一致させたまま配置できる」「複数のLineChartでスクロールバーやマウスポイントを同期させられる」「大量のデータを入れてもとりあえず固まらない」あたりです。
NodamushiChart(Github)


 とりあえず、動画とか初めてキャプチャして、それをそのままYoutubeにぶち込んだだけだから画質とか酷いけど、気にしないで。てか、相当昔に取ったnodamushiのアカウントに付けてた名前が田中太郎って宇宙人かよ。(ネタ分かる人居るのかな)


JavaFXで対数グラフを表示する

 と、いうわけで、前回の自作LineChartに対数グラフの機能を実装しますた。
 NodamushiChart
 
 拘ったのはラベルのとこ。102みたいな感じに表示されていますが、これ、JavaFXの標準NumberAxisじゃまず無理ですよね。うぇへへへ。

 LogarithmicAxisの実行サンプルはtest.Test2で見ることが出来ます。

 

JavaFXでLineChartをスクロールしたり高性能にする

やろーども。滅入り苦しみます。

 と、いうわけで、全国約80人のJavaFXユーザーにnodamushiサンタがナイスノンケなプレゼントをもってやってきたぜぃ。


 さて、妙なノリもここまでにして、JavaFXのLineChartって使いにくい、というのが私の結論です。グラフ描画領域内のマウスイベントは取れない、double[]配列でデータあるのをいちいちDataに変換しないといけない、Axisをスクロールさせて描画領域を変化させることも出来ない。複数のグラフを並べて表示させることが出来ない。その上妙なアニメーションやらまじうぜぇ。



 で、これらを解決すべく色々模索した結果………


 LineChart自作してまいました( ゚д゚)メリークリスマス



ソースコード:NodamushiChart


 あ〜、車輪の再発明糞面倒くさいぜー

 まだバグバグしてるし、欲しい機能(マウスイベント、表示範囲の変更など)全てを実装し終わったわけじゃないけど、こんな感じで動きます。(クリスマス前に記事を投稿したかったんじゃー(>_<) )

 図はsinc関数を表示した物です。sinc(0)=1ですが、テストでsinc(0)=∞にしています。このグラフは∞やNaNを自動的に判断して、無限だったら図のような垂直な線を描画、NaNだったら線を切断して表示してくれます。
 また、JavaFXのXYChartの様にグラフの表示エリアをオブジェクトの持つGroupインスタンスの中で構築し、外に出さない様な仕様と違い、グラフ表示領域は一つのクラスとしてつくってあるので、複数のグラフを整頓して配置することも容易に出来るように作ってあります。

 数値解析のような大量のデータをグラフに描画して、観察したい、という用途なら既に結構十分動きます。ソースコードは上記Githubにおいておくのでご自由に使って遊んでみてください。とりあえず、test.Testを実行すれば上の図が出てきます。ライセンスはCC0で著作権とその行使は放棄しているので、好きなように改変、利用してかまいません。
 ドキュメントとかはまぁ、そのうち(気が向いたら)正月の間にやるから待ってくだしぁ。

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

 朝から叫んでおりました内容が解決したので、記事にしておきます。
 まずは、以下のようなクラスを用意します。一辺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で設定するのと結果が異なる場合があると知っていないといけない、というのが今日の教訓でした。