Wie kann man die automatische Bewegung eines JSlider kontinuierlich machen?

Hallo zusammen, bevor das Forum schließt stelle ich mal noch eine Frage: Ich habe für ein Programm einen JSlider verwendet, um die ablaufende Zeit anzuzeigen. Leider bewegt er sich nicht kontinuierlich, sondern sprunghaft, im Beispiel im Sekundentakt.

Ich hab aus meinem Code ein KKSB extrahiert und darin alle Kommentare entfernt, um hier keinen Platz zu verschwenden, ich hoffe, es ist auch ohne diese verständlich.

Dazu benötigt man drei Klassen:

  1. TimeSliderDemo
package stc.ui.timeslider;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class TimeSliderDemo {

    private final JFrame frame;

    private final TimeRunningSlider timeSlider;

    private boolean paused;

    public TimeSliderDemo() {
        frame = new JFrame();
        timeSlider = new TimeRunningSlider();

        paused = false;

        init();
        createGui();
        start();
    }

    private void init() {
        initFrame();
    }

    private void initFrame() {
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setTitle("Time-Slider Demo");
        frame.setPreferredSize(new Dimension(600, 200));
    }

    private void createGui() {
        frame.add(createStartStopButtonPart(), BorderLayout.NORTH);
        frame.add(createTimePart(), BorderLayout.CENTER);
    }

    private Component createStartStopButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(1, 0, 10, 0));

        panel.add(createStartButton());
        panel.add(createPauseButton());
        panel.add(createStopButton());

        return panel;
    }

    private Component createStartButton() {
        JButton button = new JButton("Start");
        button.addActionListener(e -> start());
        return button;
    }

    private void start() {
        timeSlider.start(10);
    }

    private Component createPauseButton() {
        JButton button = new JButton("Pause");
        button.addActionListener(e -> pause());
        return button;
    }

    private void pause() {
        if (paused) {
            timeSlider.endPause();
        }
        else {
            timeSlider.pause();
        }
        paused = !paused;
    }


    private Component createStopButton() {
        JButton button = new JButton("Stopp");
        button.addActionListener(e -> stop());
        return button;
    }

    private void stop() {
        timeSlider.stop();
    }

    private Component createTimePart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(timeSlider.getSlider(), BorderLayout.CENTER);

        return panel;
    }

    public void run() {
        SwingUtilities.invokeLater(() -> {
            frame.pack();
            frame.setVisible(true);
        });
    }

    public static void main(String[] args) {
        new TimeSliderDemo().run();
    }

}
  1. TimeRunningSlider
package stc.ui.timeslider;

import java.util.Hashtable;

import javax.swing.JLabel;

import javax.swing.JSlider;

public class TimeRunningSlider {

    private final JSlider slider;

    private long millisStarted;

    private long deltaMillis;

    private long millisToStop;

    private long millisPausedAt;

    private ActualisationRunnable actualisationRunnable;

    public TimeRunningSlider() {
        slider = new JSlider();

        initSlider();
    }

    private void initSlider() {
        slider.setMinimum(0);
        slider.setMaximum(0);
        slider.setValue(0);
        slider.setEnabled(false);
        slider.setPaintTicks(true);
        slider.setSnapToTicks(false);
        slider.setMajorTickSpacing(60);
        slider.setMinorTickSpacing(15);
    }

    public void start(int seconds) {
        stopActualisationRunnable();

        millisStarted = System.currentTimeMillis();
        deltaMillis = 1000L * seconds;
        millisToStop = millisStarted + deltaMillis;
        int maximum = (int) (deltaMillis / 1000L);
        slider.setMaximum(maximum);
        slider.setValue(0);

        String minutesSeconds = secondsToMinutesSeconds(seconds);
        Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
        labelTable.put(Integer.valueOf(0), new JLabel("0"));
        labelTable.put(Integer.valueOf(maximum), new JLabel(minutesSeconds));
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        createRunnableAndStartActualisationThread();
    }

    private static String secondsToMinutesSeconds(int lengthInSeconds) {
        int minutes = lengthInSeconds / 60;
        int secondsLeft = lengthInSeconds % 60;
        String between;
        if (secondsLeft < 10) {
            between = ":0";
        }
        else {
            between = ":";
        }
        return minutes + between + secondsLeft;
    }

    private void createRunnableAndStartActualisationThread() {
        long actualisationTimeMillis = 10l;
        actualisationRunnable = new ActualisationRunnable(() -> actualiseSlider(),
                actualisationTimeMillis);
        Thread thread = new Thread(actualisationRunnable);
        thread.start();
    }

    private void actualiseSlider() {
        long millisNow = System.currentTimeMillis();
        if (millisNow >= millisToStop) {
            slider.setValue(slider.getMaximum());
            stop();
        }
        else {
            long millisFromStart = millisNow - millisStarted;
            int secondsFromStart = (int) (millisFromStart / 1000L);
            slider.setValue(secondsFromStart);
        }
    }

