---
category: swing
folder: RangeSlider
title: JSliderを上下に配置した範囲選択スライダーを作成する
title-en: Create a range selection slider with JSlider placed above and below
tags: [JSlider, JLabel, BorderLayout]
author: aterai
pubdate: 2026-02-09T01:26:44+09:00
description: JSliderを調整つまみとして選択範囲と目盛り表示用JLabelの上下に配置し、範囲選択スライダーを作成します。
summary-jp: JSliderを調整つまみとして選択範囲と目盛り表示用JLabelの上下に配置し、範囲選択スライダーを作成します。
summary-en: Create a range selection slider by placing JSlider as adjustment knobs above and below the JLabel that displays the selection range and scale.
image: https://drive.google.com/uc?id=1BRmH3BFdYgaZpDsD7bWP__g3nDS3ugQ0
---
* Summary [#summary]
JSliderを調整つまみとして選択範囲と目盛り表示用JLabelの上下に配置し、範囲選択スライダーを作成します。
// #en{{Create a range selection slider by placing JSlider as adjustment knobs above and below the JLabel that displays the selection range and scale.}}
`JSlider`を調整つまみとして選択範囲と目盛り表示用`JLabel`の上下に配置し、範囲選択スライダーを作成します。
// #en{{Create a range selection slider by placing `JSlider` as adjustment knobs above and below the `JLabel` that displays the selection range and scale.}}

#download(https://drive.google.com/uc?id=1BRmH3BFdYgaZpDsD7bWP__g3nDS3ugQ0)

* Source Code Examples [#sourcecode]
#code(link){{
class RangeSliderPanel extends JPanel {
  private final JSlider lowerSlider;
  private final JSlider upperSlider;

  protected RangeSliderPanel(int min, int max, int lowInit, int highInit) {
    super(new BorderLayout(0, 0));
    upperSlider = createSlider(min, max, highInit, false);
    lowerSlider = createSlider(min, max, lowInit, true);

    ChangeListener cl = e -> {
      if (lowerSlider.getValue() > upperSlider.getValue()) {
        if (Objects.equals(e.getSource(), lowerSlider)) {
          lowerSlider.setValue(upperSlider.getValue());
        } else {
          upperSlider.setValue(lowerSlider.getValue());
        }
      }
      repaint();
    };
    lowerSlider.addChangeListener(cl);
    upperSlider.addChangeListener(cl);

    add(upperSlider, BorderLayout.NORTH);
    add(new RangeBar(lowerSlider, upperSlider));
    add(lowerSlider, BorderLayout.SOUTH);
  }

  @Override public final Component add(Component comp) {
    return super.add(comp);
  }

  @Override public final void add(Component comp, Object constraints) {
    super.add(comp, constraints);
  }

  private static JSlider createSlider(int min, int max, int val, boolean isUp) {
    return new JSlider(min, max, val) {
      @Override public void updateUI() {
        super.updateUI();
        setUI(new TriangleUI(this, isUp));
        setOpaque(false);
        setPaintTicks(false);
        setPaintLabels(false);
      }

      @Override public Dimension getPreferredSize() {
        Dimension d = super.getPreferredSize();
        d.height = 10;
        return d;
      }
    };
  }
}
}}

* Description [#description]
- 上限、下限を指定するつまみとして`JSlider`を作成
-- 水平`JSlider`の矢印つまみのデフォルトの方向は下向きでこれを上向きに変更する方法もあるが、このサンプルでは`BasicSliderUI#paintThumb(...)`をオーバーライドして独自の上向き・下向き三角形を描画する
--- [[JSliderのUIや色を変更する>Swing/VolumeSlider]]
-- `JSlider`側の目盛りやラベルは不要なので、`JSlider#setPaintTicks(false)`、`JSlider#setPaintLabels(false)`などで非表示化
-- `JSlider`のトラックも不要なので`BasicSliderUI#paintTrack(...)`をオーバーライドして非表示化
-- `JSlider`側のラベルが非表示なので`JSlider`側のトラックの余白は`0`となるが、このサンプルでは選択範囲と目盛り表示用`JLabel`でトラック外側にラベルを表示するので、`BasicSliderUI#calculateTrackBuffer()`をオーバーライドして余白を確保する
--- [[JSliderの数値テキストラベルを更新する>Swing/UpdateSliderLabel]]

#code{{
class TriangleUI extends BasicSliderUI {
  private final boolean isUpward;

  protected TriangleUI(JSlider b, boolean isUpward) {
    super(b);
    this.isUpward = isUpward;
  }

  @Override public void paintTrack(Graphics g) {
    // nothing to paint
  }

  @Override public void paintFocus(Graphics g) {
    // nothing to paint
  }

  @Override protected void calculateTrackBuffer() {
    if (slider.getOrientation() == JSlider.HORIZONTAL) {
      trackBuffer = RangeBar.PAD;
    } else {
      super.calculateTrackBuffer();
    }
  }

  @Override public void paintThumb(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(new Color(40, 44, 52));
    Rectangle r = SwingUtilities.calculateInnerArea(slider, null);
    int h = 8;
    // int x = thumbRect.x + (thumbRect.width - w) / 2;
    // int y = isUpward ? thumbRect.y : thumbRect.y + thumbRect.height - h;
    int x = thumbRect.x; // + (thumbRect.width - w) / 2;
    int y = isUpward ? r.y : r.y + r.height - h;
    int[] xps = {x, x + thumbRect.width / 2, x + thumbRect.width};
    int[] yps = isUpward ? new int[] {y + h, y, y + h} : new int[] {y, y + h, y};
    g2.fillPolygon(xps, yps, xps.length);
    g2.dispose();
  }
}
}}

- 選択範囲と目盛り表示用コンポーネントを`JLabel`で作成
-- 範囲部分をマウスでドラッグすると範囲幅を一定に上限と下限を合わせて変更可能
--- [[JLabelとIconで作成した検索位置表示バーをマウスで操作する>Swing/BoundedRangeModel]]
-- 水平`JSlider`のみ対応で垂直`JSlider`には未対応

#code{{
class RangeBar extends JLabel {
  public static final int BAR_HEIGHT = 24;
  public static final int PAD = 20;
  private static final Color MAJOR_TICK_COLOR = new Color(180, 180, 185);
  private static final Color MINOR_TICK_COLOR = new Color(210, 210, 215);
  private static final Color TRACK_BGC = new Color(230, 230, 235);
  private static final Color RANGE_COLOR = new Color(0, 180, 255, 120);
  private final JSlider low;
  private final JSlider up;
  private final Point dragStartPt = new Point(0, 0);
  private int slLow;
  private int slUp;
  private transient MouseAdapter dragListener;

  protected RangeBar(JSlider low, JSlider up) {
    super();
    this.low = low;
    this.up = up;
  }

  @Override public void updateUI() {
    removeMouseListener(dragListener);
    removeMouseMotionListener(dragListener);
    super.updateUI();
    setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    dragListener = new MouseAdapter() {
      @Override public void mousePressed(MouseEvent e) {
        dragStartPt.setLocation(e.getPoint());
        slLow = low.getValue();
        slUp = up.getValue();
        repaint();
      }

      @Override public void mouseReleased(MouseEvent e) {
        dragStartPt.setLocation(-100, -100);
        repaint();
      }

      @Override public void mouseDragged(MouseEvent e) {
        if (dragStartPt.x >= 0) {
          updateRange(e.getX() - dragStartPt.x);
        }
        repaint();
      }
    };
    addMouseListener(dragListener);
    addMouseMotionListener(dragListener);
  }

  private void updateRange(int diff) {
    double trackW = low.getWidth() - PAD * 2d;
    int range = low.getMaximum() - low.getMinimum();
    int delta = (int) Math.round(diff * range / trackW);
    int ln = slLow + delta;
    int un = slUp + delta;
    if (ln >= low.getMinimum() && un <= low.getMaximum()) {
      low.setValue(ln);
      up.setValue(un);
    }
  }

