JavaFX: введите безопасный TilePane / ListView с тайлами - PullRequest
0 голосов
/ 01 февраля 2020

отредактировано: ОК, я думаю, что в основном решил свою собственную проблему, или, по крайней мере, у меня есть кое-что, что работает, но это кажется довольно хакерским и небезопасным ... Я просто добавил немного логики c в TilePane, чтобы заставить его делать то, что я хочу, но это много пользовательского кода. Не уверен, что это был правильный способ сделать это, но он работает ... Публикация моего решения на случай, если у кого-то возникнут отзывы или подобные вопросы в будущем. Это нормально?

В основном мне интересно, нормально ли мне приводить ноды в CardViews, когда я щелкаю их внутри TilePanes? Могу ли я сделать это лучше с ListView? И есть ли способ сделать это с другим компонентом, который будет создавать минимум узлов вне экрана?

Оригинал: я работаю над карточной игрой JavaFX, и мне было интересно, как я могу получить безопасный TilePane, если это имеет смысл. Прямо сейчас я работаю над составлением колоды в игре. Я хочу отображать карты, сгенерированные из списка карт, и хочу отображать их в строках по 3, если это возможно. Я хочу макет TilePane, но безопасность типов ListView.

ListView безопасен для типов, но его элементы могут отображаться только в виде списка (по горизонтали или вертикали), тогда как TilePane дает мне точно макет, который я хочу, но не типобезопасный.

Можно ли как-то получить оба? Должен ли я просто создать собственный компонент?

Вот (более полный) Минимальный воспроизводимый пример.

DeckBuilderControllerMRE

import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;

import java.util.ArrayList;
import java.util.HashMap;

public class DeckBuilderControllerMRE {
    @FXML TilePane availableCards, cardsInDeck;

    //maps from a list of available cards to the number of those cards in the deck
    ObservableMap<Card, Integer> currentDeckMap = FXCollections.observableMap(new HashMap<>());
    //these map from card data objects to view nodes, which is wasteful but works...
    HashMap<Card, CardView> availableViewsMap = new HashMap<>(), deckViewsMap = new HashMap<>();

    public void initialize() {
        //dummy card data
        ArrayList<Card> allCards = new ArrayList<>();
        allCards.add(new Card("sword guy", 5));
        allCards.add(new Card("arrow man", 3));
        allCards.add(new Card("shablagoo", 10000000));
        allCards.add(new Card("guy # 4", 4));
        allCards.add(new Card("Dr. Evil", 7));
        allCards.add(new Card("one more", -5));

        //this maps cards to total avaialble, because you're only allowed to have a certain number of each card
        HashMap<Card, Integer> cardQuantities = new HashMap<>();
        for(int i = 0; i < allCards.size(); i++)
            cardQuantities.put(allCards.get(i), i);

        //instantiate card views and add to view maps
        //also sets up binding for quantity labels
        for(Card card : allCards){
            currentDeckMap.put(card, 0);

            CardView availableView = card.getCardView();
            availableView.quantityLabel.textProperty().bind(Bindings.concat("Quantity Available: ",
                    Bindings.subtract(cardQuantities.get(card), Bindings.integerValueAt(currentDeckMap, card))));

            availableViewsMap.put(card, availableView);
            availableCards.getChildren().add(availableView);

            CardView deckView = card.getCardView();
            deckView.quantityLabel.textProperty().bind(Bindings.concat("Quantity in deck: ", Bindings.integerValueAt(currentDeckMap, card)));
            deckViewsMap.put(card, deckView);
        }

        //clicking available cards adds them to your deck
        availableCards.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
            //check if I clicked the container instead of a card
            if(event.getTarget() == availableCards)
                return;

            //otherwise I bubble up to the container node CardView
            Node node = (Node) event.getTarget();
            while(node.getParent() != availableCards)
                node = node.getParent();

            //and then I cast the nodes to CardViews
            //which is the part I think might need fixing
            CardView selectedView = (CardView) node;
            Card selectedCard = selectedView.card;


            //and then I update the map which updates the quantityLabel
            currentDeckMap.put(selectedCard, currentDeckMap.get(selectedCard) + 1);

            event.consume();
        });

        //and this does the same thing but removes them
        cardsInDeck.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
            if(event.getTarget() == cardsInDeck)
                return;

            Node node = (Node) event.getTarget();
            while(node.getParent() != cardsInDeck)
                node = node.getParent();

            CardView selectedView = (CardView) node;
            Card selectedCard = selectedView.card;

            currentDeckMap.put(selectedCard, currentDeckMap.get(selectedCard) - 1);

            event.consume();
        });

        currentDeckMap.addListener((MapChangeListener<Card, Integer>) change -> {
            Card card = change.getKey();
            int stillAvailable = cardQuantities.get(card) - currentDeckMap.get(card);

            if(stillAvailable == 0)
                availableCards.getChildren().remove(availableViewsMap.get(card));
            if(stillAvailable == 1 && !availableCards.getChildren().contains(availableViewsMap.get(card)))
                availableCards.getChildren().add(availableViewsMap.get(card));

            if(currentDeckMap.get(card) == 0)
                cardsInDeck.getChildren().remove(deckViewsMap.get(card));
            else if(currentDeckMap.get(card) == 1 && !cardsInDeck.getChildren().contains(deckViewsMap.get(card)))
                cardsInDeck.getChildren().add(deckViewsMap.get(card));
        });
    }

    class Card {
        String cardName;
        int cardStrength;

        Card(String n, int s){
            cardName = n;
            cardStrength = s;
        }

        public CardView getCardView(){
            return new CardView(this);
        }
    }

    //cards usually have images and stuff, but this is enough to see what I'm doing
    //also, CardView extends some kind of container node, currently VBox, but that might change later
    class CardView extends VBox {
        Card card;
        Label nameLabel, strengthLabel, quantityLabel;

        CardView(Card card){
            this.card = card;
            nameLabel = new Label(card.cardName);
            strengthLabel = new Label(card.cardStrength + "");
            quantityLabel = new Label();
            this.getChildren().addAll(nameLabel, strengthLabel, quantityLabel);
        }
    }

}

f xml выглядит следующим образом ( это пограничная панель с TilePanes, обернутыми в ScrollPanes слева и справа). (Вам может понадобиться добавить идентификатор пакета в атрибут fx: controller. Все остальное должно работать, хотя).

deck_builder.f xml

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="900.0" prefWidth="1300.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="DeckBuilderControllerMRE">
   <left>
      <ScrollPane hbarPolicy="NEVER" minHeight="-Infinity" minViewportHeight="200.0" minViewportWidth="400.0" minWidth="-Infinity" prefHeight="200.0" prefViewportHeight="200.0" prefViewportWidth="400.0" prefWidth="400.0" BorderPane.alignment="CENTER">
         <content>
            <TilePane fx:id="availableCards" hgap="10.0" prefColumns="3" tileAlignment="TOP_LEFT" vgap="10.0" />
         </content>
         <BorderPane.margin>
            <Insets />
         </BorderPane.margin>
      </ScrollPane>
   </left>
   <right>
      <ScrollPane hbarPolicy="NEVER" minHeight="-Infinity" minViewportHeight="200.0" minViewportWidth="400.0" minWidth="-Infinity" prefHeight="200.0" prefViewportHeight="200.0" prefViewportWidth="400.0" prefWidth="400.0" BorderPane.alignment="CENTER">
         <content>
            <TilePane fx:id="cardsInDeck" hgap="10.0" prefColumns="3" tileAlignment="TOP_LEFT" vgap="10.0" />
         </content>
         <BorderPane.margin>
            <Insets />
         </BorderPane.margin>
      </ScrollPane>
   </right>
</BorderPane>

, и это мой основной метод (в основном просто загружает и отображает f xml)

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args){
        launch(args)
    }

    public void start(Stage primaryStage) throws Exception {
        window = primaryStage;
        window.setTitle("Card Game");
        window.setScene(new Scene(FXMLLoader.load(getClass().getResource("layouts/deck_builder.fxml")), sceneWidth, sceneHeight));
        window.show();
    }
}

Это моя первая публикация на Stack Overflow, поэтому любые отзывы о том, как я могу улучшить эту вещь, приветствуются, хотя я добрый попыток практиковать быстрое развитие здесь, поэтому незначительные оптимизации на самом деле не являются приоритетом.

Спасибо за вашу помощь / время.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...