    public void pause() {
        millisPausedAt = System.currentTimeMillis();
        stopActualisationRunnable();
    }

    public void endPause() {
        long millisPauseEndAt = System.currentTimeMillis();
        long pauseDelta = millisPauseEndAt - millisPausedAt;
        millisStarted += pauseDelta;
        millisToStop += pauseDelta;
        createRunnableAndStartActualisationThread();
    }

    public void stop() {
        stopActualisationRunnable();
    }

    private void stopActualisationRunnable() {
        if (null != actualisationRunnable) {
            actualisationRunnable.stop();
        }
    }

    public JSlider getSlider() {
        return slider;
    }

}
  1. ActualisationRunnable
package stc.ui.timeslider;

class ActualisationRunnable implements Runnable {

    private final static int SLEEP_TIME = 50;


    private volatile boolean actualisationRunning;

    private final Runnable actualisationRunnable;

    private volatile long sleepTimeMillis;

    public ActualisationRunnable(Runnable actualisationRunnable) {
        this(actualisationRunnable, SLEEP_TIME);
    }

    public ActualisationRunnable(Runnable actualisationRunnable, long sleepTimeMillis) {
        this.actualisationRunnable = actualisationRunnable;
        this.sleepTimeMillis = sleepTimeMillis;
    }

    public void setSleepTimeMillis(int sleepTimeMillis) {
        this.sleepTimeMillis = sleepTimeMillis;
    }

    @Override
    public void run() {
        actualisationRunning = true;
        loop();
    }

    private void loop() {
        while (actualisationRunning) {
            actualisationRunnable.run();
            sleep(sleepTimeMillis);
        }
    }

    private static void sleep(long milliseconds) {
        try {
            Thread.sleep(milliseconds);
        }
        catch (InterruptedException e) {
            // do nothing
        }
    }

    public void stop() {
        actualisationRunning = false;
    }

}

Die Frage ist nun, wie ich den Slider zu einer kontinuierlichen, nicht sprunghaften Fortbewegung bringen könnte.

Sollte das nicht gehen, könnte ich natürlich auch eine JProgressBar nehmen, aber in meiner Anwendung soll der Benutzer den Knopf auch bewegen können.

Eigentlich müsste die Lösung ja relativ einfach sein, aber ich habe mit dem JSlider schon allerlei ausprobiert und diesen Effekt nicht wegbekommen.

Du könntest (das ist leider eher ins Blaue geraten), falls du das nicht schon gemacht hast, eine Kombination aus Major und Minor Tick Spacing und Snap To ticks (false) probieren in Kombination mit https://stackoverflow.com/a/2351341

Ansonsten würde mir jetzt auch nichts erfolgversprechenderes einfallen (Swing ist aber auch schon ewig her bei mir)

könnte das nicht damit zusammen hängen dass du nur 10 Werte hast und damit nur 10 Schritte, wenn müsste man den Slider animieren, aber da bin ich überfragt :smiley:

Denk auch dran, dass Du das UI nur im UI-Thread tatsächlich ändern kannst:

        else {
            long millisFromStart = millisNow - millisStarted;
            int secondsFromStart = (int) (millisFromStart / 1000L);
            SwingUtilities.invokeLater(() ->slider.setValue(secondsFromStart));
        }

bye
TT

1 „Gefällt mir“

Hatte ich erst kürzlich (heute Morgen …) für meine Website in JS programmiert:

let id = null;
let total = 10000;
let current = 1000;
function move() {
  if (!id) {
	let elem1 = document.getElementById("myBar");
	let elem2 = document.getElementById("duration");
	let lastTimeNow = performance.now();
	id = setInterval(frame, 100);
	function frame() {
	  let timeNow = performance.now();
	  current += timeNow - lastTimeNow;
	  lastTimeNow = timeNow;
	  if (current >= total) {
		clearInterval(id);
		id = null;
		window.location.href = "https://...";
	  } else {
		let width = parseInt(current / total * 100.0);
		elem1.style.width = width + "%";
		elem1.innerHTML = width + " %";
		elem2.innerHTML = ((total - current) / 1000.0).toFixed(1);
	  }
	}
  } else {
	clearInterval(id);
	id = null;
  }
}
move();

TL;DR: Du brauchst einfach eine höhere Granularität.

Wunderbar. Weder Swing noch ein Slider.
Wie schon festgestellt, könnte er auch eine ProgressBar nehmen… wie du

(Ich geh einfach mal davon aus, wenn das Ding „myBar“ heißt)

Ne wenn man seinen grausamen Code ansieht, hat er einfach nur paar HTML Elemente benutzt um etwas wie eine Progressbar zu bauen. Aber wie üblich, am Thema vorbei und schlechter Code

:light_bulb:
:star_struck: :backhand_index_pointing_up:

Genial! Die Option, das Ding einfach ans Ende ziehen zu können, wünsche ich mir bei Progress-Bars auch immer :smiley:

Der Hauptpunkt ist wohl tatsächlich, dass einfach nur 10 Schritte gemacht werden, und insebsondere auch, dass der Wertebereich des Sliders in der start-Methode auf [0, maximum] gesetzt wird, und das Maximum sind die Sekunden - d.h. der Slider kann gar keine Werte dazwischen annehmen

Im wesentlichen würde es darauf rauslaufen, den Slider nicht mit Sekunden, sondern Millisekunden zu füttern. Hier die wichtigsten Änderungen mit XXX markiert:


import java.util.Hashtable;

import javax.swing.JLabel;

import javax.swing.JSlider;
import javax.swing.SwingUtilities;

public class TimeRunningSlider {

    private final JSlider slider;

    private long millisStarted;

    private long deltaMillis;

    private long millisToStop;

    private long millisPausedAt;

    private ActualisationRunnable actualisationRunnable;

    public TimeRunningSlider() {
        slider = new JSlider();

        initSlider();
    }

    private void initSlider() {
        slider.setMinimum(0);
        slider.setMaximum(0);
        slider.setValue(0);
        slider.setEnabled(false);
        slider.setPaintTicks(true);
        slider.setSnapToTicks(false);
        
        // XXX
        //slider.setMajorTickSpacing(60);
        //slider.setMinorTickSpacing(15);
        slider.setMajorTickSpacing(1 * 1000); // Seconds for demo
        slider.setMinorTickSpacing(15 * 1000);
        
    }

    public void start(int seconds) {
        stopActualisationRunnable();

        millisStarted = System.currentTimeMillis();
        deltaMillis = 1000L * seconds;
        millisToStop = millisStarted + deltaMillis;
        int maximum = (int) (deltaMillis / 1000L);
        
        // XXX
        //slider.setMaximum(maximum);
        slider.setMaximum((int)deltaMillis);
        
        slider.setValue(0);

        String minutesSeconds = secondsToMinutesSeconds(seconds);
        Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
        labelTable.put(Integer.valueOf(0), new JLabel("0"));
        
        // XXX
        //labelTable.put(Integer.valueOf(maximum), new JLabel(minutesSeconds));
        labelTable.put(Integer.valueOf((int)deltaMillis), new JLabel(minutesSeconds));
        
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        createRunnableAndStartActualisationThread();
    }

    private static String secondsToMinutesSeconds(int lengthInSeconds) {
        int minutes = lengthInSeconds / 60;
        int secondsLeft = lengthInSeconds % 60;
        String between;
        if (secondsLeft < 10) {
            between = ":0";
        }
        else {
            between = ":";
        }
        return minutes + between + secondsLeft;
    }

    private void createRunnableAndStartActualisationThread() {
        long actualisationTimeMillis = 10l;
        actualisationRunnable = new ActualisationRunnable(() -> {
            // XXX Do on event dispatch thread
            SwingUtilities.invokeLater(() -> actualiseSlider()); 
        },
        actualisationTimeMillis);
        Thread thread = new Thread(actualisationRunnable);
        thread.start();
    }

    private void actualiseSlider() {
        long millisNow = System.currentTimeMillis();
        if (millisNow >= millisToStop) {
            slider.setValue(slider.getMaximum());
            stop();
        }
        else {
            long millisFromStart = millisNow - millisStarted;
            int secondsFromStart = (int) (millisFromStart / 1000L);
            
            // XXX 
            //slider.setValue(secondsFromStart);
            slider.setValue((int)millisFromStart);
        }
    }

    public void pause() {
        millisPausedAt = System.currentTimeMillis();
        stopActualisationRunnable();
    }

    public void endPause() {
        long millisPauseEndAt = System.currentTimeMillis();
        long pauseDelta = millisPauseEndAt - millisPausedAt;
        millisStarted += pauseDelta;
        millisToStop += pauseDelta;
        createRunnableAndStartActualisationThread();
    }

    public void stop() {
        stopActualisationRunnable();
    }

    private void stopActualisationRunnable() {
        if (null != actualisationRunnable) {
            actualisationRunnable.stop();
        }
    }

    public JSlider getSlider() {
        return slider;
    }

}

Darüber hinaus gibt es andere Punkte - die ganze Struktur mit dem Runnable sieht komplizierter aus, als sie vermutlich sein müßte.

Details, wie dass „to actualise“ auf Deutsch sowas wie „verwirklichen“ oder „realisieren“ heißt (und „aktualisieren“ leider kaum mit etwas besserem als „update“ übersetzt werden kann), sind vielleicht gerade nicht relevant.

2 „Gefällt mir“

Danke, hab ich eingebaut!

Oh guter Punkt! Ich habe die Klassen- und Variablennamen geändert. Peinlich, aber ich dachte wirklich, es würde aktualisieren heißen, schließlich klingt es so ähnlich.

Das ist vermutlich des Pudels Kern. Mit deinen Änderungen macht es genau das, was ich will, danke! Nun muss ich das nur noch auf meine eigentliche Anwenudng adaptieren, aber das sollte ja nicht schwer sein.