如何在 Swing 中制作一个适当的 Table 以及使用适当的布局将所有元素放在各自的位置?

How to make a proper a Table in Swing as well as put all the elements in their respective positions using a proper layout?

我正在研究网络爬虫并分几个阶段实施它。

此阶段获取网站上提到的所有链接,并在 JTable 中显示带有标题的网页的有效链接。

但是,我希望 table 在我在要解析的 TextField 中输入 URL 之前已经存在(这是此分配的要求)。

此外,有没有办法修复当前的布局(第一张 Macintosh 的图片 window) Current Swing Window 这样我就可以在指定位置正确显示所有JFrame元素,如下图所示:

Desired Swing Window where all elements are

我曾尝试使用空布局来修复布局问题,但它只会让任务变成一场噩梦,而且 table 会在我眼前消失。

在我开始输入链接之前,我找不到让 table 存在的方法。

package crawler;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class WebCrawler extends JFrame {
    public WebCrawler() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Web Crawler");
        setLocationRelativeTo(null);
        setSize(600, 400);

        JTextField urlText = new JTextField();
        urlText.setName("UrlTextField");
//        urlText.setBounds(10,10,300,20);
        urlText.setSize(300,20);

        JLabel title = new JLabel();
        JLabel titleLabel = new JLabel();
        title.setFont(new Font("Helvetica Nueve", Font.BOLD, 12));
        title.setLocation(10,30);
        title.setName("TitleLabel");

        titleLabel.setFont(new Font("Helvetica Nueve", Font.BOLD, 12));
        titleLabel.setText("Title:\t");

        JButton extract = new JButton();
        extract.setText("Parse");
        extract.setName("RunButton");
        extract.setSize(40,20);
        var LINE_SEPARATOR = System.getProperty("line.separator");
        extract.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    String url = urlText.getText()/* Get url from JTextField */;
                    url = url.replaceAll("^\"+ \"+$", "");

                    final InputStream inputStream = new URL(url).openStream();
                    final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                    final StringBuilder stringBuilder = new StringBuilder();

                    String nextLine;
                    while ((nextLine = reader.readLine()) != null) {
                        stringBuilder.append(nextLine);
                        stringBuilder.append(LINE_SEPARATOR);
                    }


                    final String siteText = stringBuilder.toString();

                    Pattern pattern = Pattern.compile("<title>(.+?)</title>", Pattern.CASE_INSENSITIVE);
                    Matcher matcher = pattern.matcher(siteText);

                    String titleString = matcher.find()?matcher.group(1):"null";
                    title.setText(titleString);

                    SortedMap<String,String> links = new TreeMap<>();

                    Pattern pTag = Pattern.compile("(?i)<a([^>]+)>(.+?)</a>",Pattern.CASE_INSENSITIVE);
                    Pattern pLink = Pattern.compile("\s*(?i)href\s*=\s*(\"([^\"]*\")|'[^']*'|([^'\">\s]+))",Pattern.CASE_INSENSITIVE);

                    Matcher mTag = pTag.matcher(siteText);


                    try {
                        while (mTag.find()) {

                            String href = mTag.group(1);     // get the values of href
                            String linkElem = mTag.group(2); // get the text of link Html Element

                            Matcher mLink = pLink.matcher(href);

                            while (mLink.find()) {

                                String link = mLink.group(1);

                                link = link.substring(1,link.length()-1);

                                pattern = Pattern.compile("http",Pattern.CASE_INSENSITIVE);
                                matcher = pattern.matcher(link);
                                if(!matcher.find())
                                    link = String.join("",url,link);

                                try {
                                    URL urlValidator = new URL(link);

                                    if(urlValidator.getContent().equals("text/html"));
                                        links.put(link, linkElem);
                                } catch(Exception ex){
                                    System.out.println("Exception encountered at " + link );
                                }

                            }

                        }

                        JTable table = new JTable(toTableModel(links));
                        table.setName("TitlesTable");
                        JScrollPane scrollPane = new JScrollPane(table);
                        table.disable();
                        add(scrollPane, BorderLayout.CENTER);
                        table.clearSelection();
                    }
                    catch (Exception ex){
                        System.out.println(ex.getMessage());
                    }
                }
                catch(Exception ex){ System.out.println(ex.getMessage());}
            }
        });
        add(urlText, BorderLayout.PAGE_START);
        add(extract, BorderLayout.PAGE_END);
        add(titleLabel, BorderLayout.WEST);
        add(title, BorderLayout.LINE_START);
        //TODO: change design and layout

        setVisible(true);
    }

    public TableModel toTableModel(Map<?,?>map){
        DefaultTableModel model = new DefaultTableModel (
                new Object[] { "URL", "Title" }, 0
        );

        for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry)it.next();
            model.addRow(new Object[] { entry.getKey(), entry.getValue() });
        }

        return model;
    }
}

当我 运行 作业测试工具给出的测试时,它显示 "No suitable table element found"。

发生这种情况是因为它需要在将测试用例URL输入文本字段进行解析之前检测活动table。

I want the table to be already present before I enter the URL

然后您需要在创建所有其他组件并将其添加到框架的同时创建一个空 table 并将其添加到框架。

您需要为 JTable 创建实例变量才能修改其模型。

将 table 添加到框架的代码类似于:

add(title, BorderLayout.LINE_START);
DefaultTableModel model = new DefaultTableModel (new Object[] { "URL", "Title" }, 0);
table = new JTable( model );
add(new JScrollPane(table), BorderLayout.CENTER);

现在您将在启动应用程序时看到一个空的 table,其中只有列。

您的 toTableModel(...) 方法中的代码现在变成如下内容:

//DefaultTableModel model = new DefaultTableModel (new Object[] { "URL", "Title" }, 0);
DefaultTableModel model = (DefaultTableModel)table.getModel();
model.setRowCount(0);

这只是删除了模型中的所有行,因此您的循环代码可以添加每一行新数据。

当然,您还可以删除当前创建 JTable 和 JScrollPane 的 ActionListener 中的代码,因为不再需要它。

Also, is there a way to fix the current layout

您的嵌套面板具有不同的布局管理器以达到您想要的效果。

例如,您希望 table 能够 grow/shrink,具体取决于 table 中的数据,因此应将其添加到 CENTER BorderLayout.

但是,最上面的两行是固定的,应该将它们添加到子面板中。然后此面板将添加到 BorderLayout. Maybe a verticalBoxLayout` 的 PAGE_START

那么每一行都是另一个子面板。第一行可以是带有水平 BoxLayout 的面板,第二行可以是带有 FlowLayout 的面板。

阅读 Layout Manager 上的 Swing 教程,然后使用适当的布局管理器对子面板上的组件进行逻辑分组。

不要在 JButtonActionPerformed 事件。但是,如果您愿意,可以从该事件重新启用 JTable,并将链接应用到 JTable 的模型。

如果可以,请在单独的 class 中创建您的表单。这清理了很多东西,因为它使表单代码杂乱无章,远离手头的主要任务……页面抓取链接。表单 class 可能如下所示:

package webcrawler;

public class Form extends javax.swing.JFrame {

private static final long serialVersionUID = 101101L;

// Member Variables for Form Components 
public javax.swing.JTextField urlText;
public javax.swing.JButton extract;
public javax.swing.JLabel title;
public javax.swing.JTable table;

private javax.swing.JLabel jLabel1;
private javax.swing.JPanel jPanel1;
private javax.swing.JPanel jPanel2;
private javax.swing.JScrollPane scrollPane;
private javax.swing.JLabel titleLabel;

public Form() {
    initializeFormComponents();
    setLookAndFeel();
    openForm();
}

private void setLookAndFeel() {
    /* Set the Nimbus look and feel if you have it 
       or even like it. Use whatever you like   */
    try {
        for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
            if ("Nimbus".equals(info.getName())) {
                javax.swing.UIManager.setLookAndFeel(info.getClassName());
                break;
            }
        }
    }
    catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
        System.out.println(ex.getMessage());
    }
}

private void openForm() {
    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            setVisible(true);
            setLocationRelativeTo(null);
        }
    });
}

private void initializeFormComponents() {
    jPanel1 = new javax.swing.JPanel();
    jLabel1 = new javax.swing.JLabel();
    urlText = new javax.swing.JTextField();
    extract = new javax.swing.JButton();
    jPanel2 = new javax.swing.JPanel();
    scrollPane = new javax.swing.JScrollPane();
    table = new javax.swing.JTable();
    titleLabel = new javax.swing.JLabel();
    title = new javax.swing.JLabel();

    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
    setTitle("Web Crawler");
    setAlwaysOnTop(true);

    jLabel1.setFont(new java.awt.Font("sansserif", 1, 12)); // NOI18N
    jLabel1.setText("URL:");
    urlText.setName("UrlTextField"); // NOI18N
    extract.setText("Parse");
    extract.setName("RunButton"); // NOI18N

    javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
    jPanel1.setLayout(jPanel1Layout);
    jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel1Layout.createSequentialGroup()
                            .addContainerGap()
                            .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                            .addComponent(urlText, javax.swing.GroupLayout.PREFERRED_SIZE, 667, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                            .addComponent(extract, javax.swing.GroupLayout.DEFAULT_SIZE, 84, Short.MAX_VALUE)
                            .addContainerGap())
    );
    jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel1Layout.createSequentialGroup()
                            .addContainerGap()
                            .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                    .addComponent(jLabel1)
                                    .addComponent(urlText, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                    .addComponent(extract))
                            .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
    );

    table.setModel(new javax.swing.table.DefaultTableModel(
            new Object[][]{
                {null, null},
                {null, null},
                {null, null},
                {null, null}
            },
            new String[]{
                "URL", "TITLE"
            }
    ) {
        private static final long serialVersionUID = 101110L;
        Class[] types = new Class[]{
            java.lang.String.class, java.lang.String.class
        };
        boolean[] canEdit = new boolean[]{
            false, false
        };

        @Override
        public Class getColumnClass(int columnIndex) {
            return types[columnIndex];
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return canEdit[columnIndex];
        }
    });
    table.setName("TitlesTable");
    table.setEnabled(false);
    table.setGridColor(new java.awt.Color(198,195,195));
    table.setInheritsPopupMenu(true);
    table.setRowHeight(20);
    table.setShowHorizontalLines(true);
    table.setShowVerticalLines(true);
    scrollPane.setViewportView(table);

    titleLabel.setFont(new java.awt.Font("sansserif", 1, 12));
    titleLabel.setText("Title:");

    title.setFont(new java.awt.Font("sansserif", 1, 12));
    title.setText("None");
    title.setName("TitleLabel");

    javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
    jPanel2.setLayout(jPanel2Layout);
    jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel2Layout.createSequentialGroup()
                            .addContainerGap()
                            .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                    .addComponent(scrollPane)
                                    .addGroup(jPanel2Layout.createSequentialGroup()
                                            .addComponent(titleLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 27, javax.swing.GroupLayout.PREFERRED_SIZE)
                                            .addGap(18, 18, 18)
                                            .addComponent(title, javax.swing.GroupLayout.PREFERRED_SIZE, 348, javax.swing.GroupLayout.PREFERRED_SIZE)
                                            .addGap(0, 0, Short.MAX_VALUE)))
                            .addContainerGap())
    );
    jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel2Layout.createSequentialGroup()
                            .addContainerGap()
                            .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                    .addComponent(titleLabel)
                                    .addComponent(title))
                            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                            .addComponent(scrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 478, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addContainerGap())
    );

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jPanel2, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
    );
    layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                            .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                            .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
    );
        pack();
    }
}

然后您的 WebCrawler Class 可能看起来像这样:

package webcrawler;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;


public class WebCrawler {

private static final long serialVersionUID = 101011L;
private Form form;
String LINE_SEPARATOR = System.getProperty("line.separator");

public static void main(String[] args) {
    // To eliminate the need for statics. I Don't want it right now. 
    new WebCrawler().startApp(args);
}

private void startApp(String[] args) {
    form = new Form();
    form.extract.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                clearJTable();  // Clear the JTable
                String url = form.urlText.getText();
                /* Get url from JTextField */
                url = url.replaceAll("^\"+ \"+$", "");

                final InputStream inputStream = new URL(url).openStream();
                final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                final StringBuilder stringBuilder = new StringBuilder();

                String nextLine;
                while ((nextLine = reader.readLine()) != null) {
                    stringBuilder.append(nextLine);
                    stringBuilder.append(LINE_SEPARATOR);
                }

                final String siteText = stringBuilder.toString();

                Pattern pattern = Pattern.compile("<title>(.+?)</title>", Pattern.CASE_INSENSITIVE);
                Matcher matcher = pattern.matcher(siteText);

                String titleString = matcher.find() ? matcher.group(1) : "null";
                form.title.setText(titleString);

                SortedMap<String, String> links = new TreeMap<>();

                Pattern pTag = Pattern.compile("(?i)<a([^>]+)>(.+?)</a>", Pattern.CASE_INSENSITIVE);
                Pattern pLink = Pattern.compile("\s*(?i)href\s*=\s*(\"([^\"]*\")|'[^']*'|([^'\">\s]+))", Pattern.CASE_INSENSITIVE);

                Matcher mTag = pTag.matcher(siteText);

                try {
                    while (mTag.find()) {

                        String href = mTag.group(1);     // get the values of href
                        String linkElem = mTag.group(2); // get the text of link Html Element

                        Matcher mLink = pLink.matcher(href);

                        while (mLink.find()) {
                            String link = mLink.group(1);
                            link = link.substring(1, link.length() - 1);
                            pattern = Pattern.compile("http", Pattern.CASE_INSENSITIVE);
                            matcher = pattern.matcher(link);
                            if (!matcher.find()) {
                                link = String.join("", url, link);
                            }
                            try {
                                //URL urlValidator = new URL(link);
                                //if (urlValidator.getContent().equals("text/html")) {
                                links.put(link, linkElem);
                                //}
                            }
                            catch (Exception ex) {
                                System.out.println(ex);
                                //System.out.println("Exception encountered at " + link);
                            }
                        }
                    }
                    form.table.setEnabled(true);    // Enable JTable
                    form.table.setModel(toTableModel(links));
                }
                catch (Exception ex) {
                    System.out.println(ex);
                }
            }
            catch (Exception ex) {
                System.out.println(ex);
            }
            // Is there anything in the table?
            if (form.table.getModel().getRowCount() == 0) {
                // NOPE...So Disable JTable.
                form.table.setEnabled(true);
            }
        }
    });
    form.urlText.requestFocus();
}

private void clearJTable() {
    DefaultTableModel tableModel = (DefaultTableModel) form.table.getModel();
    while (tableModel.getRowCount() > 0) {
        for (int i = 0; i < tableModel.getRowCount(); i++) {
            tableModel.removeRow(i);
        }
    }
    // Reset 4 blank rows in the table for looks :) 
    form.table.setModel(new DefaultTableModel(
            new Object[][]{{null, null}, {null, null}, {null, null},{null, null}},
            new String[]{"<html><font color=blue><b>U R L</b></font></html>", 
                         "<html><font color=red><b>T I T L E</b></font></html>"}));
    // Yes...you can use basic HTML in a JTable Header.
}

public TableModel toTableModel(Map<?, ?> map) {
    DefaultTableModel model = new DefaultTableModel(
            new Object[]{"URL", "TITLE"}, 0
    );
    for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
        Map.Entry entry = (Map.Entry) it.next();
        model.addRow(new Object[]{entry.getKey(), entry.getValue()});
    }
    return model;
}

}