简体   繁体   English

如何使用LinkedList和MouseListeners在屏幕上绘制曲线?

[英]How to draw curved lines on the screen with LinkedList and MouseListeners?

I am new to Java, as for my class's final project, I am developing a paint application that enables users to draw rich curved lines and common geometric shapes like rectangle and oval, also users can undo their shape drawing. 我是Java的新手,关于我班的最后一个项目,我正在开发一个绘画应用程序,它使用户能够绘制丰富的曲线以及常见的几何形状(例如矩形和椭圆形),并且用户可以撤消其形状绘制。

My app worked well on shape drawing. 我的应用程序在形状绘制方面效果很好。 When I want to draw shapes, the shapes I want to draw are stored in the LinkedList which will be called to draw shapes on the screen When I applied this LinkedList concept to draw curved lines on the screen. 当我要绘制形状时,要绘制的形状存储在LinkedList中,当我应用此LinkedList概念在屏幕上绘制曲线时,该链接被称为在屏幕上绘制形状。 I modified shape drawing code to draw curved lines. 我修改了形状绘制代码以绘制曲线。 I ran to a problem; 我遇到一个问题; The result I got was little dots on the screen. 我得到的结果是屏幕上的小点。 If I tried to copy similar code that draws shapes for drawing curved lines, I would get straight lines instead of the curved lines. 如果我尝试复制类似的代码来绘制用于绘制曲线的形状,则将获得直线而不是曲线。

I think this problem is related to MouseListener interfaces. 我认为此问题与MouseListener接口有关。 I need some suggestions on how can I modify my MouseListener methods so that I can draw the curved lines on the screen properly. 我需要一些有关如何修改MouseListener方法的建议,以便可以在屏幕上正确绘制曲线。

