2011年6月5日日曜日

JSlider でクリックした位置にノブを動かせるようにする

1. JSlider でクリックした位置にノブを動かす場合の課題

Java Swing で JSlider(スライドバーみたいなもの)を使う場合、そのままではバーの特定場所をクリックしても、近くのメモリ上にスライダーは動きますがメモリの間の細かいところには動きません
このスライダーを使うのが数回程度なら、別にクリックで細かく動かせるようにしなくてもいいのではと思いますが、これで操作を行うような場面が何回もある場合、クリックで細かい指定ができないと不便なのですよね。

「JSlider」、「クリック」等、日本語で検索すると、以下の比較的有名なサイトの解決方法が上位に出てきて、このクリック時の問題をうまく解決することができます(非常に参考になります)。
 JSliderでクリックした位置にノブをスライド
  http://terai.xrea.jp/Swing/JumpToClickedPositionSlider.html

ですが、コードを見るとわかるように、「new MetalSliderUI()」となっており、Nimbus 等の Metal でない look and feel を使っている場合、うまく動きません。個人的に Nimbus を使いたい場面もあり、問題でした。


2. 簡単に出来る解決方法(挙動がヘンだけど…)

どういう look and feel でもこの問題を解決できる方法はないかと探してみましたが、日本語で検索した場合、上位に以下のような示唆しか見つかりませんでした。

 スライダーの値をマウスのクリックで設定する方法
  http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=17944&forum=12
  「現時点では簡単な実現方法がないため、
   とりあえずクリックされた座標から値を計算するようにしたいと思います。 」

これをなげやりに実装すると、以下のようになります。固定値の 8 などは location を計算するための補正です(もっと丁寧な方法があると思います)。

 slider.addMouseListener(new MouseListener(){

  @Override
  public void mouseClicked(MouseEvent mouseEvent) {
   Point point = slider.getMousePosition(); 

   int width = slider.getBounds().width - 8*2; 
   int location = (int)(((double)point.x-8.0)/(double)width*slider.getMaximum()); 

   slider.getModel().setValue(location);
  }

  @Override 
  public void mouseEntered(MouseEvent arg0) {  } 

  @Override 
  public void mouseExited(MouseEvent arg0) {  } 

  @Override 
  public void mousePressed(MouseEvent arg0) {  } 

  @Override 
  public void mouseReleased(MouseEvent arg0) {  }
 });

しかし、やってみると分かりますが、この実装にするとクリック後の動きがかなりでこぼこします(1 どキリのいいメモリに動いてから、上記の location の位置に動くため)。


3. 実用的な解決方法

英語で調べてみると、Metal UI でない場合のもう少しスマートな解決方法がありました。

 JSlider question: Position after leftclick
  http://stackoverflow.com/questions/518471/jslider-question-position-after-leftclick

上のほうのコードの方法は、1 で紹介したのと類似で Metal 用ですが、下の「This question is kind of old, but I just ran across this problem myself. This is my solution:」以下のコードは、それ以外の look and feel でも動きます。

引用:

 JSlider slider = new JSlider(/* your options here if desired */) {
 {
  MouseListener[] listeners = getMouseListeners();

  for (MouseListener l : listeners)
   removeMouseListener(l); // remove UI-installed TrackListener …(i)

  final BasicSliderUI ui = (BasicSliderUI) getUI(); // …(ii)
  BasicSliderUI.TrackListener tl = ui.new TrackListener() {

   // this is where we jump to absolute value of click
   @Override
   public void mouseClicked(MouseEvent e) {
    Point p = e.getPoint();
    int value = ui.valueForXPosition(p.x);

    setValue(value);
   }

   // disable check that will invoke scrollDueToClickInTrack
   @Override
   public boolean shouldScroll(int dir) {
    return false;
   }
  };
  addMouseListener(tl);
 }
 };

簡単に説明すると、JSlider を拡張して、 (i) JSlider 中にあるクリックを受け取るリスナ (MouseListener) を一旦 remove して、(ii) クリックで細かく動く listener に新たに設定し直している、ということですね。
これなら実際に動かしてみても、キレイに動きます。

ある程度使う場面が考えられるような機能なので、簡単な設定でこのような挙動で実装できるようになっているといいなと思うのですが、Java 7 でも機能追加されないかな?

基本的に目的の達成を目指して実施してきた過程で副産物的に得られた知見で、英語で調べられれば誰でも解決できる話なのですが、多少他の人の役に立つかなと思い少しまとめてみました。