英文:
How to get coordinates of content node within ScrollPane?
问题
我正在尝试实现一个与CorelDraw/InDesign类似功能的ScrollPane:在ScrollPane内部可以移动的一张纸(更改视口),并且可以放大/缩小。
我的问题是我不知道如何获取ScrollPane内页面的坐标。我阅读了许多关于如何实现具有可缩放内容的ScrollPane的文章,并了解到需要使用Group和额外的Node来使整个页面在ScrollPane内可见。
我的目标是:
如果页面被放大,以至于上部和下部超出视口,但背景(在我这里是一个VBox)在左侧和右侧可见,我想知道在页面内容左侧的ScrollPane视口中可见多少个像素的VBox(右侧相同)。
使用当前的代码,我知道页面的左上角与ScrollPane视口的左边缘有多远,但如果整个页面可见,它总是显示0,0。
有没有办法以我的组件排列方式实现这个目标?
我的目前代码如下:
// 你的Java代码
请注意,上面的代码是Java代码,如果你需要更多帮助或解释,请告诉我。
英文:
I'm trying to implement a ScrollPane with similar functionality as CorelDraw/InDesign: A piece of paper within a ScrollPane that can be moved around (Changing ViewPort) and that can be enlarged/ zoomed in out.
My problem is that I don't know how to get the coordinates of the Page within the ScrollPane. I read many articles about how to implement a ScrollPane with zoomable content and learned that a Group and an additional Node was necessary to make the entire Page visible within a ScrollPane.
What I want:
If the Page is zoomed in so that the upper part and lower part is outside the viewport but the background (in my case a VBox) is visible left and right, I want to know how many pixels of the VBox are visible in the ScrollPanes viewport left of the page content (And same right).
With the current code I know how far the upper left corner of the page is "away" from the left edge of the ScrollPanes viewport but if the entire page is visible it always says 0,0.
Any ideas how to achieve that with my arrangements of components?
My code so far:
package demo;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class MVP extends Application {
private Stage stage = null;
private Group zoomGroup = null;
private VBox centeredVBox = null;
private ScrollPane scrollPane = null;
private StackPane page = null;
@Override
public void start(Stage primaryStage) throws Exception {
this.stage = primaryStage;
this.page = new StackPane();
this.page.setPrefWidth(42840);
this.page.setPrefHeight(60624);
this.page.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
this.zoomGroup = new Group(page);
this.centeredVBox = new VBox(zoomGroup);
this.centeredVBox.setAlignment(Pos.CENTER);
this.centeredVBox
.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
this.scrollPane = new ScrollPane(centeredVBox);
this.scrollPane.setPannable(true);
this.scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
this.scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
this.scrollPane.setFitToHeight(true); // center
this.scrollPane.setFitToWidth(true); // center
this.scrollPane.setPrefSize(800, 600);
this.centeredVBox.setOnScroll(evt -> {
if (evt.isControlDown()) {
this.onScroll(evt.getTextDeltaY(), new Point2D(evt.getX(), evt.getY()));
}
this.scrollPane.layout();
});
this.scrollPane.setOnMouseReleased(evt -> {
// HOW TO GET THE POSITION OF THE PAGE WITHIN VIEWPORT OF SCROLLPANE IN PIXEL COORDINATES?
System.out.println(
"Upper left: " + (this.scrollPane.getViewportBounds().getMinX() * (1 / this.page.getScaleX())) + "/"
+ (this.scrollPane.getViewportBounds().getMinY() * (1 / this.page.getScaleY())));
System.out.println(
"Lower Right: " + (this.scrollPane.getViewportBounds().getMaxX() * (1 / this.page.getScaleX()))
+ "/" + (this.scrollPane.getViewportBounds().getMaxY() * (1 / this.page.getScaleY())));
});
this.page.setOnMouseMoved(evt -> {
this.stage.setTitle(String.valueOf(evt.getX() + "/" + evt.getY()));
});
Scene scene = new Scene(this.scrollPane);
primaryStage.setTitle("Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
// this.pageRepresentation.setLastClickedPoint(mousePoint);
double zoomFactor = Math.exp(wheelDelta * 0.02);
Bounds innerBounds = this.zoomGroup.getLayoutBounds();
Bounds viewportBounds = this.scrollPane.getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.scrollPane.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.scrollPane.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = this.page.parentToLocal(this.zoomGroup.parentToLocal(mousePoint));
// this.pageRepresentation.setLastClickedPoint(posInZoomTarget);
// calculate adjustment of scroll position (pixels)
Point2D adjustment = this.page.getLocalToParentTransform()
.deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// this.pageRepresentation.setLastClickedPoint(adjustment);
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomGroup.getBoundsInLocal();
this.scrollPane
.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.scrollPane
.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
double zoomValue = this.page.getScaleX() * zoomFactor;
this.page.setScaleX(zoomValue);
this.page.setScaleY(zoomValue);
this.page.layout();
this.scrollPane.layout();
}
}
答案1
得分: 4
Solution
将坐标转换为一个通用的坐标系统,例如场景坐标,然后您可以直接比较它们相对于彼此的位置。
Sample Application Discussion
我从这里提取了代码:
然后修改它以报告坐标。也许它会报告您正在寻找的值。
我并不真正理解您的应用程序代码,所以我没有使用它。
这个答案中有很多代码,但其中大部分只是从链接的答案中复制而来。
唯一新的部分是用于报告场景根、ScrollPane视口和视口中各个节点的坐标的内容。
private void reportCoords() {
Node viewport = scene.lookup(".viewport");
Bounds sceneBounds = scene.getRoot().getLayoutBounds();
Bounds viewportBoundsInScene = viewport.localToScene(viewport.getBoundsInLocal());
Bounds starBoundsInScene = star.localToScene(star.getBoundsInLocal());
Bounds curveBoundsInScene = star.localToScene(curve.getBoundsInLocal());
String alertText = """
Root: %s
Viewport: %s
Star: %s
Curve: %s
""".formatted(
formatBounds(sceneBounds),
formatBounds(viewportBoundsInScene),
formatBounds(starBoundsInScene),
formatBounds(curveBoundsInScene)
);
System.out.println(alertText);
Alert displayCoordsAlert = new Alert(
Alert.AlertType.INFORMATION,
alertText
);
displayCoordsAlert.initOwner(stage);
displayCoordsAlert.setHeaderText("Coordinate bounds in scene coordinates");
displayCoordsAlert.getDialogPane()
.lookup(".content")
.setStyle("-fx-font-family: monospace");
displayCoordsAlert.getDialogPane().setPrefSize(550, 200);
displayCoordsAlert.showAndWait();
}
private String formatBounds(Bounds bounds) {
return "minX: %5d, minY: %5d, maxX: %5d, maxY: %5d".formatted(
(int) bounds.getMinX(),
(int) bounds.getMinY(),
(int) bounds.getMaxX(),
(int) bounds.getMaxY()
);
}
要使用它,请运行应用程序,然后使用菜单或按 r
键生成一个警报和 sysout 日志,报告感兴趣的事物的当前坐标(以场景坐标表示)。
场景坐标用于使所有坐标在相同的坐标系统中。您可以使用它们来比较绝对位置,如果我理解您的问题正确的话,这是您正在尝试做的事情。
示例中的ScrollPane是可平移和可缩放的,这在计算视图中项目的坐标时会考虑到。因此,滚动和平移以移动内容,然后按 r
键报告所有内容的当前场景坐标。
对节点边界进行到场景坐标系统的转换是使用以下代码完成的:
star.localToScene(star.getBoundsInLocal())
如果您要获取坐标的节点也进行了转换(那么您可能需要使用 getBoundsInParent()
而不是 getBoundsInLocal()
)。但在这种情况下,这并不是必要的。ScrollPane中的根节点进行了转换,而不是直接转换其中的节点。因此,根节点的每个子节点的boundsInLocal和boundsInParent是等价的。
Sample Application
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
// ...
// (此处省略了代码的其余部分)
}
以上是您提供的代码的翻译部分。如果您有任何其他翻译需求,请告诉我。
英文:
Solution
Convert coordinates to a common coordinate system, e.g. scene coordinates, then you can directly compare their positions relative to each other.
Sample Application Discussion
I lifted the code from here:
And modified it to report coordinates. Perhaps it reports the values you are looking for.
I didn't really understand your app code, so I did not use that.
There is a lot of code in this answer, but most of it is just copied from the linked answer.
The only new bit is the stuff that reports coordinates for the scene root, ScrollPane viewport, and individual nodes in the viewport.
private void reportCoords() {
Node viewport = scene.lookup(".viewport");
Bounds sceneBounds = scene.getRoot().getLayoutBounds();
Bounds viewportBoundsInScene = viewport.localToScene(viewport.getBoundsInLocal());
Bounds starBoundsInScene = star.localToScene(star.getBoundsInLocal());
Bounds curveBoundsInScene = star.localToScene(curve.getBoundsInLocal());
String alertText = """
Root: %s
Viewport: %s
Star: %s
Curve: %s
""".formatted(
formatBounds(sceneBounds),
formatBounds(viewportBoundsInScene),
formatBounds(starBoundsInScene),
formatBounds(curveBoundsInScene)
);
System.out.println(alertText);
Alert displayCoordsAlert = new Alert(
Alert.AlertType.INFORMATION,
alertText
);
displayCoordsAlert.initOwner(stage);
displayCoordsAlert.setHeaderText("Coordinate bounds in scene coordinates");
displayCoordsAlert.getDialogPane()
.lookup(".content")
.setStyle("-fx-font-family: monospace");
displayCoordsAlert.getDialogPane().setPrefSize(550, 200);
displayCoordsAlert.showAndWait();
}
private String formatBounds(Bounds bounds) {
return "minX: %5d, minY: %5d, maxX: %5d, maxY: %5d".formatted(
(int) bounds.getMinX(),
(int) bounds.getMinY(),
(int) bounds.getMaxX(),
(int) bounds.getMaxY()
);
}
To use it, run the app and either use the menu or press the r
key to generate an alert and sysout log reporting the current coordinates of interesting things (in scene coordinates).
Scene coordinates are used so that all coordinates are in the same coordinate system. You can use them to compare absolute positions, which, if I understood your question, is something you are trying to do.
The ScrollPane in the example is pannable and zoomable and that is taken into account when calculating the coordinates of items in the viewpoint. So scroll and pan to move the content around, then press the r
key to report the current scene coordinates of everything.
Transformation on node bounds to the scene coordinate system is done using this code:
star.localToScene(star.getBoundsInLocal())
If the node you want coordinates of is also transformed (then you may need to use getBoundsInParent()
rather than getBoundsInLocal()
). But in this case, it wasn't necessary. The root node in the scroll pane was transformed and not, directly, the nodes inside it. So each child node of the root has boundsInLocal and boundsInParent that are equivalent to each other.
Sample Application
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
public static void main(String[] args) {
launch(args);
}
private final Node star = createStar();
private final Node curve = createCurve();
private Stage stage;
private Scene scene;
@Override
public void start(final Stage stage) {
this.stage = stage;
final Group group = new Group(star, curve);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener((observable, oldValue, newValue) ->
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight())
);
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(event -> {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(event ->
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()))
);
scrollContent.setOnMouseDragged(event -> {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(event -> stage.close());
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(event -> {
group.setScaleX(1);
group.setScaleY(1);
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(event -> {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(event -> {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
Menu coordsMenu = new Menu("_Coords");
MenuItem reportCoordsMenuItem = new MenuItem("_Report");
reportCoordsMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.R));
reportCoordsMenuItem.setOnAction(event -> reportCoords());
coordsMenu.getItems().setAll(reportCoordsMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu, coordsMenu);
return menuBar;
}
private void reportCoords() {
Node viewport = scene.lookup(".viewport");
Bounds sceneBounds = scene.getRoot().getLayoutBounds();
Bounds viewportBoundsInScene = viewport.localToScene(viewport.getBoundsInLocal());
Bounds starBoundsInScene = star.localToScene(star.getBoundsInLocal());
Bounds curveBoundsInScene = star.localToScene(curve.getBoundsInLocal());
String alertText = """
Root: %s
Viewport: %s
Star: %s
Curve: %s
""".formatted(
formatBounds(sceneBounds),
formatBounds(viewportBoundsInScene),
formatBounds(starBoundsInScene),
formatBounds(curveBoundsInScene)
);
System.out.println(alertText);
Alert displayCoordsAlert = new Alert(
Alert.AlertType.INFORMATION,
alertText
);
displayCoordsAlert.initOwner(stage);
displayCoordsAlert.setHeaderText("Coordinate bounds in scene coordinates");
displayCoordsAlert.getDialogPane()
.lookup(".content")
.setStyle("-fx-font-family: monospace");
displayCoordsAlert.getDialogPane().setPrefSize(550, 200);
displayCoordsAlert.showAndWait();
}
private String formatBounds(Bounds bounds) {
return "minX: %5d, minY: %5d, maxX: %5d, maxY: %5d".formatted(
(int) bounds.getMinX(),
(int) bounds.getMinY(),
(int) bounds.getMaxX(),
(int) bounds.getMaxY()
);
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论