This discussion is archived
7 Replies Latest reply: Mar 3, 2013 4:07 PM by James_D RSS

variable size array in a FlowPane

990211 Newbie
Currently Being Moderated
If i have an array of variable size (for example a array of cards).

How can i build something to show the elements(in a FlowPane for example) and each of them having its own controller (fxml + controller for each card), so the container (flow pane) of the elements (cards) can have it's elements swaped, removed or added new ones?

Controller:
public class HandController extends FlowPane implements Initializable{
    @Override public void initialize(URL arg0, ResourceBundle arg1){
    }
    public void setHand(ArrayList<Cards> Hand){
        //this would work if the hand were static
        for(int i = 0; i < Hand.size(); ++i){
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("CardView.fxml"));
            CardController controller = new CardController();
            fxmlLoader.setController(controller);
            fxmlLoader.setRoot(controller);
            Parent card = (Parent)fxmlLoader.load();
            fxmlLoader.setRoot(card);
            this.getChildren().add(card);
            controller.setCard(Hand.get(i));
        }
    }
}
Fxml:
<fx:root type="FlowPane" xmlns:fx="http://javafx.com/fxml"
         stylesheets="view/Style.css">
    <children>
        <!--should i put something here?-->
    </children>
</fx:root>
  • 1. Re: variable size array in a FlowPane
    James_D Guru
    Currently Being Moderated
    You just need to define the appropriate methods in your controller class for adding, removing, etc cards from the hand. Presumably you will have some kind of controls (buttons, probably) for adding a card, etc; just map the handlers for those buttons to those methods. To implement an add(...) method, create a new card (via the FXML loader) just as you currently do inside the setHand(...) for loop, and pass it to getChildren().add(...). To implement the remove(...) method, just do getChildren(...).remove().

    You might find that your CardController needs a reference to the HandController, but since you're instantiating that controller by hand (pun intended), this won't present any problems.

    If you're going to have an initial hand showing at startup (or when the Hand is first shown), then you could create the cards in the fxml:
    <fx:root type="FlowPane" xmlns:fx="http://javafx.com/fxml"
             stylesheets="view/Style.css">
        <children>
            <fx:include source="CardView.fxml"  fx:id="initialCard" />
            <!-- etc -->
        </children>
    </fx:root>
    or, just leave the children tag empty and initialize in HandController.initialize().
  • 2. Re: variable size array in a FlowPane
    990211 Newbie
    Currently Being Moderated
    thank you for your reply

    is there any way to make it automated? (like the TableView and it's ObservableList, if it is necesary to change the ArrayList to a ObservableList is not a problem)
  • 3. Re: variable size array in a FlowPane
    James_D Guru
    Currently Being Moderated
    I'm not quite sure what you're looking to do...

    The TableView maintains it's list of items in an observable list which can be retrieved with getItems(), and can also be set with setItems(...). To add new rows to a table you call getItems().add(...); to remove them you call getItems().remove(...).

    In this case you have a FlowPane, which contains the content (the cards) as its child nodes. Just as you do in your sample code, you add the card with getChildren().add(...). You can remove them with getChildren().remove(...).

    What do you mean by "automated"?
  • 4. Re: variable size array in a FlowPane
    990211 Newbie
    Currently Being Moderated
    by automated i mean by when removing/adding an element to the list, it automatically removes/adds the element on the pane, like in the TableView (you only do SetItems() one in the TableView wich is the observable list, then when the list is affected, the table shows the changes)

    since adding/removing elements of the hand would be done in the model and not in the view or controller
  • 5. Re: variable size array in a FlowPane
    James_D Guru
    Currently Being Moderated
    The Pane classes don't quite work in relation to their children in the same way as the TableView works in relation to its items. Panes expose the children as an ObservableList<Node> via a getChildren() method. TableView exposes its items as an ObjectProperty<ObservableList<S>>. So one difference is that TableView has a setItems(...) method, enabling you to use an ObservableList from your model as the list of items to display. There's no equivalent setChildren(...) method in the Pane class. The other difference is that TableView is generic, allowing you to specify a type from your model. With a Pane, the type of the children is fixed at Node.

    So to mimic this "automated" updating, you need to bridge the gap between the children and your model. One way to do this would be simply to use the list of children as the data in your model. So something like:

    Model.java
    public class Model {
      private ObservableList<Node> cards ;
    
      ...
     
      public void setCards(ObservableList<Node> cards) {
        this.cards = cards ;
      }
    
      ...
    
      public void addCard(CardView card) {
        cards.add(card);
      }
    
      public void removeCard(CardView card) {
        cards.remove(card);
      }
    
      ...
    }
    HandController.java:
    public class HandController extends FlowPane implements Initializable {
      private final Model model ;
      public HandController() {
         this.model = new Model();
      }
      public void initialize(URL url, ResourceBundle bundle) {
        model.setCards(this.getChildren());
      }
      @FXML 
      public void handleAddCard() {
        CardView card = // load new card from FXML...
        model.addCard(card);
      }
      ...
    }
    The problem with this approach is it blurs the boundary between your model and your view: the model really should be completely agnostic as to the view being used. In this code the model stores the cards as Node objects, which are inherently view-specific.

    If you build a "real" model which is independent of the view, it would look like this:

    Model.java
    public class Model {
        private ObservableList<Card> cards ;
    
      ...
      public ObservableList<Card> getCards() {
        return cards ;
      }
      ...
    
      public void addCard(Card card) {
        cards.add(card);
      }
    
      public void removeCard(Card card) {
        cards.remove(card);
      }
    
      ...
    }
    with an appropriate Card class defined.

    Now your controller needs to bridge the gap between the model and the child list: in other words you need to implement the "automation" you need. The easiest, but not necessarily the most efficient, way would be something like this:

    HandController.java:
    public class HandController extends FlowPane implements Initializable {
      private final Model model ;
      public HandController() {
         this.model = new Model();
      }
      public void initialize(URL url, ResourceBundle bundle) {
        model.getCards().addListener(new ListChangeListener<Card>() {
          @Override
          public void onChanged(Change<? extends Card> change) {
             getChildren().clear();
             for (Card card :  model.getCards()) {
               CardView cardView = // Load card view from FXML file, setting its controller to display the card
               getChildren().add(cardView);
             }
          }
        });
      }
      @FXML 
      public void handleAddCard() {
        Card card = // card to be added...
        model.addCard(card);
      }
      ...
    }
    Here I just rebuild the children every time there's a change in the model. If you find you need something more efficient, you can examine the change passed to the onChanged(...) method and do something more intelligent. Assume there's only a few cards in a hand, though, this is probably fine.

    Hope this gives you some ideas... it's a quiet Sunday, so I can probably throw together something more complete if you need it.
  • 6. Re: variable size array in a FlowPane
    990211 Newbie
    Currently Being Moderated
    yes, something like that is what i was looking for, thank you very much

    in my case, sundays are my lazy days, so i will try it tomorrow
  • 7. Re: variable size array in a FlowPane
    James_D Guru
    Currently Being Moderated
    Well, I was bored...

    This "game" has a hand of up to five cards. You can deal from a deck to the hand, and then play (or discard) a card by double-clicking on it.

    The model comprises a Card class, a Deck class, and a GameModel class. The last one tracks the hand and deck and the relationship between them. Card and Deck were written from my memory of reading an example of using Enums by Josh Bloch.

    Card.java
    package cardgame;
    
    public class Card {
    
         public enum Rank {
              Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King
         };
    
         public enum Suit {
              Clubs, Diamonds, Hearts, Spades
         };
    
         private final Rank rank;
         private final Suit suit;
    
         public Card(Rank rank, Suit suit) {
              this.rank = rank;
              this.suit = suit;
         }
    
         public Rank getRank() {
              return rank;
         }
    
         public Suit getSuit() {
              return suit;
         }
    
         @Override
         public String toString() {
              return String.format("%s of %s", rank, suit);
         }
    
         @Override
         public int hashCode() {
              final int prime = 31;
              int result = 1;
              result = prime * result + ((rank == null) ? 0 : rank.hashCode());
              result = prime * result + ((suit == null) ? 0 : suit.hashCode());
              return result;
         }
    
         @Override
         public boolean equals(Object obj) {
              if (this == obj)
                   return true;
              if (obj == null)
                   return false;
              if (getClass() != obj.getClass())
                   return false;
              Card other = (Card) obj;
              if (rank != other.rank)
                   return false;
              if (suit != other.suit)
                   return false;
              return true;
         }
    
    }
    Deck.java
    package cardgame;
    
    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Random;
    
    import cardgame.Card.Rank;
    import cardgame.Card.Suit;
    
    public class Deck {
    
         private final List<Card> cards;
    
         private Deck() {
              cards = new LinkedList<Card>();
              for (Suit suit : Suit.values())
                   for (Rank rank : Rank.values())
                        cards.add(new Card(rank, suit));
         }
    
         public static Deck newDeck() {
              return new Deck();
         }
    
         public static Deck shuffledDeck() {
              return newDeck().shuffle();
         }
    
         public Deck shuffle() {
              final List<Card> copy = new ArrayList<Card>(cards.size());
              Random rng = new Random();
              while (cards.size() > 0) {
                   int index = rng.nextInt(cards.size());
                   copy.add(cards.remove(index));
              }
              cards.clear();
              cards.addAll(copy);
              return this;
         }
    
         public Card deal() {
              return cards.remove(0);
         }
    
         public int size() {
              return cards.size();
         }
    
    }
    GameModel.java
    package cardgame;
    
    import javafx.beans.binding.BooleanBinding;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    
    public class GameModel {
         private ObservableList<Card> hand;
         private Deck deck;
         private BooleanProperty canDeal;
    
         public GameModel() {
              this.hand = FXCollections.observableArrayList();
              this.deck = Deck.newDeck().shuffle();
              this.canDeal = new SimpleBooleanProperty(this, "canDeal");
              canDeal.bind(new BooleanBinding() {
                   {
                        super.bind(hand);
                   }
    
                   @Override
                   protected boolean computeValue() {
                        return deck.size() > 0 && hand.size() < 5;
                   }
              });
         }
    
         public ObservableList<Card> getHand() {
              return hand;
         }
    
         public ReadOnlyBooleanProperty canDealProperty() {
              return canDeal;
         }
    
         public boolean canDeal() {
              return canDeal.get();
         }
    
         public void deal() throws IllegalStateException {
              if (deck.size() <= 0) {
                   throw new IllegalStateException("No cards left to deal");
              }
              if (hand.size() >= 5) {
                   throw new IllegalStateException("Hand is full");
              }
              hand.add(deck.deal());
         }
    
         public void playCard(Card card) throws IllegalStateException {
              if (hand.contains(card)) {
                   hand.remove(card);
              } else {
                   throw new IllegalStateException("Hand does not contain " + card);
              }
         }
    }
    The CardController is the controller for a CardView. It takes a reference to a Card and its initialize method initializes the view to display it (the view is just a simple label).

    CardController.java
    package cardgame;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    
    public class CardController {
         private final Card card;
    
         @FXML
         private Label label;
    
         public CardController(Card card) {
              this.card = card;
         }
    
         public void initialize() {
              label.setText(String.format("%s%nof%n%s", card.getRank(),
                        card.getSuit()));
         }
    
    }
    The HandController is the controller for the display of the hand. This is where most of the action happens. The trick here is that it requires a reference to a GameModel, which needs to be injected from somewhere, so we need a setModel(...) method, or something similar. I didn't want to assume the order of events: i.e. whether the model is injected before or after the initialize() method is invoked. To keep this flexibility, I used an ObjectProperty to wrap the model, and listen for changes to it. When the model is updated, I register a listener with the hand (exposed by the model). This listener in turn rebuilds the views of the cards when the hand changes. There's also a "deal" button, whose disabled state is managed by binding to a property in the model (using a neat trick from the Bindings class to allow the model value to be dynamic).

    HandController.java
    package cardgame;
    
    import java.io.IOException;
    
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.ListChangeListener;
    import javafx.event.EventHandler;
    import javafx.fxml.FXML;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Node;
    import javafx.scene.control.Button;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.Pane;
    
    public class HandController {
    
         private ObjectProperty<GameModel> model;
         @FXML
         private Pane container;
         @FXML
         private Button dealButton;
    
         public HandController() {
              this.model = new SimpleObjectProperty<GameModel>(this, "model", null);
              final ListChangeListener<Card> handListener = new ListChangeListener<Card>() {
                   @Override
                   public void onChanged(Change<? extends Card> change) {
                        container.getChildren().clear();
                        try {
                             for (Card card : model.get().getHand()) {
                                  container.getChildren().add(loadCardView(card));
                             }
                        } catch (IOException e) {
                             e.printStackTrace();
                        }
                   }
              };
              model.addListener(new ChangeListener<GameModel>() {
    
                   @Override
                   public void changed(
                             ObservableValue<? extends GameModel> observable,
                             GameModel oldValue, GameModel newValue) {
                        if (oldValue != null) {
                             oldValue.getHand().removeListener(handListener);
                        }
                        if (newValue != null) {
                             newValue.getHand().addListener(handListener);
                        }
                   }
    
              });
         }
    
         public void setModel(GameModel model) {
              this.model.set(model);
         }
    
         public void initialize() {
              dealButton.disableProperty().bind(
                        Bindings.selectBoolean(model, "canDeal").not());
         }
    
         private Node loadCardView(final Card card) throws IOException {
              FXMLLoader loader = new FXMLLoader(getClass().getResource(
                        "CardView.fxml"));
              CardController controller = new CardController(card);
              loader.setController(controller);
              Node cardView = (Node) loader.load();
              cardView.setOnMouseClicked(new EventHandler<MouseEvent>() {
                   @Override
                   public void handle(MouseEvent event) {
                        if (event.getClickCount() == 2) {
                             model.get().playCard(card);
                        }
                   }
              });
              return cardView;
         }
    
         public void dealCard() {
              model.get().deal();
         }
    
    }
    Here are the FXML files:

    CardView.fxml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.StackPane?>
    <?import javafx.scene.control.Label?>
    <?import java.lang.String?>
    
    <StackPane xmlns:fx="http://javafx.com/fxml" prefHeight="150" prefWidth="80" minWidth="80" maxWidth="80" minHeight="150" maxHeight="150">
         <Label fx:id="label" >
    
         </Label>
         <styleClass>
              <String fx:value="card"/>
         </styleClass>
     </StackPane>
    Hand.fxml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.FlowPane?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.ScrollPane?>
    <?import java.lang.String?>
    
    <BorderPane xmlns:fx="http://javafx.com/fxml" fx:controller="cardgame.HandController" >
         <center>
              <FlowPane hgap="5" vgap="10" fx:id="container">
              </FlowPane>
         </center>
         <bottom>
              <Button text="Deal" onAction="#dealCard" fx:id="dealButton"/>
         </bottom>
    </BorderPane>
    The overall application is managed by a Game.fxml:
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.BorderPane?>
    
    <BorderPane xmlns:fx="http://javafx.com/fxml" fx:controller="cardgame.GameController">
         <center>
              <fx:include source="Hand.fxml" fx:id="hand" />
         </center>
    </BorderPane>
    with a GameController:
    package cardgame;
    
    import javafx.fxml.FXML;
    
    public class GameController {
    
         @FXML private HandController handController ;
         
         private GameModel model ;
         
         public GameController() {
              model = new GameModel();
         }
         
         public void initialize() {
              handController.setModel(model);
         }
    }
    Game.java is the main class:
    package cardgame;
    
    import java.io.IOException;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.paint.Color;
    import javafx.stage.Stage;
    
    public class Game extends Application {
    
         @Override
         public void start(Stage primaryStage) throws IOException {
              Scene scene = new Scene(FXMLLoader.<Parent>load(getClass().getResource("Game.fxml")), 600, 400, Color.DARKGREEN);
              scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
              primaryStage.setScene(scene);
              primaryStage.show();
         }
    
         public static void main(String[] args) {
              launch(args);
         }
    }
    and then a style sheet

    style.css:
    @CHARSET "US-ASCII";
    
    .card {
         -fx-background-color: white ;
         -fx-border-color: black ;
         -fx-border-radius: 5 ;
         -fx-border-style: solid ;
         -fx-padding: 5 ;
    }
    
    .card .label {
         -fx-text-alignment: center ;
    }

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points