JavaFX中TableView内双向变色滑块实现实时股票走势可视化
Alright, let's walk through how to build this real-time stock trend visualization in JavaFX TableView. I've built similar custom cell renderers for financial UIs before, so here's a hands-on, step-by-step solution that covers all your requirements:
First, create a data class using JavaFX properties to enable automatic UI updates when data changes. This model will hold all the key stock metrics we need:
import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; public class StockData { private final DoubleProperty openingPrice = new SimpleDoubleProperty(); // 当日开盘价 private final DoubleProperty currentPrice = new SimpleDoubleProperty(); // 当前价格 private final DoubleProperty dailyHigh = new SimpleDoubleProperty(); // 当日最高价 private final DoubleProperty dailyLow = new SimpleDoubleProperty(); // 当日最低价 private final ObservableList<Double> trendData = FXCollections.observableArrayList(); // 实时走势序列 // Getters, setters, and property accessors public double getOpeningPrice() { return openingPrice.get(); } public void setOpeningPrice(double price) { openingPrice.set(price); } public DoubleProperty openingPriceProperty() { return openingPrice; } public double getCurrentPrice() { return currentPrice.get(); } public void setCurrentPrice(double price) { currentPrice.set(price); } public DoubleProperty currentPriceProperty() { return currentPrice; } public double getDailyHigh() { return dailyHigh.get(); } public void setDailyHigh(double price) { dailyHigh.set(price); } public DoubleProperty dailyHighProperty() { return dailyHigh; } public double getDailyLow() { return dailyLow.get(); } public void setDailyLow(double price) { dailyLow.set(price); } public DoubleProperty dailyLowProperty() { return dailyLow; } public ObservableList<Double> getTrendData() { return trendData; } // Add a new trend point and auto-update high/low public void addTrendPoint(double price) { trendData.add(price); if (price > getDailyHigh()) setDailyHigh(price); if (price < getDailyLow()) setDailyLow(price); } }
The core of this implementation is a custom TableCell that draws the trend line, price slider, and high/low labels. We'll override updateItem to bind data and layoutChildren to handle the drawing logic:
import javafx.scene.control.TableCell; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Path; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; public class StockTrendTableCell extends TableCell<StockData, StockData> { private final Text lowPriceLabel = new Text(); private final Text highPriceLabel = new Text(); private final Rectangle priceSlider = new Rectangle(10, 10); // 双向滑动的价格标记 private StockData currentStock; public StockTrendTableCell() { // Set up slider drag interaction (bidirectional sliding) priceSlider.setOnMouseDragged(e -> { if (currentStock == null || getHeight() == 0) return; // Map mouse Y position to price range double priceRange = currentStock.getDailyHigh() - currentStock.getDailyLow(); double normalizedY = e.getY() / getHeight(); double newPrice = currentStock.getDailyHigh() - (normalizedY * priceRange); currentStock.setCurrentPrice(newPrice); }); // Style the slider for visibility priceSlider.setStroke(Color.BLACK); priceSlider.setStrokeWidth(1); } @Override protected void updateItem(StockData stock, boolean empty) { super.updateItem(stock, empty); currentStock = stock; if (empty || stock == null) { setGraphic(null); return; } // Bind labels to stock data (auto-updates when values change) lowPriceLabel.textProperty().bind(stock.dailyLowProperty().asString("%.2f")); highPriceLabel.textProperty().bind(stock.dailyHighProperty().asString("%.2f")); // Auto-switch slider color when crossing opening price stock.currentPriceProperty().addListener((obs, oldPrice, newPrice) -> { double current = newPrice.doubleValue(); priceSlider.setFill(current >= stock.getOpeningPrice() ? Color.GREEN : Color.YELLOW); }); // Trigger layout refresh requestLayout(); } @Override protected void layoutChildren() { super.layoutChildren(); if (currentStock == null || getWidth() == 0 || getHeight() == 0) return; // Position high/low labels (bottom-left and bottom-right) double labelOffset = 5; lowPriceLabel.relocate(labelOffset, getHeight() - lowPriceLabel.getLayoutBounds().getHeight() - labelOffset); highPriceLabel.relocate( getWidth() - highPriceLabel.getLayoutBounds().getWidth() - labelOffset, getHeight() - highPriceLabel.getLayoutBounds().getHeight() - labelOffset ); // Draw the trend line using Path (better for continuous data) ObservableList<Double> trendPoints = currentStock.getTrendData(); if (trendPoints.size() < 2) return; double cellWidth = getWidth(); double cellHeight = getHeight(); double priceRange = currentStock.getDailyHigh() - currentStock.getDailyLow(); double xStep = cellWidth / (trendPoints.size() - 1); Path trendPath = new Path(); for (int i = 0; i < trendPoints.size(); i++) { double price = trendPoints.get(i); double x = i * xStep; // Map price to Y coordinate (higher prices = lower Y in UI) double y = cellHeight - ((price - currentStock.getDailyLow()) / priceRange) * cellHeight; if (i == 0) { trendPath.getElements().add(new MoveTo(x, y)); } else { trendPath.getElements().add(new LineTo(x, y)); } } trendPath.setStroke(Color.GRAY); trendPath.setStrokeWidth(2); // Position the price slider based on current price double sliderY = cellHeight - ((currentStock.getCurrentPrice() - currentStock.getDailyLow()) / priceRange) * cellHeight; priceSlider.relocate(getWidth() / 2 - 5, sliderY - 5); // Center horizontally // Assemble all elements into the cell's graphic Pane cellPane = new Pane(trendPath, lowPriceLabel, highPriceLabel, priceSlider); setGraphic(cellPane); } }
To simulate or connect to a real-time data stream, you can use a timer (for simulation) or WebSocket (for live data). Here's a quick simulation example:
import javafx.application.Platform; import java.util.Timer; import java.util.TimerTask; public class RealTimeDataFeeder { public static void startSimulation(StockData stock) { Timer timer = new Timer(); // Push new data point every second timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { double currentPrice = stock.getCurrentPrice(); // Simulate small price fluctuations double newPrice = currentPrice + (Math.random() - 0.5) * 1.5; // Update on JavaFX application thread Platform.runLater(() -> { stock.addTrendPoint(newPrice); stock.setCurrentPrice(newPrice); }); } }, 0, 1000); } }
Finally, set up your TableView in the main application to use the custom cell factory:
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.stage.Stage; public class StockTrendTableApp extends Application { @Override public void start(Stage stage) { TableView<StockData> tableView = new TableView<>(); // Create trend column TableColumn<StockData, StockData> trendColumn = new TableColumn<>("实时走势"); trendColumn.setCellValueFactory(cellData -> new javafx.beans.property.SimpleObjectProperty<>(cellData.getValue())); trendColumn.setCellFactory(col -> new StockTrendTableCell()); trendColumn.setPrefWidth(350); // Set a wide enough width for the trend tableView.getColumns().add(trendColumn); // Add test stock data StockData testStock = new StockData(); testStock.setOpeningPrice(150.0); testStock.setCurrentPrice(150.0); testStock.setDailyHigh(150.0); testStock.setDailyLow(150.0); tableView.getItems().add(testStock); // Start real-time data simulation RealTimeDataFeeder.startSimulation(testStock); Scene scene = new Scene(tableView, 400, 300); stage.setTitle("Real-Time Stock Trend"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
- Color Logic: The slider automatically switches between green (above/equal to opening price) and yellow (below opening price) via a listener on the
currentPriceProperty. - Bidirectional Slider: The drag event maps the mouse position to the stock's daily price range, letting you slide the marker up/down to adjust the current price.
- Dynamic High/Low: Every new data point automatically updates the daily high/low values, which are reflected in the bottom labels.
- Performance: If you're dealing with hundreds of data points, consider limiting the
trendDatalist to a fixed window (e.g., last 50 points) to keep rendering smooth.
内容的提问来源于stack exchange,提问作者iCoder