Changes to my source code are welcome: 欢迎更改我的源代码:

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class PaintAppPlusSecondDraft extends JApplet implements ActionListener {
private static final long serialVersionUID = 1L;

private JMenuBar menuBar = new JMenuBar();

private JMenu mainMenu = new JMenu("Main menu");
private JMenuItem howToUse = new JMenuItem("How to use?");
private JMenuItem toDefaultMode = new JMenuItem("Return to default mode");

private JMenu boardSettings = new JMenu("Paint board settings");
private JMenuItem clearBoard = new JMenuItem("Clear screen");
private JMenuItem toDefaultBoard = new JMenuItem("Set to default paint board");
private JMenuItem bCustom = new JMenuItem("Set background colour");

private JMenu brushSettings = new JMenu("Paintbrush settings");
private JMenuItem eraser = new JMenuItem("Eraser");
private JMenuItem toDefaultBrush = new JMenuItem("Set to default paintbrush");
private JMenu setBrushSize = new JMenu("Set paintbrush size");
private JMenuItem Two = new JMenuItem("2 pixels");
private JMenuItem Four = new JMenuItem("4 pixels");
private JMenuItem Six = new JMenuItem("6 pixels");
private JMenuItem Eight = new JMenuItem("8 pixels");
private JMenuItem Ten = new JMenuItem("10 pixels");
private JMenu setBrushType = new JMenu("Set paintbrush type");
private JMenuItem defaultType1 = new JMenuItem("Default");
private JMenuItem defaultType2 = new JMenuItem("Default (light stroke)");
private JMenuItem Custom = new JMenuItem("Set paintbrush colour");

private JMenu drawShapes = new JMenu("Draw shapes");
private JMenuItem undoShape = new JMenuItem("Undo shape drawing");
private JMenuItem StraightLine = new JMenuItem("Straight line");
private JMenuItem Rectangle = new JMenuItem("Rectangle");
private JMenuItem Oval = new JMenuItem("Oval");
private JMenuItem filledRectangle = new JMenuItem("Filled rectangle");
private JMenuItem filledOval = new JMenuItem("Filled oval");

private int prevBrushSize = PaintBoard.brushSize;
private int prevBrushType = PaintBoard.brushType;
private Color prevBrushColour = PaintBoard.currentColour; /* For better user experience */

public void init() {
    Frame frame = (Frame) getParent().getParent();
    frame.setTitle("JAVA Paint plus");
    frame.setResizable(false);

    this.setSize(600, 400);
    this.setContentPane(new PaintBoard());

    mainMenu.add(howToUse);
    mainMenu.add(toDefaultMode);
    mainMenu.add(about);

    boardSettings.add(clearBoard);
    boardSettings.add(toDefaultBoard);
    boardSettings.add(bCustom);

    brushSettings.add(eraser);
    brushSettings.add(toDefaultBrush);
    setBrushSize.add(Two);
    setBrushSize.add(Four);
    setBrushSize.add(Six);
    setBrushSize.add(Eight);
    setBrushSize.add(Ten);
    brushSettings.add(setBrushSize);
    setBrushType.add(defaultType1);
    setBrushType.add(defaultType2);
    setBrushType.add(waterColourBrush);
    setBrushType.add(triangleType);
    setBrushType.add(squareType);
    setBrushType.add(hexagonType);
    setBrushType.add(starType);
    setBrushType.add(heartType);
    brushSettings.add(setBrushType);
    brushSettings.add(Custom);

    drawShapes.add(undoShape);
    drawShapes.add(StraightLine);
    drawShapes.add(Rectangle);
    drawShapes.add(Oval);
    drawShapes.add(filledRectangle);
    drawShapes.add(filledOval);

    menuBar.add(mainMenu);
    menuBar.add(boardSettings);
    menuBar.add(brushSettings);
    menuBar.add(drawShapes);

    howToUse.addActionListener(this);
    toDefaultMode.addActionListener(this);

    clearBoard.addActionListener(this);
    toDefaultBoard.addActionListener(this);
    bCustom.addActionListener(this);

    eraser.addActionListener(this);
    toDefaultBrush.addActionListener(this);
    Two.addActionListener(this);
    Four.addActionListener(this);
    Six.addActionListener(this);
    Eight.addActionListener(this);
    Ten.addActionListener(this);
    defaultType1.addActionListener(this);
    defaultType2.addActionListener(this);
    Custom.addActionListener(this);

    undoShape.addActionListener(this);
    StraightLine.addActionListener(this);
    Rectangle.addActionListener(this);
    Oval.addActionListener(this);
    filledRectangle.addActionListener(this);
    filledOval.addActionListener(this);

    this.setJMenuBar(menuBar);
    this.setVisible(true);
}

@Override
public void actionPerformed(ActionEvent e) {
    if (e.getSource() == howToUse) {
        JOptionPane.showMessageDialog(null, "Press your mouse on the board and drag to draw!", "How to use?", JOptionPane.INFORMATION_MESSAGE);
    }

    if (e.getSource() == toDefaultMode) {
        PaintBoard.canvasColour = Color.WHITE;
        PaintBoard.brushSize = prevBrushSize = 6;
        PaintBoard.brushType = prevBrushType = 1;
        PaintBoard.currentColour = prevBrushColour = Color.BLACK;
        PaintBoard.shapes.makeEmpty();
        repaint();
    }

    if (e.getSource() == clearBoard) {
        PaintBoard.shapes.makeEmpty();
        repaint();
    }

    if (e.getSource() == toDefaultBoard) {
        PaintBoard.canvasColour = Color.WHITE;
        repaint();
    }

    if (e.getSource() == bCustom) {
        try {
            Color customColour = JColorChooser.showDialog(null, "Select colour:", PaintBoard.canvasColour);

            PaintBoard.canvasColour = customColour;
            PaintBoard.shapes.makeEmpty();
            repaint();
        } catch (NullPointerException ex) {

        }
    }

    if (e.getSource() == eraser) {
        prevBrushSize = PaintBoard.brushSize;
        prevBrushType = PaintBoard.brushType;
        prevBrushColour = PaintBoard.currentColour;
        PaintBoard.brushSize = 44;
        PaintBoard.brushType = 1;
        PaintBoard.currentColour = PaintBoard.canvasColour;
    }

    if (e.getSource() == toDefaultBrush) {
        PaintBoard.brushSize = prevBrushSize = 6;
        PaintBoard.brushType = prevBrushType = 1;
        PaintBoard.currentColour = prevBrushColour = Color.BLACK;
    }

    if (e.getSource() == Two) {
        PaintBoard.brushSize = prevBrushSize = 2;
        PaintBoard.brushType = prevBrushType;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == Four) {
        PaintBoard.brushSize = prevBrushSize = 4;
        PaintBoard.brushType = prevBrushType;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == Six) {
        PaintBoard.brushSize = prevBrushSize = 6;
        PaintBoard.brushType = prevBrushType;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == Eight) {
        PaintBoard.brushSize = prevBrushSize = 8;
        PaintBoard.brushType = prevBrushType;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == Ten) {
        PaintBoard.brushSize = prevBrushSize = 10;
        PaintBoard.brushType = prevBrushType;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == defaultType1) {
        PaintBoard.brushSize = prevBrushSize;
        PaintBoard.brushType = prevBrushType = 1;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == defaultType2) {
        PaintBoard.brushSize = prevBrushSize;
        PaintBoard.brushType = prevBrushType = 2;
        PaintBoard.currentColour = prevBrushColour;
    }

    if (e.getSource() == Custom) {
        try {
            Color customColour = JColorChooser.showDialog(null, "Select colour:", PaintBoard.currentColour);

            PaintBoard.brushSize = prevBrushSize;
            PaintBoard.brushType = prevBrushType;
            PaintBoard.currentColour = prevBrushColour = customColour;
        } catch (NullPointerException ex) {

        }
    }

    if (e.getSource() == undoShape) {
        try {
            PaintBoard.shapes.removeFront();
            repaint();
        } catch (NullPointerException ex) {

        }
    }

    if (e.getSource() == StraightLine) {
        PaintBoard.brushSize = prevBrushSize;
        PaintBoard.currentColour = prevBrushColour;
        PaintBoard.shapeType = 1;
    }

    if (e.getSource() == Rectangle) {
        PaintBoard.currentColour = prevBrushColour;
        PaintBoard.shapeType = 2;
        PaintBoard.filled = false;
    }

    if (e.getSource() == Oval) {
        PaintBoard.currentColour = prevBrushColour;
        PaintBoard.shapeType = 3;
        PaintBoard.filled = false;
    }

    if (e.getSource() == filledRectangle) {
        PaintBoard.currentColour = prevBrushColour;
        PaintBoard.shapeType = 2;
        PaintBoard.filled = true;
    }

    if (e.getSource() == filledOval) {
        PaintBoard.currentColour = prevBrushColour;
        PaintBoard.shapeType = 3;
        PaintBoard.filled = true;
    }
}

abstract static class brushLine {
    private int brushSize;
    private int brushType;
    private Color brushColour;

    public brushLine() {
        brushSize = 6;
        brushType = 1;
        brushColour = Color.BLACK;
    }

    public brushLine(int brushSize, int brushType, Color brushColour) {
        this.brushSize = brushSize;
        this.brushType = brushType;
        this.brushColour = brushColour;
    }

    public void setBrushSize(int brushSize) {
        this.brushSize = brushSize;
    }

    public void setBrushType(int brushType) {
        this.brushType = brushType;
    }

    public void setBrushColour(Color brushColour) {
        this.brushColour = brushColour;
    }

    public int getBrushSize() {
        return brushSize;
    }

    public int getBrushType() {
        return brushType;
    }

    public Color getBrushColour() {
        return brushColour;
    }

    abstract void show(Graphics window);
}

static class paintBrushLine extends brushLine {
    private int x1, y1, x2, y2;

    public paintBrushLine() {
        super();
        x1 = 0;
        y1 = 0;
        x2 = 0;
        y2 = 0;
    }

    public paintBrushLine(int brushSize, int brushType, Color brushColour, int x1, int y1, int x2, int y2) {
        super(brushSize, brushType, brushColour);
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }

    public void setX1(int x1) {
        this.x1 = x1;
    }

    public void setY1(int y1) {
        this.y1 = y1;
    }

    public void setX2(int x2) {
        this.x2 = x2;
    }

    public void setY2(int y2) {
        this.y2 = y2;
    }

    public int getX1() {
        return x1;
    }

    public int getY1() {
        return y1;
    }

    public int getX2() {
        return x2;
    }

    public int getY2() {
        return y2;
    }

    public void show(Graphics window) {
        window.setColor(getBrushColour());

        switch (getBrushType()) {

        case 1://Default brush
            ((Graphics2D) window).setStroke(new BasicStroke(getBrushSize(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
            window.drawLine(getX1(), getY1(), getX2(), getY2());
            break;
        case 2://Default brush (light stroke) --> Makes empty squares with few randomly positioned pixels
            int[] pixelPos = new int[2];

            for (int i = 0; i < ((getBrushSize()*getBrushSize()) / 10); i++) {
                pixelPos[0] = new Random().nextInt(getBrushSize());
                pixelPos[1] = new Random().nextInt(getBrushSize());

                window.drawRect(getX1() + pixelPos[0], getY1() + pixelPos[1], 1, 1);
            }

            break;
        }
    }
}

abstract static class Shape {
    private int x1, y1, x2, y2;
    private int thickness;
    private Color shapeColour;

    public Shape() {
        x1 = 0;
        y1 = 0;
        x2 = 0;
        y2 = 0;
        shapeColour = Color.BLACK;
    }

    public Shape(int x1, int y1, int x2, int y2, int thickness, Color shapeColour)
    {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.thickness = thickness;
        this.shapeColour = shapeColour;
    }

    public void setX1(int x1) {
        this.x1 = x1;
    }

    public void setY1(int y1) {
        this.y1 = y1;
    }

    public void setX2(int x2) {
        this.x2 = x2;
    }

    public void setY2(int y2) {
        this.y2 = y2;
    }

    public void setThickness(int thickness) {
        this.thickness = thickness;
    }

    public void setShapeColour(Color shapeColour) {
        this.shapeColour = shapeColour;
    }

    public int getX1() {
        return x1;
    }

    public int getY1() {
        return y1;
    }

    public int getX2() {
        return x2;
    }

    public int getY2() {
        return y2;
    }

    public int getThickness() {
        return thickness;
    }

    public Color getShapeColour() {
        return shapeColour;
    }

    abstract void sketch(Graphics window);
}

abstract static class boundedShape extends Shape {
    private int shapeType;
    private boolean filled;

    public boundedShape() {
        super();
        filled = false;
    }

    public boundedShape(int x1, int y1, int x2, int y2, int thickness, Color shapeColour, int shapeType, boolean filled) {
        super(x1, y1, x2, y2, thickness, shapeColour);
        this.shapeType = shapeType;
        this.filled = filled;
    }

    public void setShapeType(int shapeType) {
        this.shapeType = shapeType;
    }

    public void setFilled(boolean filled) {
        this.filled = filled;
    }

    public int getUpperLeftX() {
        return Math.min(getX1(), getX2());
    }

    public int getUpperLeftY() {
        return Math.min(getY1(), getY2());
    }

    public int getWidth() {
        return Math.abs(getX1() - getX2());
    }


    public int getHeight() {
        return Math.abs(getY1() - getY2());
    }

    public int getShapeType() {
        return shapeType;
    }

    public boolean getFilled() {
        return filled;
    }

    abstract public void sketch(Graphics window);
}

static class geometricShape extends boundedShape {
    public geometricShape() {
        super();
    }

    public geometricShape(int x1, int y1, int x2, int y2, int thickness, Color shapeColour, int shapeType, boolean filled) {
        super(x1, y1, x2, y2, thickness, shapeColour, shapeType, filled);
    }

    public void sketch(Graphics window) {
        window.setColor(getShapeColour());

        switch(getShapeType()) {

        case 1://Straight line
            ((Graphics2D) window).setStroke(new BasicStroke(getThickness(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
            window.drawLine(getX1(), getY1(), getX2(), getY2());
            break;
        case 2://Rectangle
            if (getFilled())
                window.fillRect(getUpperLeftX(), getUpperLeftY(), getWidth(), getHeight());
            else {
                ((Graphics2D) window).setStroke(new BasicStroke(getThickness(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                window.drawRect(getUpperLeftX(), getUpperLeftY(), getWidth(), getHeight());
            }

            break;
        case 3://Oval
            if (getFilled())
                window.fillOval(getUpperLeftX(), getUpperLeftY(), getWidth(), getHeight());
            else {
                ((Graphics2D) window).setStroke(new BasicStroke(getThickness(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                window.drawOval(getUpperLeftX(), getUpperLeftY(), getWidth(), getHeight());
            }

            break;
        }
    }
}

static class PaintBoard extends JPanel implements MouseMotionListener, MouseListener {
    private static final long serialVersionUID = 1L;

    private boolean painting;
    private int prevX, prevY;
    private static Color canvasColour = Color.WHITE;
    private static int brushSize = 6;
    private static int brushType = 1;
    private static Color currentColour = Color.BLACK;

    private static LinkedList<brushLine> brushLines = new LinkedList<>();
    private brushLine currentBrushLine;

    private static LinkedList<Shape> shapes = new LinkedList<>();
    private static int shapeType = 0;
    private Shape currentShape = null;
    private static boolean filled;

    private JLabel mouseCoordinates = new JLabel("X: 0 pixels     Y: 0 pixels");

    public PaintBoard() {
        setSize(getWidth(), getHeight());
        setLayout(new BorderLayout());
        add(mouseCoordinates, BorderLayout.SOUTH);
        addMouseMotionListener(this);
        addMouseListener(this);
    }

    @Override
    public void paintComponent(Graphics board) {
        super.paintComponent(board);

        board.setColor(canvasColour);
        board.fillRect(0, 0, getWidth(), getHeight());

        ArrayList<brushLine> linesToDraw = brushLines.getArray();

        for (int i = linesToDraw.size() - 1; i >= 0; i--)
            linesToDraw.get(i).show(board);

        if (currentBrushLine != null)
            currentBrushLine.show(board);

        ArrayList<Shape> shapesToDraw = shapes.getArray();

        for (int j = shapesToDraw.size() - 1; j >= 0; j--)
            shapesToDraw.get(j).sketch(board);

        if (currentShape != null)
            currentShape.sketch(board);
    }

    public void mouseDragged(MouseEvent e) {
        mouseCoordinates.setText(String.format("X: %d pixels     Y: %d pixels", e.getX(), e.getY()));

        if (shapeType > 0) {
            currentShape.setX2(e.getX());
            currentShape.setY2(e.getY());
            repaint();
        } else {
            if (!painting)
                return;

            ((paintBrushLine) currentBrushLine).setX1(prevX);
            ((paintBrushLine) currentBrushLine).setY1(prevY);
            ((paintBrushLine) currentBrushLine).setX2(e.getX());
            ((paintBrushLine) currentBrushLine).setY2(e.getY());
            repaint();

            prevX = e.getX();
            prevY = e.getY();
        }
    }

    public void mouseMoved(MouseEvent e) {
        mouseCoordinates.setText(String.format("X: %d pixels     Y: %d pixels", e.getX(), e.getY()));
    }

    public void mouseClicked(MouseEvent e) {

    }

    public void mouseEntered(MouseEvent e) {

    }

    public void mouseExited(MouseEvent e) {

    }

    public void mousePressed(MouseEvent e) {
        switch (shapeType) {

        case 0://Curved line
            if (painting)
                return;

            prevX = e.getX();
            prevY = e.getY();

            currentBrushLine = new paintBrushLine(brushSize, brushType, currentColour, prevX, prevY, prevX, prevY);

            painting = true;
            break;
        default://Other shapes
            currentShape = new geometricShape(e.getX(), e.getY(), e.getX(), e.getY(), brushSize, currentColour, shapeType, filled);
            painting = false;
            break;
        }
    }

    public void mouseReleased(MouseEvent e) {
        if (shapeType > 0) {
            currentShape.setX2(e.getX());
            currentShape.setY2(e.getY());

            shapes.addFront(currentShape);

            currentShape = null;
            shapeType = 0;
            repaint();
        } else {
            if (!painting)
                return;

            ((paintBrushLine) currentBrushLine).setX2(e.getX());
            ((paintBrushLine) currentBrushLine).setY2(e.getY());

            brushLines.addFront(currentBrushLine);
            currentBrushLine = null;
            painting = false;
            repaint();
        }
    }
}
}

LinkedList class: LinkedList类:

import java.util.ArrayList;

class LinkedList<T> {
private int numberOfNodes = 0; 
private ListNode<T> front = null;

// Returns true if the linked list has no nodes, or false otherwise.
public boolean isEmpty() {
    return (front == null);
}

// Deletes all of the nodes in the linked list.
// Note: ListNode objects will be automatically garbage collected by JVM.
public void makeEmpty() {
    front = null;
    numberOfNodes = 0;
}

// Returns the number of nodes in the linked list
public int size() {
    return numberOfNodes;
}

// Adds a node to the front of the linked list.
public void addFront( T element ) {
    front = new ListNode<T>( element, front );
    numberOfNodes++;
}

// Returns a reference to the data in the first node, or null if the list is empty.
public T peek() {
    if (isEmpty()) 
        return null;

    return front.getData();
}

// Removes a node from the front of the linked list (if there is one).
// Returns a reference to the data in the first node, or null if the list is empty.
@SuppressWarnings("unchecked")
public T removeFront() {
    T tempData;

    if (isEmpty()) 
        return null;

    tempData = front.getData();
    front = front.getNext();
    numberOfNodes--;
    return tempData;
}

@SuppressWarnings("unchecked")
public void removeEnd(T element) {
    ListNode<T> node=front;
    while(node.getNext() != null)
    {
        node = node.getNext();
    }
    node.setNext(new ListNode<T>((T)element, null));
}

// Return array filled with T objects
@SuppressWarnings("unchecked")
public ArrayList<T> getArray() {

    ArrayList<T> shapeArray=new ArrayList<T>();

    ListNode<T> node=front;
    while (node!=null)
    {
        shapeArray.add(node.getData());
        node = node.getNext();
    }

    return shapeArray;
}
}

ListNode class: ListNode类:

public class ListNode<T> {
private T data;
private ListNode next;

// Constructor: No reference to next node provided so make it null 
public ListNode( T nodeData ) {
    this( nodeData, null);
}

// Constructor: Set data and reference to next node.
public ListNode( T nodeData, ListNode nodeNext ) {
    data = nodeData;
    next = nodeNext;
}

// Accessor: Return the data for current ListNode object
public T getData() {
    return data;
}

// Accessor: Return reference to next ListNode object
public ListNode getNext() {
    return next;
}

// Mutator: Set new data for current ListNode object
public void setData( T newData ) {
    data = newData;
}


// Mutator: Set new reference to the next node object
public void setNext( ListNode newNext ) {
    next = newNext;
}
}

Both your shapes and your lines only store a start and end point. 形状和线条都只存储起点和终点。

abstract static class Shape {
    private int x1, y1, x2, y2;
    private int thickness;
    private Color shapeColour;

So how does your paint method know what to do? 那么您的绘画方法如何知道呢? How can it draw a curve when it only ever knows two points? 仅知道两个点时如何绘制曲线?

What you need to do: 你需要做什么:

Once you have the shape completed you need to do some math to work out the radius and coordinates of each arc based on the coordinates of the previous and next segment, and then in your paint method you can draw lots and lots of small lines so that it appears a nice smooth arc. 完成形状后,您需要做一些数学运算以根据上一个和下一个线段的坐标计算出每个弧的半径和坐标,然后在绘制方法中您可以绘制很多小线,以便看起来很圆滑。

Also, add some debugging code to find out what values your paint method is using, you may find that the reason you only see dots or small lines is because the values for each shape/line is not being stored correctly. 另外,添加一些调试代码以找出您的paint方法正在使用的值,您可能会发现只看到点或细线的原因是每个形状/线的值没有正确存储。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM