gpt4 book ai didi

java - 如果 swing 在 Jpanel 中调用 validate(),JComponent 会更改框架的位置

转载 作者:行者123 更新时间:2023-12-01 14:11:45 25 4
gpt4 key购买 nike

我有一个 JPanel,其中有可移动的组件。

我的问题是,如果我在面板中使用某个操作,整个面板中组件的位置将在一个框架内将其位置更改为中上,而不是更改回其位置。我调试了它,知道它来自 validate()。如果我手动使用 validate() ,它也会在没有任何操作的情况下发生。

下面是面板中使用的组件的代码:

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;

import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.UIManager;

import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.designtime.DataSourcePlugin;

import com.inform_ac.utils.misc.gui.resizer.ComponentResizer;



/**
* AButton is a JAVA-Swing component with integrated functional buttons.
*/
public class AButton extends JPanel {

/**
* SerialVersionUID: 111111111L
*/
protected static final long serialVersionUID = 111111111L;

/**
* Standard font for displaying text.
*/
protected Font standardFont = new Font("Dialog", Font.PLAIN, 12);

/**
* Defining functional JButtons to delete and edit a AButton.
*/
protected ADeleteButton delBtn = new ADeleteButton(this);
protected JButton editBtn = new JButton();
protected String[] editOptionNames = { "Connect", "Deconnect", "Edit" };
protected JPopupMenu popupmenu = new JPopupMenu();
protected Dimension minDimension = new Dimension(120,120);
protected Point location = new Point();

protected DataFactory dataFactory;
protected DataSourcePlugin dataSourcePlugin;


/**
* Mode: 0 - moving
* 1 - connecting
*/
protected int mode = 0;

/**
* Defining the label for displaying OK or error images.
*/
protected JLabel lblIcon = new JLabel();

protected final JLabel lblInfo1 = new JLabel();
protected final JLabel lblInfo2 = new JLabel();
protected final JLabel lblInfo3 = new JLabel();

public final JButton connectBtn_right = new JButton();
public final JButton connectBtn_left = new JButton();

protected AButton parent;
protected ArrayList<AButton> children = new ArrayList<AButton>();

/**
* Identifier
*/
protected int id = 0;

/**
* Constructor with given start coordinates.
*
* @param X
* - coordinate
* @param Y
* - coordinate
*/
public AButton(int X, int Y, int index) {
this.id = index;
location = new Point(X,Y);
setBounds(location.x, location.y, minDimension.width, minDimension.height);
init();

}

/**
* Private method to initialize main components.
*/
private void init() {
initPnl();
initBtns();
setVisible(true);
setFocusable(true);
}

/**
* Private method to initialize the main panel.
*/
private void initPnl() {
setBackground(UIManager.getColor("InternalFrame.activeTitleBackground"));
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
setPreferredSize(minDimension);
setBorder(UIManager.getBorder("CheckBox.border"));
setFont(standardFont);
setLayout(null);
}

/**
* Private method to initialize functional {@linkplain JButton}.
*/
private void initBtns() {
initEditBtn();
initDelBtn();
initIconPnl();
initConnectorBtns();
}

/**
* Private method to initialize the delete button. Method have to refresh
* the size of this AButton to set the button on the top right corner of
* this AButton.
*/
private void initDelBtn() {
delBtn.setBounds(getWidth() - 18 - 2, 2, 18, 18);
delBtn.setFont(standardFont);
delBtn.setBackground(null);
delBtn.setBorderPainted(false);
delBtn.setBorder(null);
delBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
delBtn.setPreferredSize(new Dimension(16, 16));
delBtn.setMinimumSize(new Dimension(12, 12));
delBtn.setMaximumSize(new Dimension(20, 20));
delBtn.setIcon(new ImageIcon(AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/close.gif")));
add(delBtn);
}

/**
* Private method to initialize the edit button.
*/
private void initEditBtn() {
initPopupmenu();
initMouseListener();
editBtn.setBounds(2,2,21,21);
editBtn.setFont(standardFont);
editBtn.setBorder(null);
editBtn.setBorderPainted(false);
editBtn.setBackground(UIManager
.getColor("InternalFrame.activeTitleGradient"));
editBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
editBtn.setAlignmentX(Component.CENTER_ALIGNMENT);
editBtn.setPreferredSize(new Dimension(21, 21));
editBtn.setMinimumSize(new Dimension(18, 18));
editBtn.setMaximumSize(new Dimension(25, 25));
editBtn.setIcon(new ImageIcon("C:\\Users\\akaradag\\Pictures\\JavaIcon\\icon_bearbeiten.gif"));
add(editBtn);
}

protected void initPopupmenu(){
for(int i = 0; i < editOptionNames.length; i++) {
popupmenu.add(new AbstractAction(editOptionNames[i]) {

private static final long serialVersionUID = 5550466652812249477L;

@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("Connect")) {
if (mode == 1) {
mode = 0;
showConnectors();
} else {
mode = 1;
showConnectors();
}
} else if (e.getActionCommand().equals("Deconnect")) {
resetConnections();
}
else if(e.getActionCommand().equals("Edit")) {

}
}

});
}
}


protected void initMouseListener()
{
editBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
popupmenu.show(e.getComponent(), e.getX(), e.getY());
}
});
}

/**
* Private method to display or not display the connector buttons
*/
public void showConnectors() {
boolean connect = false;
if (mode == 1) {
connect = true;
}
connectBtn_left.setVisible(connect);
connectBtn_right.setVisible(connect);
}

/**
* Private method to initialize the connector buttons
*/
private void initConnectorBtns() {
connectBtn_right.setCursor(Cursor
.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
connectBtn_right.setVisible(false);
connectBtn_right.setPreferredSize(new Dimension(15, 15));
connectBtn_right.setMinimumSize(new Dimension(12, 12));
connectBtn_right.setMaximumSize(new Dimension(15, 15));
connectBtn_right.setFont(new Font("Dialog", Font.PLAIN, 12));
connectBtn_right.setBorderPainted(false);
connectBtn_right.setBorder(null);
connectBtn_right.setBackground(SystemColor.activeCaption);
connectBtn_right
.setBounds(getWidth() - 16, getHeight() / 2 - 5, 15, 15);
add(connectBtn_right);
connectBtn_left.setCursor(Cursor
.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

connectBtn_left.setVisible(false);
connectBtn_left.setPreferredSize(new Dimension(25, 25));
connectBtn_left.setMinimumSize(new Dimension(12, 12));
connectBtn_left.setMaximumSize(new Dimension(15, 15));
connectBtn_left.setFont(new Font("Dialog", Font.PLAIN, 12));
connectBtn_left.setBorderPainted(false);
connectBtn_left.setBorder(null);
connectBtn_left.setBackground(SystemColor.activeCaption);
connectBtn_left.setBounds(2, getHeight() / 2 - 5, 15, 15);
add(connectBtn_left);
}

/**
* Private method to initialize the {@linkplain JLabel} for displaying
* informations.
*/
private void initIconPnl() {
lblIcon.setHorizontalTextPosition(SwingConstants.CENTER);
lblIcon.setHorizontalAlignment(SwingConstants.CENTER);
lblIcon.setAlignmentX(Component.CENTER_ALIGNMENT);
lblIcon.setIcon(new ImageIcon(AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
lblIcon.setBorder(null);
lblIcon.setFont(new Font("Dialog", Font.PLAIN, 12));

lblIcon.setBounds(getWidth() / 4, 3, getWidth() / 2,
getHeight() / 4 + 2);
lblIcon.setLayout(null);
add(lblIcon);

lblInfo1.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo1.setForeground(SystemColor.desktop);
lblInfo1.setBounds(22, getHeight() / 2 - 5, getWidth() - 42,
getHeight() / 8);
add(lblInfo1);

lblInfo2.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo2.setForeground(Color.BLACK);
lblInfo2.setBounds(10, getHeight() / 2 - 5 + getHeight() / 8 + 5,
getWidth() - 20, getHeight() / 8);
add(lblInfo2);

lblInfo3.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo3.setForeground(Color.BLACK);
lblInfo3.setBounds(10, getHeight() / 2 - 5 + 2 * (getHeight() / 8 + 5),
getWidth() - 20, getHeight() / 8);
add(lblInfo3);
}

public String getLblInfo(int index) {
if (index == 1) {
return lblInfo1.getText();
} else if (index == 2) {
return lblInfo2.getText();
} else {
return lblInfo3.getText();
}
}

public void setLblInfo(String text, int index) {
if (index == 1) {
lblInfo1.setText(text);
} else if (index == 2) {
lblInfo2.setText(text);
} else {
lblInfo3.setText(text);
}
}

public Point getLocation() {
return new Point(getX(), getY());
}

public Point getInputLocation() {
return connectBtn_left.getLocation();
}

/**
* Methode um die Location des Objektes zu ändern und dies auch zu repainten.
* Dient dazu damit der Fehler das wenn ein Objekt gelöscht wird die restlichen in die Mitte
* wandern.
*/
public void setLocation(Point p){
location = p;
}

public Point getOutputLocation() {
return connectBtn_right.getLocation();
}

public int getMode() {
return mode;
}

public void setMode(int mode) {
this.mode = mode;
showConnectors();
}

public JButton getDelBtn() {
return delBtn;
}

public int getIndex() {
return id;
}

public void setIndex(int index) {
this.id = index;
}

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
this.setLocation(location.x, location.y);
if (lblInfo1.getText().equals("Join")) {
if (children.size() == 2) {
lblIcon.setIcon(new ImageIcon(
"C:\\Users\\akaradag\\Pictures\\JavaIcon\\ok-icon.png"));
} else if (children.size() > 2) {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/warning.png")));
} else {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
}
} else if (lblInfo1.getText().equals("JDBC")
|| lblInfo1.getText().equals("File")) {
if (parent != null) {
lblIcon.setIcon(new ImageIcon(
"C:\\Users\\akaradag\\Pictures\\JavaIcon\\ok-icon.png"));
} else {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
}
}
}

public void setOutput(AButton out) {
parent = out;
}

public AButton getOutput() {
return parent;
}

public void addInput(AButton input) {
if (!contains(input)) {
this.children.add(input);
}
}

private boolean contains(AButton in) {
for (int i = 0; i < this.children.size(); i++) {
if (this.children.get(i).getIndex() == in.getIndex()) {
return true;
}
}
return false;
}

public ArrayList<AButton> getInput() {
return this.children;
}

public void removeFromInput(AButton remove) {
for (int i = 0; i < this.children.size(); i++) {
if (this.children.get(i) != null) {
if (this.children.get(i).getIndex() == remove.getIndex()) {
this.children.remove(i);
}
}
}
}

public void resetConnections() {
if (parent != null) {
ArrayList<AButton> in = parent.getInput();
for (int i = 0; i < in.size(); i++) {
if (in.get(i).getIndex() == id) {
in.remove(i);
}
}
parent = null;
}

for (int i = 0; i < this.children.size(); i++) {
this.children.get(i).setOutput(null);
this.children.get(i).repaint();
}
this.children = new ArrayList<AButton>();

// if(connectionsDeletedNotify != null)
// connectionsDeletedNotify.actionPerformed(new ActionEvent(this, 0, "Deconnect"));
}

/**
* Setter for the DataFactory
* @param df
*/
public void setDataFactory(DataFactory df)
{
this.dataFactory = df;
}

/**
* Getter for the DataFactory
* @return
*/
public DataFactory getDataFactory()
{
return this.dataFactory;
}

public DataSourcePlugin getDataSourcePlugin() {
return dataSourcePlugin;
}

public void setDataSourcePlugin(DataSourcePlugin dataSourcePlugin) {
this.dataSourcePlugin = dataSourcePlugin;
}
}

这是主面板的代码

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.util.ArrayList;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

import org.pentaho.reporting.designer.core.settings.WorkspaceSettings;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.designtime.DataSourcePlugin;
import org.pentaho.reporting.engine.classic.core.designtime.DefaultDataFactoryChangeRecorder;
import org.pentaho.reporting.engine.classic.core.metadata.DataFactoryMetaData;
import org.pentaho.reporting.engine.classic.core.metadata.DataFactoryRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.inform_ac.reporting.datasource.dataintegrator.frontend.DataintegratorDesignTimeContext;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.AButton;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.AConfirmDialog;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.ADeleteButton;

public class DataintegratorMainPanel extends JPanel {


DataintegratorDesignTimeContext context;
JPopupMenu popmen = new JPopupMenu();

JMenuItem menu1 = new JMenuItem("Add new Datasource:");
JMenuItem menu2 = new JMenuItem("Join");

Dimension dim = new Dimension();
Point holdingPoint , point1, point2;;

/**
* ArrayList für die visuellen Datasources
*/
ArrayList<AButton> abuttonList = new ArrayList<AButton>();

/**
* Nummerierungen der abuttons
*/
int index = 0;

/**
* The <code>Logger</code> of this instance
*/
protected final static Logger LOGGER = LoggerFactory.getLogger(DataintegratorQueryPanel.class);

private static final long serialVersionUID = 4705352682546516889L;

public DataintegratorMainPanel() {
initPopupMenu();
initMouseListener();
}

protected void initMouseListener()
{
this.addMouseListener(new MouseAdapter() {

@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3){
popmen.show(e.getComponent(), e.getX(), e.getY());
}
}
});
}

/**
* Überschrift bei Rechtsklick
* @param header
* @return JComponent
*/
protected JComponent createHeader(String header) {
JLabel label = new JLabel(header);
label.setFont(label.getFont().deriveFont(Font.BOLD,14));
label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

return label;
}

protected void giveMouseListenerTo(AButton button) {
button.addMouseMotionListener(new MouseAdapter() {

@Override
public void mouseDragged(MouseEvent me) {
super.mouseDragged(me);
AButton tmp = (AButton) me.getSource();

//Mode Moving
if(tmp.getMode()==0){

Point point = getMousePosition();

//Über den Rand raus
if(point != null) {
point.x = point.x - holdingPoint.x;
point.y = point.y - holdingPoint.y;

if(point.x >= 0 && point.y >= 0) {
tmp.setLocation(point);
}
//LOGGER.info(""+point);
}
}
else if (tmp.getMode()==1) {
point2 = getMousePosition();
}
repaint();
}
});


button.addMouseListener(new MouseAdapter() {

@Override
public void mousePressed(MouseEvent me) {
super.mousePressed(me);

AButton tmp = (AButton) me.getSource();
if (tmp.getMode() == 0) {
holdingPoint = tmp.getMousePosition();
}

else if (tmp.getMode() == 1) {
Point tmp_point = tmp.getLocation();

point1 = new Point(tmp.connectBtn_right.getLocation().x+tmp_point.x,tmp.connectBtn_right.getLocation().y+tmp_point.y);
LOGGER.info("point1: "+point1);
}
}

@Override
public void mouseReleased(MouseEvent me) {
super.mouseReleased(me);
try {
AButton destination = (AButton) getComponentAt(getMousePosition());
AButton source = (AButton) me.getSource();
if (destination != null && source.getMode() == 1) {

// Hier muss der Baum verkettet werden
if (destination != source) {
destination.addInput(source);

if (source.getOutput() == null) {
source.setOutput(destination);
} else {
AButton tmp = source.getOutput();
tmp.removeFromInput(source);
source.setOutput(destination);
}
source.setMode(0);
destination.setMode(0);
}
}
} catch (ClassCastException e) {
point2 = null;
}
point1 = null;
point2 = null;
repaint();
}
});
repaint();
}

protected void initPopupMenu() {
popmen.add(createHeader("Neue Datasource"));
popmen.addSeparator();

context = new DataintegratorDesignTimeContext(new MasterReport());

final DataFactoryMetaData[] datas = DataFactoryRegistry.getInstance()
.getAll();

for (final DataFactoryMetaData data : datas) {

// Some of the DataFactories are not needed
if (data.isHidden()) {
continue;
} else if (!WorkspaceSettings.getInstance().isShowExpertItems()
&& data.isExpert()) {
continue;
} else if (!WorkspaceSettings.getInstance().isShowDeprecatedItems()
&& data.isDeprecated()) {
continue;
} else if (!WorkspaceSettings.getInstance()
.isExperimentalFeaturesVisible() && data.isExperimental()) {
continue;
} else if (!data.isEditorAvailable()) {
continue;
}

if (data.getDisplayName(getLocale()).equals("Dataintegrator")) {
continue;
}

JMenuItem item = new JMenuItem(new AbstractAction(
data.getDisplayName(getLocale())) {

private static final long serialVersionUID = 7700562297221703939L;

@Override
public void actionPerformed(ActionEvent arg0) {
Point mousePos = getMousePosition();
final AButton tmp = new AButton(mousePos.x,mousePos.y,index++);

// Action listener hier und nicht in die for schleife
tmp.getDelBtn().addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
ADeleteButton tmp_1 = (ADeleteButton) arg0
.getSource();
AButton source = tmp_1.getAbutton();
AConfirmDialog dialog = new AConfirmDialog();

int opt = dialog.getState();
if (opt == 1) {
for (int i = 0; i < abuttonList.size(); i++) {
if (source.getIndex() != abuttonList.get(i)
.getIndex()) {

if (abuttonList.get(i).getOutput() != null
&& abuttonList.get(i).getOutput()
.getIndex() == source
.getIndex()) {
abuttonList.get(i).setOutput(null);
}
abuttonList.get(i).removeFromInput(source);
}
}

// seperat erst die referenzen löschen dann das
// objekt aus der liste
for (int i = 0; i < abuttonList.size(); i++) {
if (source.getIndex() == abuttonList.get(i)
.getIndex()) {
abuttonList.remove(i);
source.setVisible(false);
source.setEnabled(false);
}
}
}
}
});

LOGGER.info("--> data: " + data);
final DataSourcePlugin editor = data.createEditor();
LOGGER.info("--> editor: " + editor);
final DefaultDataFactoryChangeRecorder recorder = new DefaultDataFactoryChangeRecorder();

final DataFactory dataFactory = editor.performEdit(context,
null, null, recorder);
LOGGER.info("--> datafactory: " + dataFactory);

// Falls die Datafactory initialisiert werden konnte
if (dataFactory != null) {
tmp.setDataSourcePlugin(editor);
tmp.setDataFactory(dataFactory);

add(tmp);

//LOGGER.info("New datafact"+mousePos);

abuttonList.add(tmp);

//Moving listener
giveMouseListenerTo(tmp);
validate();
}


}
});
popmen.add(item);
}
}

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

Graphics2D g2d = (Graphics2D) g;

g2d.setStroke(new BasicStroke(1.5f));


g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

Line2D tmp_line;
for (int i = 0; i < abuttonList.size(); i++) {
if (abuttonList.get(i).getOutput() != null) {
g2d.setStroke(new BasicStroke(1.5f));
int x1 = abuttonList.get(i).getLocation().x
+ abuttonList.get(i).connectBtn_right.getLocation().x;
int y1 = abuttonList.get(i).getLocation().y
+ abuttonList.get(i).connectBtn_right.getLocation().y;
int x2 = abuttonList.get(i).getOutput().getLocation().x
+ abuttonList.get(i).getOutput().connectBtn_left
.getLocation().x - 4;
int y2 = abuttonList.get(i).getOutput().getLocation().y
+ abuttonList.get(i).getOutput().connectBtn_left
.getLocation().y;

tmp_line = new Line2D.Double(x1, y1, x2, y2);
double m = (y2 - y1) / (double) (x2 - x1);
//logger.info("m: "+m);

int vz = x2-x1;
//LOGGER.info("vz: "+vz);

if(vz<0)
{
vz = 20;
}
else if (vz>0){
vz = -20;
}

if(m<4 && m>-4) {

Point p = new Point(x2 -20, (int) (y1 + m * (x2 - x1 - 20)));
//logger.info(p.toString());

Line2D tmp2_line = new Line2D.Double(x2, y2, x2 + vz,
p.y + 3);
//logger.info("x2: "+x2+", y2: "+y2+", p.y:"+p.y);

Line2D tmp3_line = new Line2D.Double(x2, y2, x2 + vz,
p.y - 3);
//logger.info("x2: "+x2+", y2: "+y2+", p.y:"+p.y);

g2d.setPaint(Color.BLACK);
g2d.draw(tmp2_line);
g2d.draw(tmp3_line);
}
g2d.draw(tmp_line);

}
}
if (point1 != null && point2 != null) {
Line2D line2d = new Line2D.Double(point1, point2);
g2d.setPaint(Color.RED);
g2d.setStroke(new BasicStroke(1.5f));// set stroke size
g2d.draw(line2d);
}
}
}

因此,如果我单击 editBtn 并单击“连接”,就会发生错误。它重新绘制整个面板,并且对于某些帧,面板中的按钮垂直位于面板的中间顶部,并且它返回到对象本身中保存的位置。

我还可以使用 validate() 重现错误。

我不明白这里有什么问题。为什么验证后会更改主面板中组件的位置?

抱歉,如果代码不是 SSCCE,但如果没有 pentaho 库,我就无法让它工作......

最佳答案

好的,我修好了。

问题出在 JPanel 的默认布局管理器上。

将其更改为 setLayout(null); 后,问题就消失了。

关于java - 如果 swing 在 Jpanel 中调用 validate(),JComponent 会更改框架的位置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18486262/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com