  @Override public Dimension getPreferredSize() {
    return new Dimension(300, BAR_HEIGHT);
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    int w = getWidth() - PAD * 2 - 1;
    int cy = getHeight() / 2;
    int barH = BAR_HEIGHT - 1;
    // 1. paint Track
    paintTrack(g2, w, cy, barH);
    // 2. paint Ticks
    paintTicks(g2, w, cy, barH);
    int lx = getPositionX(low);
    int ux = getPositionX(up);
    // 3. Range bar
    paintRangeBar(g2, cy, barH, lx, ux);
    // 4. Numeric text
    paintNumber(g2, cy, lx, ux);
    g2.dispose();
  }

  private static void paintTrack(Graphics2D g2, int w, int cy, int barH) {
    g2.setColor(TRACK_BGC);
    Shape track = new RoundRectangle2D.Float(
        PAD, cy - barH / 2f, w, barH, 4f, 4f);
    g2.fill(track);
    g2.setColor(TRACK_BGC.darker());
    g2.draw(track);
  }

  private static void paintTicks(Graphics2D g2, int w, int cy, int barH) {
    // g2.setStroke(new BasicStroke(1f));
    for (int i = 0; i <= 100; i += 2) {
      int tx = PAD + (i * w / 100);
      if (i % 10 == 0) {
        // MajorTick
        g2.setColor(MAJOR_TICK_COLOR);
        g2.drawLine(tx, cy - barH / 2, tx, cy + barH / 2);
      } else {
        // MinorTick
        g2.setColor(MINOR_TICK_COLOR);
        g2.drawLine(tx, cy - 4, tx, cy + 4);
      }
    }
  }

  private static void paintRangeBar(
      Graphics2D g2, int cy, int barH, int lx, int ux) {
    g2.setPaint(RANGE_COLOR);
    Shape bar = new RoundRectangle2D.Float(
        lx, cy - barH / 2f, ux - lx, barH, 4f, 4f);
    g2.fill(bar);
    g2.setColor(RANGE_COLOR.darker());
    g2.draw(bar);
  }

  private void paintNumber(Graphics2D g2, int cy, int lx, int ux) {
    g2.setColor(UIManager.getColor("Button.foreground"));
    String txtLow = String.valueOf(low.getValue());
    String txtUp = String.valueOf(up.getValue());
    FontMetrics fm = g2.getFontMetrics();
    int gap = 2;
    int ty = cy + fm.getAscent() / 2 - 1;
    g2.drawString(txtLow, lx - fm.stringWidth(txtLow) - gap, ty);
    g2.drawString(txtUp, ux + gap, ty);
  }

  private static int getPositionX(JSlider slider) {
    int iv = slider.getValue() - slider.getMinimum();
    int range = slider.getMaximum() - slider.getMinimum();
    double v = (double) iv / range;
    Rectangle r = SwingUtilities.calculateInnerArea(slider, null);
    return PAD + (int) (v * (r.width - PAD * 2d));
  }
}
}}

* Reference [#reference]
- [https://github.com/ernieyu/Swing-range-slider/tree/master ernieyu/Swing-range-slider: Swing range slider demo application]
-- 上記の`ernieyu/Swing-range-slider`は下限を`JSlider#getValue()`、上限を`JSlider#getValue() + JSlider#getExtent()`とし、`Extent`の幅を範囲に見立ててひとつの`JSlider`で範囲選択スライダーコンポーネントを作成している
- [[JSliderのUIや色を変更する>Swing/VolumeSlider]]
- [[JSliderの数値テキストラベルを更新する>Swing/UpdateSliderLabel]]
- [[JSliderのトラック内部に目盛りを描画する>Swing/MajorTickOnTrack]]
- [[JSliderの目盛り表示位置をトラック上部に変更する>Swing/SliderTicksPosition]]
- [[JLabelとIconで作成した検索位置表示バーをマウスで操作する>Swing/BoundedRangeModel]]

* Comment [#comment]
#comment
#comment