Java+SwingでGUIアプリケーションを作ってみる

JavaとSwingを使い、JREJava Runtime Environment)をインストールしたコンピュータで実行可能なGUIアプリケーションを作ってみる。

環境

Windows 8.1 Pro 64 bit版

>systeminfo
OS 名:                  Microsoft Windows 8.1 Pro
OS バージョン:          6.3.9600 N/A ビルド 9600
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

JDK 8のインストール

まず、JDKのダウンロードページからWindows x64用のインストーラをダウンロードし、JDK 8をインストールする。 インストール後、PATH環境変数C:\Program Files\Java\jdk1.8.0_91\binを追加しておく。 コマンドプロンプトを起動し、次のコマンドが使えることを確認する。

>javac -version
javac 1.8.0_91

Eclipseのインストール

次に、EclipseのダウンロードページからWindows 64 bit用のzipファイルをダウンロードし、適当なディレクトリに展開する。 続けて、eclipse.exeを起動するとコード一式を置くパス(workspace)の設定を求められるので、適当なディレクトリを設定し、「Use this as default and do not ask again」にチェックを入れてOKを押す。 Tutorialsの「Create a Hello World application」を選択し、簡単にEclipseの使い方を確認しておくとよい。

SwingでGUIアプリケーションを作ってみる

ここでは、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。

まず、ツールバーの「New」→「Java Project」から「SHA1Calculator」プロジェクトを作成し、続けて「Create new visual classes」から「Swing」→「JFrame」を選択する。 クラス名を「SHA1Calculator」にしてOKを押すとファイルの雛形が作成されるので、続けて「Run」を押し、空のウィンドウが表示されることを確認する。

雛形を編集し、実際にコードを書いてみると次のようになる。 javax.swing.JTableなど、使用するクラスを新たにimportする必要があることに注意。 なお、EclipseではCtrl+Spaceで補完リストの表示、Ctrl+Shift+Fでコードの自動整形、Ctrl+Shift+Oで不要なimport文の削除を行うことができる。

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;

public class SHA1Calculator extends JFrame {

    private JPanel contentPane;
    private DefaultTableModel model;

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    SHA1Calculator frame = new SHA1Calculator();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public SHA1Calculator() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 600, 300);  // expanded default frame size
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        setTitle("SHA-1 Calculator");

        // create scrollable JTable
        model = new DefaultTableModel(new String[]{"File Path", "sha1sum"}, 0);
        JTable table = new JTable(model);
        JScrollPane scrollPane = new JScrollPane(table);
        contentPane.add(scrollPane);

        // enable file drop
        contentPane.setTransferHandler(new DropFileHandler());
    }

    private class DropFileHandler extends TransferHandler {
        @Override
        public boolean canImport(TransferSupport support) {
            // Check if dropped data is files
            return (support.isDrop()
                    && support.isDataFlavorSupported(DataFlavor.javaFileListFlavor));
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean importData(TransferSupport support) {
            if (!canImport(support)) {
                return false;
            }

            Transferable t = support.getTransferable();
            List<File> files = null;
            try {
                files = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
            } catch (UnsupportedFlavorException | IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }

            for (File file : files) {
                String path = file.getAbsolutePath();
                model.addRow(new String[]{path, "calculating..."});
                ModelUpdater mu = new ModelUpdater(path, model.getRowCount()-1);
                mu.execute();
            }

            return true;
        }
    }

    private class ModelUpdater extends SwingWorker<String, Long> {
        private String path;
        private Integer rowindex;

        public ModelUpdater(String path, Integer rowindex) {
            this.path = path;
            this.rowindex = rowindex;
        }

        @Override
        public String doInBackground() throws IOException {
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // update digest with each 8MB chunk
            long totalBytes = (new File(path)).length();
            long currentBytes = 0;
            try (FileInputStream istream = new FileInputStream(path)) {
                byte[] buf = new byte[8*1024*1024];
                int n;
                while ((n = istream.read(buf, 0, buf.length)) != -1) {
                    md.update(Arrays.copyOf(buf, n));
                    currentBytes += n;
                    publish(100*currentBytes/totalBytes);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }

        @Override
        protected void process(List<Long> values) {
            // only last one is effective
            String message = String.format("calculating %d%%...", values.get(values.size()-1));
            model.setValueAt(message, rowindex, 1);
        }

        @Override
        protected void done() {
            try {
                model.setValueAt(get(), rowindex, 1);
            } catch (Exception ignore) {
            }
        }
    }

}

上のコードでは、SHA1Calculatorクラスでウィンドウとコンポーネントを生成し、そのサブクラスのDropFileHandlerクラスでドラッグアンドドロップに対する処理を記述している。 また、SwingWorkerクラスを継承したModelUpdaterクラスでSHA-1ハッシュ値の計算を行っている。 Swingでハッシュ値計算などの時間がかかる処理を実行する場合、メインスレッドで実行すると画面描画がブロックしてしまうため、SwingWorkerを使ってバックグラウンドで実行させる必要がある。

コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160425223859p:plain

計算されたハッシュ値を別の方法で調べたハッシュ値と照らし合わせることにより、正しく計算できていることが確認できる。

実行可能なJARファイルを作ってみる

上のコードをJREで実行可能なJARファイルにするには、Package Explorerのプロジェクト名(SHA1Calculator)を右クリックし、「Export」を選択する。 表示されるウィザードから「Java」→「Runnable JAR file」を選択し、configurationと出力先パスを指定してFinishを押すと、JARファイルが生成される。

生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク