LINUX.ORG.RU

Дошлифовать Swing GUI desktop application

 , ,


0

2

Хотел написать для Андроида, но решил протестировать и доделать рабочий вариант для компа. Первый блин на Java, выбор пал на Swing поскольку инет завален ссылками по нему, а с JavaFX связываться не решился. Собственно, от интерфейса требуется минимализм, надежность и кроссплатформенность. Это РЕПЛ (рид-эвал-принт-лууп) одного языка, в нижнем окне набираются команды, в верхнем отображается результат (как он будет готов). Здесь только UI оболочка, парсинг и вычисление вынес в отдельные классы, здесь стоят заглушки. Собственно, хотел узнать - это так вообще делается? Прямо класс Main наследуется от JFrame и вся UI логика прописывается в нем? Или выделять отдельный класс для этого? Как сделать изменение мышкой относительных размеров окон ввода и вывода (сейчас они у меня в таблице, высота одинакова)? Хорошо ли из отдельного потока выполнения изменять элементы UI перед завершением потока? Если лучше делать это в основном потоке, то как организовать красивый надежный коллбэк по завершению потока вычислений? Ну и вообще, любая конструктивная критика/рекомендации приветствуются.

package com.company;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;

public class Main extends JFrame {

    private static JTextArea textArea;
    private static JEditorPane textAreaIn;
    private static DefaultListModel<InterThread> threadListModel =
            new DefaultListModel<InterThread> ();
    private static JList threadList = new JList<InterThread> (threadListModel);
    private static JToolBar buttonsPanel;

    public Main() {
        super("GUI REPL");

        textArea = new JTextArea(5, 30);
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        textArea.setFont(new Font("Courier New", Font.PLAIN, 12));

        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setPreferredSize(new Dimension(500, 300));
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        final StyleContext sctextAreaIn = new StyleContext();
        final DefaultStyledDocument doctextAreaIn = new DefaultStyledDocument(sctextAreaIn);
        textAreaIn = new JTextPane(doctextAreaIn);
        final Style style0 = sctextAreaIn.addStyle("emptyStyle", null);
        style0.addAttribute(StyleConstants.FontSize, 12);
        style0.addAttribute(StyleConstants.FontFamily, "Courier New");
        doctextAreaIn.setParagraphAttributes(0, 1, style0, true);

        JScrollPane scrollPaneIn = new JScrollPane(textAreaIn);
        scrollPaneIn.setPreferredSize(new Dimension(500, 300));
        scrollPaneIn.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        threadList.setCellRenderer(new DefaultListCellRenderer() {
            @Override
            public Component getListCellRendererComponent(
                    JList<?> list, Object value, int index, boolean isSelected,
                    boolean cellHasFocus) {
                Component renderer = super.getListCellRendererComponent(
                        list, value, index, isSelected, cellHasFocus);
                if (renderer instanceof JLabel && value instanceof Thread) {
                    ((JLabel) renderer).setText(((Thread) value).getName());
                }
                return renderer;
            }
        });
        threadList.setFont(new Font("Courier New", Font.BOLD, 20));

        JScrollPane scrollPaneThreads = new JScrollPane(threadList);
        scrollPaneThreads.setPreferredSize(new Dimension(150, 300));
        scrollPaneThreads.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);


        JLabel lastLoadFileNameLabel = new JLabel();

        Action sendUserInputAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sendUserInput();
            }
        };
        JButton sendUserInput = new JButton();
        sendUserInput.setAction(sendUserInputAction);
        sendUserInput.setText("send (CTRL+ENTER)");
        sendUserInput.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_MASK)
                        , "sendUserInputAction");
        sendUserInput.getActionMap().put("sendUserInputAction", sendUserInputAction);


        Action loadFileAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JFileChooser fileopen = new JFileChooser();
                fileopen.setFileFilter(new FileNameExtensionFilter("TXT files", "txt"));
                int ret = fileopen.showDialog(getParent(), "Выберите файл скрипта");
                if (ret == JFileChooser.APPROVE_OPTION) {
                    File file = fileopen.getSelectedFile();
                    try {
                        String s = new Scanner(file).useDelimiter("\\Z").next();
                        lastLoadFileNameLabel.setText(file.getAbsolutePath());
                        startNewThread(false, s);
                    } catch (IOException ex) {
                        cout(true, "problem accessing file " + file.getAbsolutePath());
                    }
                }
            }
        };
        JButton loadFile = new JButton();
        loadFile.setAction(loadFileAction);
        loadFile.setText("load file");

        Action reloadFileAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String fileAbsolutePath = lastLoadFileNameLabel.getText();
                try {
                    File file = new File(fileAbsolutePath);
                    String s = new Scanner(file).useDelimiter("\\Z").next();
                    startNewThread(false, s);
                } catch (IOException ex) {
                    cout(true, ex.getLocalizedMessage());
                }
            }
        };
        JButton reloadFile = new JButton();
        reloadFile.setAction(reloadFileAction);
        reloadFile.setText("reload file");

        Action showActiveThreadsAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
                ThreadGroup parent;
                while ((parent = threadGroup.getParent()) != null) {
                    threadGroup = parent;
                    Thread[] threadList = new Thread[threadGroup.activeCount()];
                    threadGroup.enumerate(threadList);
                    for (Thread thread : threadList)
                        cout(true, thread.getThreadGroup().getName()
                                + " " + thread.getPriority()
                                + " " + thread.getName());
                }
            }
        };
        JButton showActiveThreads = new JButton();
        showActiveThreads.setAction(showActiveThreadsAction);
        showActiveThreads.setText("active threads");


        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(scrollPaneThreads, BorderLayout.EAST);

        JPanel textsPanel = new JPanel(new GridLayout(2,1));
        textsPanel.add(scrollPane);
        textsPanel.add(scrollPaneIn);
        getContentPane().add(textsPanel, BorderLayout.CENTER);

        sendUserInput.setDefaultCapable(true);

        buttonsPanel = new JToolBar(SwingConstants.VERTICAL);
        buttonsPanel.add(sendUserInput);
        buttonsPanel.addSeparator();
        buttonsPanel.add(loadFile);
        buttonsPanel.add(reloadFile);
        buttonsPanel.add(showActiveThreads);
        getContentPane().add(buttonsPanel, BorderLayout.WEST);
        getContentPane().add(lastLoadFileNameLabel, BorderLayout.SOUTH);

        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public class Read {
        public Object read(String s) { return "(" + s + ")"; }
    }

    public class Eval {
        Eval() {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                //e.printStackTrace();
            }
        }
        public Object eval(Object o) { return "evaluated: " + o.toString(); }
    }

    public class InterThread extends Thread {
        public String expression;
        public boolean showEcho;

        InterThread(boolean _showEcho, String _exp) { showEcho = _showEcho; expression = _exp; }

        public void run() {
            try {
                Object ro = new Read().read(expression);
                if (showEcho) cout(true, ro.toString());
                cout(true, new Eval().eval(ro).toString());
            } catch (Throwable e) {
                cout(true, e.toString());
                Thread.currentThread().interrupt();
            }
            textAreaIn.setText("");
            if (threadListModel.contains(this))
                threadListModel.remove(threadListModel.indexOf(this));

            for (Component c : buttonsPanel.getComponents()) {
                if (c.getClass().getSimpleName().equals("InterThreadJButton")) {
                    if (((InterThreadJButton) c).thread.equals(this))
                        buttonsPanel.remove(c);
                }
            }
            buttonsPanel.updateUI(); // .revalidate();
        }
    }

    class InterThreadJButton extends JButton {
        public InterThread thread;

        InterThreadJButton(InterThread t) {
            thread = t;

            Action interruptAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    cout(true, "interrupt");
                    thread.interrupt();
                    cout(true, thread.getName());
                }
            };
            this.setAction(interruptAction);
            this.setText(thread.getName());
        }
    }

    public void startNewThread(boolean showEcho, String exp) {
        if (exp == null || exp.trim().isEmpty()) return;

        InterThread it = new InterThread(showEcho, exp);
        it.start();
        threadListModel.addElement(it);
        buttonsPanel.add(new InterThreadJButton(it));
    }

    public static void cout(boolean ln, String s) {
        if (s == null) return;
        if (ln) textArea.append(s + "\n"); else textArea.append(s);
        textArea.setCaretPosition(textArea.getDocument().getLength());
    }

    public void sendUserInput() {
        startNewThread(true, textAreaIn.getText());
    }

    //--------------------------------- MAIN -----------------------------

    public void run() throws FileNotFoundException {
        cout(true, "Lets begin");
    }

    public static void main(String[] args) throws FileNotFoundException {
        Main application = new Main();
        application.setVisible(true);
        application.pack();
        application.run();
    }
}

советую сделать веб-интерфейс на java + spring boot =)

stevejobs ★★★★☆ ()

а с JavaFX связываться не решился.

Зря. Если пишешь с нуля и не смущает ограничение на >=8u40, то пиши на JavaFX.

https://docs.oracle.com/javase/8/javase-clienttechnologies.htm

В твоём случае весь гуй будет нарисован в fxml и один листнер в контроллере на поле ввода.

aidan ★★★★ ()
Ответ на: комментарий от aidan

stevejobs, я думал про вэб-интерфейс, но не знаю какие технологии позволяют вызывать Java-код, там же Javascript нативный язык браузеров вроде. Надо будет посмотреть.

aidan спасибо, думаю что и на JavaFX попробую нарисовать гуй. Вообще в идеале сделать все 3 рабочих варианта, чтобы был выбор ) Хотя та же IntelliJ IDEA на Swing вроде написана, и смотрится ничего так, если привыкнуть ))

ЗЫ сейчас решаю сверхзадачу - форматирование кода в окне ввода, хотя бы подсветку парных скобок от курсора, а если еще будет выделение ключевых слов, то совсем хорошо. Я вот смотрю как в IDEA это работает, и кажется мне, что анализ текста кода висит на отдельном потоке, потому что реакция форматирования подтормаживает, но курсор не замерзает. Я повесил подсветку скобок на листенер курсора, но тормозит если на стрелку нажать и держать.

Ivana ()
Ответ на: комментарий от Ivana

ЗЫ сейчас решаю сверхзадачу - форматирование кода в окне ввода, хотя бы подсветку парных скобок от курсора, а если еще будет выделение ключевых слов, то совсем хорошо. Я вот смотрю как в IDEA это работает, и кажется мне, что анализ текста кода висит на отдельном потоке, потому что реакция форматирования подтормаживает, но курсор не замерзает. Я повесил подсветку скобок на листенер курсора, но тормозит если на стрелку нажать и держать.

Если достаточно просто подсветки слов и скобок, то из-коробки оно делается этим: https://github.com/TomasMikula/RichTextFX#automatic-highlighting-of-java-keyw...

aidan ★★★★ ()

Да, лучше бы сразу начал с JavaFX.
Здесь можно почитать как запилить для десктопа и мобилки (и даже для мобильной гейОси): http://gluonhq.com/open-source/javafxports/

ii8_ ★★★★ ()
Ответ на: комментарий от Ivana

какие технологии позволяют вызывать Java-код

Вебсокеты позволяют гонять данные в обоих направлениях. Вызывать методы напрямую ненужно, нужно просто передать ввод от пользователя на бэкенд (в джава приложение), а обработанные данные во фронтенд.

Но я лично за JavaFX.

Weres ★★★ ()
Ответ на: комментарий от ii8_

Спасибо за ссылки и за единодушие в рекомендациях ) Создаю новый проект JavaFX )

Ivana ()
Ответ на: комментарий от Weres

нужно просто передать ввод от пользователя на бэкенд (в джава приложение), а обработанные данные во фронтенд.

А, ну тогда вроде можно на чем угодно бэкенд написать, и возвращать html. Я думал речь о каких-нибудь апплетах или как это называется, чтобы все на клиенте жило без сервера вообще. Но это отдельная браузерная область, в которую я еще не окунался.

Ivana ()
Ответ на: комментарий от Ivana

:)
Удачи! И постарайся не создавать всё приложение в одном классе. Разбей на модули, продумай взаимосвязи между ними. Мало ли, вдруг твой проект разрастётся во что-то большее.

ii8_ ★★★★ ()
Ответ на: комментарий от ii8_

До удачи еще... ) Если со Swing я разобрался с лэйаутами, сам руками в коде быстренько накидал элементов, задал их параметры и экшены-листенеры, даже не пользуясь гуй-дизайнером, то тут наверное придется Scene Builder скачать и прикрутить к Идее.

А модули... В Swing у меня сначала было все в одном классе, потом вынес пару-тройку классов в отдельные файлы (парсинг, вычисление, преобразование в строку), но сам гуй остался в мэйне - пример в стартовом посте. И как его оттуда выдрать, пока не представляю. Да и надо ли... Я полагаю, что каждый вариант приложения будет в своем мэйне описывать свой гуй, а эти внешние файлы-классы логики просто буду копировать из проекта в проект, они везде будут одинаковыми. Вот такая мне видится компромиссная модульность.

Ivana ()
Ответ на: комментарий от Ivana

то тут наверное придется Scene Builder

Он позволяет вынести визуальную модель в отдельный файл, проверить его и удобно изменять. Но это не обязательно. Ты всегда можешь в коде создать нужный тебе интерфейс. Свинг простой по сути, с нюансами, но тем не менее.

ii8_ ★★★★ ()
Ответ на: комментарий от ii8_

Swing-альфа-версия интерпретатора готова, если у кого будет интерес, вышлю архив с исполняемым файлом и демо-примерами. Или скажите куда можно выложить для общего доступа (70 Кб весь архив).

Ivana ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.