Search code examples
multithreadingbuttonjavafximageview

JavaFX. Threads in button click


After click myButton runs 2 Thread. First change the button image, second - loop. I want my program at first сhange image and after run loop. But it doesnt work, they are finished in one moment.

myButton.setOnMouseClicked(event -> {
            if (event.getButton() == MouseButton.PRIMARY) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        ImageView imageView = new ImageView(image);
                        imageView.setFitHeight(my_button_height - 16);
                        imageView.setFitWidth(my_button_width - 16);
                        myButton.setStyle("-fx-background-color: #ffffff00; ");
                        myButton.setGraphic(imageView);
                    }
                });
                thread.run();
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Thread thread1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 15; i++){
                            System.out.println(i);
                            try {
                                Thread.sleep(500);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                });
                thread1.run();
            }
        });
    }

I tried to add .join. It didnt help


Solution

  • Use a PauseTransition, not threads

    You don't need to use threads to implement your requirement.

    the game, where to find identical cards with image. Card is button with image. When player opened two cards, my program check them and if they are identical everything is good, in another way closed. But I ran into a problem when player open second card, he cannot see it

    The issue is that there is no pause after a player action under certain conditions, so the player cannot see the cards they have flipped over, before they are flipped back.

    This can be fixed using a PauseTransition. Usage is explained in this answer:

    Example

    Here is some example code for a memory game. The example:

    • Applies a pause transition after the user has flipped two cards.
    • UI input is disabled during the pause.
    • When the pause is complete, the UI is updated (either flipping the cards back if they did not match or removing them from the board if they did match).

    The relevant code is the snippet:

    public MemoryGame(String tileText) {
        ObservableList<Tile> tiles = createTiles(tileText);
        FilteredList<Tile> visibleTiles = tiles.filtered(Tile::isShowing);
    
        board = createBoard(tiles);
    
        PauseTransition pauseForCompletion = new PauseTransition(
                FLIP_PAUSE_DURATION
        );
        pauseForCompletion.setOnFinished(e ->
                completeMove(tiles, visibleTiles, board)
        );
    
        visibleTiles.addListener((ListChangeListener<Tile>) c -> {
            if (visibleTiles.size() == 2) {
                board.setDisable(true);
                pauseForCompletion.playFromStart();
            }
        });
    }
    

    This is just a demo app, it isn't thoroughly tested and may have some bugs in it. The demo is probably more complex then needed, it uses a filtered list to register flipped cards, but could be simplified with just a couple of object properties to represent flipped cards instead.

    For a slightly more complex app (or even this one refactored), I'd suggest using a more MVC style approach where the game state is modeled and tested independent of the UI, but for this proof-of-concept demo, the code below is OK for that purpose.

    Hopefully, it illustrates one way to use the PauseTransition concept to solve an issue similar to the one you have.

    MemoryGame.java

    import javafx.animation.PauseTransition;
    import javafx.beans.Observable;
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    import javafx.collections.*;
    import javafx.collections.transformation.FilteredList;
    import javafx.geometry.Insets;
    import javafx.scene.control.Labeled;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.TilePane;
    import javafx.util.Duration;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class MemoryGame {
        private static final Duration FLIP_PAUSE_DURATION = Duration.seconds(
                1
        );
    
        private final TilePane board;
        private final ReadOnlyBooleanWrapper solved = new ReadOnlyBooleanWrapper(
                false
        );
    
        public MemoryGame(String tileText) {
            ObservableList<Tile> tiles = createTiles(tileText);
            FilteredList<Tile> visibleTiles = tiles.filtered(Tile::isShowing);
    
            board = createBoard(tiles);
    
            PauseTransition pauseForCompletion = new PauseTransition(
                    FLIP_PAUSE_DURATION
            );
            pauseForCompletion.setOnFinished(e ->
                    completeMove(tiles, visibleTiles, board)
            );
    
            visibleTiles.addListener((ListChangeListener<Tile>) c ->
                    respondToMove(visibleTiles, board, pauseForCompletion)
            );
        }
    
        private static void respondToMove(
                FilteredList<Tile> visibleTiles,
                TilePane board,
                PauseTransition pauseForCompletion
        ) {
            if (visibleTiles.size() == 2) {
                board.setDisable(true);
                pauseForCompletion.playFromStart();
            }
        }
    
        private void completeMove(
                ObservableList<Tile> tiles,
                FilteredList<Tile> visibleTiles,
                TilePane board
        ) {
            long nShowing = visibleTiles.stream()
                    .map(Labeled::getText)
                    .distinct()
                    .count();
    
            if (nShowing == 1) { // all visible tiles match
                // replace all matching tiles with a blank pane.
                for (Tile tile : visibleTiles) {
                    int tileIdx = board.getChildren().indexOf(tile);
                    board.getChildren().set(tileIdx, new Pane());
                }
                for (int i = visibleTiles.size() - 1; i >= 0; i--) {
                    tiles.remove(visibleTiles.get(i));
                }
    
                if (tiles.isEmpty()) {
                    solved.set(true);
                }
            } else {
                // tiles don't match, so hide them until they are turned again.
                for (int i = visibleTiles.size() - 1; i >= 0; i--) {
                    visibleTiles.get(i).setShowing(false);
                }
            }
    
            board.setDisable(false);
        }
    
        private static TilePane createBoard(ObservableList<Tile> tiles) {
            TilePane board = new TilePane(
                    10, 10
            );
    
            board.getStyleClass().add("board");
    
            board.getChildren().addAll(tiles);
            board.setPrefColumns((int) Math.sqrt(tiles.size()));
            board.setPadding(new Insets(10));
    
            return board;
        }
    
        private static ObservableList<Tile> createTiles(String tileText) {
            List<String> gameText =
                    (tileText + tileText)
                            .chars()
                            .mapToObj(Character::toString)
                            .collect(Collectors.toCollection(ArrayList::new));
            Collections.shuffle(gameText);
    
            ObservableList<Tile> tiles = FXCollections.observableArrayList(
                    tile -> new Observable[] { tile.showingProperty() }
            );
            gameText.stream()
                    .map(Tile::new)
                    .forEachOrdered(tiles::add);
            return tiles;
        }
    
        public TilePane getBoard() {
            return board;
        }
    
        public boolean isSolved() {
            return solved.get();
        }
    
        public ReadOnlyBooleanProperty solvedProperty() {
            return solved.getReadOnlyProperty();
        }
    }
    

    Tile.java

    import javafx.beans.binding.Bindings;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.scene.control.Button;
    
    class Tile extends Button {
        private static final String BLANK = " ";
    
        private final BooleanProperty showing = new SimpleBooleanProperty(
                false
        );
    
        public Tile(String tileText) {
            getStyleClass().add("tile");
    
            textProperty().bind(
                    Bindings
                            .when(showing)
                            .then(tileText)
                            .otherwise(BLANK)
            );
    
            setOnAction(e -> setShowing(true));
        }
    
        public boolean isShowing() {
            return showing.get();
        }
    
        public BooleanProperty showingProperty() {
            return showing;
        }
    
        public void setShowing(boolean showing) {
            this.showing.set(showing);
        }
    }
    

    MemoryGameApp.java

    import javafx.application.Application;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class MemoryGameApp extends Application {
        private static final String TILE_TEXT = "Qn39Pzr#";
        private static final String CSS =
                """
                data:text/css,
                .board .button:disabled {
                    -fx-opacity: 1;
                }
                .tile {
                    -fx-font-family: monospace; -fx-font-size: 40px;
                }
                """;
    
        private MemoryGame memoryGame;
        private Scene scene;
    
        @Override
        public void start(Stage stage) {
            memoryGame = new MemoryGame(TILE_TEXT);
    
            scene = new Scene(memoryGame.getBoard());
            scene.getStylesheets().add(CSS);
            stage.setScene(scene);
            stage.show();
    
            memoryGame.solvedProperty().addListener(new EndGameListener());
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        class EndGameListener implements ChangeListener<Boolean> {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (memoryGame.isSolved()) {
                    memoryGame.solvedProperty().removeListener(this);
                    memoryGame = new MemoryGame(TILE_TEXT);
                    memoryGame.solvedProperty().addListener(this);
                    scene.setRoot(memoryGame.getBoard());
                }
            }
        }
    }