Autogui: 自動GUIバインディングライブラリ
English | 日本語 |
AutoguiはJava/SwingのGUIアプリケーションをplain-old Javaオブジェクトから作り出すライブラリです。 基本的な仕組みは与えられたオブジェクトのクラス構造をリフレクションAPIを通じて解析し、 クラスに定義されたプロパティとアクションに紐づけられたSwingベースのコンポーネントを組み合わせてユーザーインターフェースを作り出します。
ライセンス
ソースからのビルド
git clone https://github.com/ppp-kohe/autogui.git
cd autogui
本プロジェクトはビルドにapache-mavenを利用し、Javaの下記のバージョンに依存します。
- 1.7- : Java 21以降
- 1.2- : Java 11以降
- -1.1x : Java 8以降
mvn package
# このコマンドは target/autogui-1.7.jar を生成します。
本プロジェクトの主要部分はJDKクラス以外の他のライブラリに依存しません。
src/main/java
にあるソースファイル(及びsrc/main/resources
にあるリソース)を手動でコンパイルすることもできます。
Mavenの利用
本ライブラリを他のapache-mavenのプロジェクトで利用する場合は、pom.xml
ファイルに下記のdependency
セクションを挿入してください。
<dependency>
<groupId>org.autogui</groupId>
<artifactId>autogui</artifactId>
<version>1.7</version>
</dependency>
本ライブラリはMaven Central Repositoryから取得できます: org.autogui:autogui
APIドキュメント
簡単な利用方法の説明: jshellでのサンプル
本ライブラリはJava 9以降にJDKに導入された標準のREPLツールであるjshell
コマンド上で利用することができます。
利用するには、まず本ライブラリのjarファイルをクラスパスに含める必要があります。
jshell
では、/env -class-path <path/to/jar>
コマンドを実行することで可能です。
jshell
コマンドを起動したら、下記のコードのjshell>
プロンプト行以降を貼り付けることで試すことができます。(JDK, Git, Mavenがインストールされている環境を前提としています。)
$ git clone https://github.com/ppp-kohe/autogui.git
$ cd autogui
$ mvn package
$ jshell
| Welcome to JShell -- Version 21.0.3
| For an introduction type: /help intro
jshell>
class Hello {
String value;
void action() {
System.out.println(value);
}
}
/env -class-path target/autogui-1.7.jar
import org.autogui.swing.*
Hello h = new Hello();
AutoGuiShell.showLive(h)
上記のコードはまずHello
クラスを定義します。Hello
クラスは1つのインスタンスフィールドとメソッドを定義しています。
その後、org.autogui.swing.AutoGuiShell.showLive(Object)
の呼び出しを行います。これにより引数のオブジェクトからGUIウィンドウを生成して表示します。
生成されたウィンドウには”Value”とラベルのついたテキストフィールドと、”Action”とラベルのついたツールバーボタンが表示されているはずです。
テキストフィールドに文字列”hello, world”を入力し、ボタンをクリックすると、jshell
のコンソールに”hello, world”が出力されるはずです。
これは本ライブラリが、まずvalue
インスタンスフィールドからテキストフィールドを作り出していることを意味します。
このフィールドの型であるString
によってコンポーネントの種類はテキストフィールドになります。
ユーザーが生成されたウィンドウ上でテキストフィールドの文字列を編集した場合、本ライブラリは自動的に与えられたオブジェクトh
のフィールドの値を入力された文字列に代入します。これはjsehll> h.value
を実行することで確認することができるでしょう。
また、メソッドaction
はツールバーのボタンのアクションに紐づきます。
ボタンをクリックすると与えられたオブジェクトに対してそのメソッドが実行されます。
サンプルアプリケーション
src/test/java/autogui/demo
以下にサンプルアプリケーションのソースコードがあります。
コマンドmvn test-compile exec:java -Dexec.classpathScope=test -Dexec.mainClass=...
によって上記のディレクトリにあるコードを実行することができます。
ImageFlipDemo
下記のコードは以下のコマンドで実行できます:
mvn test-compile exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=org.autogui.demo.ImageFlipDemo
この ImageFlipDemo.java
は多少意味のある実用的なサンプルです。
package org.autogui.demo;
import org.autogui.swing.AutoGuiShell;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
public class ImageFlipDemo {
public static void main(String[] args) {
AutoGuiShell.showLive(new ImageFlipDemo());
}
BufferedImage image;
File output = new File("output.png");
void flipY() {
int w = image.getWidth();
int h = image.getHeight();
BufferedImage newImage = new BufferedImage(w, h, image.getType());
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
newImage.setRGB(x, h - y - 1, image.getRGB(x, y));
}
}
image = newImage;
}
void save() throws Exception {
if (output.exists()) {
System.err.println("File already exists: " + output);
} else {
ImageIO.write(image, "png", output);
}
}
}
このプログラムは下図のようなGUIウィンドウを表示します:
この表示されたウィンドウは以下のGUIコンポーネントを持ちます:
- 画像を表示するImageペイン: フィールド
BufferedImage image
から生成されます。画像ファイルをドラッグ&ドロップで入力データとして設定することができます。ドロップした画像は自動的にBufferedImage
オブジェクトとして読み込まれペインに表示された後、フィールドに代入されます。 - アクションボタンFlip Y: メソッド
void flipY()
から生成されます。画像をドロップして入力した後、ボタンをクリックすると画像がY方向で反転します。メソッドの実行で、生成された画像オブジェクトnewImage
がimage
フィールドに代入されます。メソッドの実行後、画像ペインは反転した画像を即座に表示するでしょう。 - ファイル名テキストフィールドOutput: フィールド
File output
から生成されます。このテキストフィールドは最初はフィールドの初期値”output.png”を表示します。テキストフィールドを編集するとフィールドの値が変更され、新しいFile
を保持するようになります。 - アクションボタンSave: メソッド
void save()
から生成されます。このアクションは反転した画像を新たなファイルとしてOutputフィールドに設定されたファイルへ書き出します。
FileRenameDemo
この例はライブラリのテーブル生成機能のデモアプリケーションです。
mvn test-compile exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=org.autogui.demo.FileRenameDemo
上記のコマンドは下図のようなGUIウィンドウを表示します:
利用方法としては、Dirフィールドにディレクトリをドラッグ&ドロップで設定すると、Entriesテーブルにディレクトリの内容の一覧を表示します。
ソースコードは下記のようになっています:
package org.autogui.demo;
import org.autogui.GuiIncluded;
import org.autogui.swing.AutoGuiShell;
import java.io.File;
import java.util.*;
@GuiIncluded public class FileRenameDemo {
public static void main(String[] args) {
AutoGuiShell.get().showWindow(new FileRenameDemo());
}
File dir;
@GuiIncluded public File getDir() { return dir; }
@GuiIncluded public void setDir(File dir) {
boolean update =
dir != null && !Objects.equals(this.dir, dir);
this.dir = dir;
if (update && dir.isDirectory()) {
List<RenameEntry> es = new ArrayList<>();
int i = 0;
List<File> files = new ArrayList<>(
Arrays.asList(dir.listFiles()));
files.sort(Comparator.naturalOrder());
for (File file : files) {
es.add(new RenameEntry(file,
String.format("%03d-%s",
i, file.getName())));
++i;
}
entries = es;
}
}
List<RenameEntry> entries = new ArrayList<>();
@GuiIncluded public List<RenameEntry> getEntries() {
return entries;
}
@GuiIncluded public static class RenameEntry {
File file;
String newName;
public RenameEntry(File file, String newName) {
this.file = file;
this.newName = newName;
}
@GuiIncluded public File getFile() {
return file;
}
@GuiIncluded public String getNewName() {
return newName;
}
@GuiIncluded public void setNewName(String newName) {
this.newName = newName;
}
}
@GuiIncluded public void rename() {
for (RenameEntry e : entries) {
File newFile = new File(
e.getFile().getParentFile(), e.getNewName());
if (e.getFile().exists() && !newFile.exists()) {
e.getFile().renameTo(newFile);
}
}
}
}
- このデモにおいて、本ライブラリは
@GuiIncluded
アノテーションがついたメンバからのみGUIコンポーネントを生成します。この利用形態をStrictモードと呼んでおり、AutoGuiShell.get().showWindow(...)
によって起動します。 - ファイルパスフィールドDir: 文字列を編集すると
File
オブジェクトを生成してsetDir(File)
メソッドの引数に与えて呼び出します。このメソッドは引数のディレクトリからファイル一覧を取得し、RenameEntry
オブジェクトとしてArrayList
に生成して追加します。RenameEntry
はstaticなネストクラスで、File
オブジェクトとインデックス番号を持つ新しいファイル名のプレフィックス文字列を保持します(String.format("%03d-%s",...)
によって設定)。 - Entriesテーブル:
getEntries()
メソッドで返されるList
から生成されます。さらにこのテーブルのカラムはList
の要素型である<RenameEntry>
のFileとNew Nameプロパティから生成されます。 テーブルの各行はList
オブジェクトの各要素と対応します。 ゲッターメソッドが返すList
オブジェクトのアイデンティティが変化するとテーブルが変更されたとみなし、自動的にテーブルの行データの表示が更新されます。 - Renameボタン:
rename()
メソッドから生成され、すべてのエントリのファイル名を更新します。
本ライブラリのコレクションオブジェクトからのテーブルコンポーネント生成機能は特に強力で、アプリケーションの適用範囲がとても広くなります。
@GuiIncludeを使ったStrictモード
本ライブラリをmain
メソッドから実行するアプリケーションで利用する場合、GUIコンポーネントに対応するメンバーを限定する方が望ましいといえます。
@GuiIncluded
と
AutGuiShell.get().showWindow(o)
によってこの制限を得ることができます。
Strictモードは…
AutoGuiShell.get().showWindow(o)
によって起動します。@GuiIncluded
が付加されたpublic
なクラス、プロパティ、メンバーのみを対象とします。
以下のJavaプログラムはStrictモードのサンプルアプリケーションです:
import org.autogui.GuiIncluded;
import org.autogui.swing.AutoGuiShell;
@GuiIncluded
public class Hello {
public static void main(String[] args) {
AutoGuiShell.get().showWindow(new Hello());
}
@GuiIncluded
public String hello;
@GuiIncluded
public void action() {
System.out.println(hello);
}
@GuiIncluded
public NonIncludedType label = new NonIncludedType();
//このプロパティはtoString()を表示するラベルになる
static class NonIncludedType {
public String toString() { return "non-included"; }
}
}
AutoGuiShell
クラスのインスタンスメソッドshowWindow
は生成対象のメンバーをorg.autogui.GuiIncluded
アノーテーションが付加されたもののみに限定します。 AutoGuiShell.get().showWindow(o)
から利用する場合、GUIに含めたいすべてのメンバー(クラス、フィールド、ゲッターメソッド、セッターメソッド、アクションメソッド)にアノーテーションをつける必要があります。
モジュールでの利用
Java 9以降に導入されたモジュールシステム上で本ライブラリを利用する場合、モジュールメンバーとして定義されたコードを本ライブラリに「公開(open)」する必要があります。これはライブラリがリフレクションAPIを通じて対象オブジェクトのコードにアクセスするためです。 名前付きモジュールにおいて、リフレクションAPIはopenされたメンバーのみに限定して利用できます。
本ライブラリのモジュール名はorg.autogui
です。以下の記述をmodule-info.java
に追加する必要があります:
open
修飾子を利用したいモジュール宣言に追加する。もしくは、exports
(またはopens
) 利用したいパッケージto org.autogui;
を追加する。requires org.autogui;
//自分の module-info.java の例
open module your.module { //"open"を自分のモジュールyour.moduleに追加する, もしくは...
exports your.pack to org.autogui; //自分のパッケージyour.packを本ライブラリに公開する
//もしくは "opens your.pack to autogui;" でも可能
requires org.autogui; //自分のコードから本ライブラリにアクセスできるようにする
//なお、org.autogui はJDKの java.desktop, java.datatransfer, java.prefs モジュールに依存する
}
本ライブラリの初期のバージョン(-1.1)では自動モジュールでした。これはmodule-info.class
を含まないjarで、Java 8以前との互換性を持っています。
この場合、本ライブラリのjarをクラスパスやモジュールパスの両方に追加することができます。しかし、自動モジュールはjlinkのモジュールに含めることができないという制限があります。
最近のバージョン(1.2-)では本ライブラリはmodule-info.class
を含んでおり、Java 11以降の要求に対応しています。
この状態でもクラスパスとモジュールパスの両方に追加することができ、さらにjlinkによりカスタムイメージが生成できます。(一方で本ライブラリはリフレクションAPIに依存しているため、実際の製品に適用する場合は十分な考慮が必要です。)
対応する型とコンポーネント
- 基本的な値の型
- String text-field: (文字列テキストフィールド)
java.lang.String
- File text-field: (ファイルパステキストフィールド)
java.nio.file.Path
またはjava.io.File
- Number spinner: (数値スピナー) 基本型の数値(
byte
,short
,int
,long
,float
,double
) またはjava.lang.Number
のサブタイプ - Boolean check-box: (ブーリアンチェックボックス)
boolean
もしくはjava.lang.Boolean
- Enum pull-down menu: (列挙プルダウン)
java.lang.Enum
のサブタイプ - Image pane: (画像ペイン)
java.awt.Image
のサブタイプ - Document editor: (ドキュメントエディター)
java.lang.StringBuilder
もしくはjavax.swing.text.Document
- Embedded component: (埋め込みコンポーネント)
javax.swing.JComponent
のサブタイプ
- String text-field: (文字列テキストフィールド)
- Object pane: (オブジェクトペイン) ユーザー定義のオブジェクト型で、複数のプロパティとアクションの組み合わせ
- オブジェクトのプロパティ:
T getP() {...}
,T p() {...}
(1.2-) ,void setP(T) {...}
,T p;
- もしすべてのメンバーが他のユーザー定義オブジェクトに関するものであれば、それらのオブジェクトはタブペインのタブに格納されます。
- アクションメソッド:
void m() {...}
- オブジェクトのプロパティ:
- Collection table: (コレクションテーブル)
java.util.Collection<E>
のサブタイプ、 配列E[]
。カラムは要素型E
から生成されます。- カラム
E
の値型- 文字列カラム:
java.lang.String
- ファイルカラム:
java.nio.file.Path
またはjava.io.File
- 数値カラム: 基本型の数値、もしくは
java.lang.Number
のサブタイプ - ブーリアンカラム:
boolean
もしくはjava.lang.Boolean
- 列挙カラム:
java.lang.Enum
のサブタイプ - 画像カラム:
java.awt.Image
のサブタイプ - 埋め込みコンポーネントカラム:
javax.swing.JComponent
のサブタイプ
- 文字列カラム:
- Object rows: (オブジェクト行) ユーザー定義型のオブジェクトは、プロパティのメンバーによるカラムを組み合わせて生成する。
- 動的なコレクションテーブル: ネストした
Collection<Collection<E>>
か多次元配列E[][]
- カラム
String text-field
java.lang.String
のプロパティはテキストフィールドに紐づきます。
class Hello {
String prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
File text-field
java.nio.file.Path
もしくは
java.io.File
のプロパティはファイルパス操作が可能なテキストフィールドに紐づきます。
import java.io.File;
class Hello {
File prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Number spinner
数値をあらわす基本型 (byte
, short
, int
, long
, float
and double
) や
java.lang.Number
のサブタイプ、具体的には
java.math.BigInteger
や java.math.BigDecimal
は数値のスピナーに紐づきます。
class Hello {
int prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Boolean check-box
真偽値のboolean
または
java.lang.Boolean
はチェックボックスに紐づきます。
class Hello {
boolean prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Enum pull-down menu
enum
型 (
java.lang.Enum
のサブタイプ) はプルダウンメニューに対応づきます。 (編集テキストフィールドがないコンボボックス).
列挙型のメンバーがメニューアイテムになります。
enum HelloEnum {
Hello, World
}
class Hello {
HelloEnum prop = HelloEnum.Hello;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Image pane
java.awt.Image
もしくはそのサブタイプは画像のプレビューが可能なペインに紐づきます。
import java.awt.*;
class Hello {
Image prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
このペインはドラッグ&ドロップをサポートし、Altを押しながらマウスホイールスクロールにより拡大縮小が可能です。
コード上の画像の操作はjava.awt.image.BufferedImage
と
javax.imageio.ImageIO
が便利です。
import java.io.File;
import java.awt.image.*;
import javax.imageio.*;
class Hello {
private File file;
void setFile(File f) throws Exception {
file = f;
if (f != null && f.isFile()) prop = ImageIO.read(f);
}
File getFile() { return file; }
BufferedImage prop;
int getImageWidth() {
return prop == null ? 0 : prop.getWidth();
}
int getImageHeight() {
return prop == null ? 0 : prop.getHeight();
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Document editor
java.lang.StringBuilder
もしくは
javax.swing.text.Document
のサブタイプはテキストエディタに紐づきます。
class Hello {
StringBuilder prop = new StringBuilder();
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
プロパティ値の変更(ドキュメントオブジェクトのアイデンティティの変更)はエディタのドキュメントを置き換えることになります。 したがって、このプロパティはユーザーの編集のために一貫した参照値を返す必要があります。
ドキュメントプロパティの適切な利用方法
テキストエディターのペインはプロパティの値に紐づいたテキスト内容の編集を反映します。
StringBuilder
については、そのオブジェクトをラップするDocument
オブジェクトが自動で生成されます。
そのドキュメントの編集に関する現在の実装は簡単なものになっており、巨大なサイズのテキストを編集する用途には適していません。
Swingのドキュメントとテキスト編集コンポーネント群は、どうやらどんなドキュメントの変更もテキスト編集コンポーネント内で起こることを仮定しているように見えます。
したがって、ユーザー定義のコードでStringBuilder
やDocument
のプロパティ値の内容を変種することは避けるべきです。
プロパティがjavax.swing.text.StyledDocument
型のサブタイプの場合、もしくはStringBuilder
の場合は、エディタ上で(ドキュメント全体の)スタイルをコンテキストメニューから変更することができます。
Object pane
ユーザー定義のオブジェクトのクラスのプロパティはそのクラスのメンバーを組み合わせたペインに紐づきます。
class Hello {
String prop1;
int prop2;
E prop3 = new E();
void action() {
System.out.printf("Hello: %s %d %s\n", prop1, prop2, prop3);
}
}
class E {
String prop;
void actionE() {
System.out.printf("E: %s\n", prop);
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
あるブジェクトのクラスは他のオブジェクトクラスのプロパティを持つことができ、それらは各オブジェクトに対応したサブペインとなります。
オブジェクトクラスのメンバーはアクションメソッドを含むことができ、それらはツールバーのボタンとなります。
これらのメソッドの条件としてその名前はget
, is
, set
で開始せず、引数を1つも取らない必要があります。
そのようなアクションメソッドは(当然)オブジェクトのプロパティを読み書きすることができます。もし、アクションメソッドがオブジェクトのプロパティを変更した場合、メソッド実行後、本ライブラリのUIは自動的に変更されたプロパティを特定し、プロパティに紐づいたUIコンポーネントを更新します。
プロパティ定義
ユーザー定義のオブジェクトクラスはサブコンポーネントに紐づいたプロパティを持つことができます。 ここで、プロパティとは 1) アクセス可能なフィールド もしくは 2) ゲッターとセッターメソッドのペア として定義されます。
class Hello {
String prop1;
private String prop2 = "hello";
String getProp2() { return prop2; } //read-only
private String prop3;
String getProp3() { return prop3; }
void setProp3(String p) {
prop3 = p;
System.err.println("updated " + prop3);
}
private boolean flag = true;
boolean isFlag() { return flag; } //is...() for boolean
void setFlag(boolean b) { flag = b; }
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
ゲッターメソッドは名前がget
またはis
(真偽値の場合)で開始し、引数を取らず、プロパティの型で値を返す定義です。
本ライブラリの1.2以降では T prop()
メソッド(例えばString prop()
)がゲッターとして利用できるようになりました。
これはJava 14で導入されたレコード型をサポートするためです。
この規則は既存のアクションメソッドを意図せずプロパティにしてまう可能性があります(voidでない型を返す場合)。
アクションメソッドとして認識させたい場合アノーテーションの引数@GuiIncluded(action=true)
を指定することで実現できます。
セッターメソッドは名前がset
で開始し、プロパティの値の型を1つとる定義です。
もしプロパティがゲッターメソッドのみで定義された場合、プロパティの値は読み出し専用になります。
セッターメソッド内のコードでは他のプロパティの値の変更を行うことができます。生成されたUIは自動的に変更されたプロパティを特定します。 このUIの更新を実現するため、ゲッターメソッドは頻繁に呼び出されます。
AutoCloseableサポート
ユーザー定義のオブジェクトクラスがjava.lang.AutoCloseable
インターフェースを実装する場合、そのオブジェクトのclose()
メソッドが対応するウィンドウを閉じた時に自動的に実行されます。
(なおAutoGuiShell.showLive(o)
で生成されたウィンドウはウィンドウを閉じても自動的に呼び出しません。ウィンドウを完全に閉じるにはcleanUp()
を利用してください。)
アクションプロパティの組み合わせ
特別な追加規則として、オブジェクトペインでのアクションボタンとテキストフィールドを組み合わせることができます:
- オブジェクトペインのクラスが
String <propName>
プロパティを持っており、 - そのクラスが
<propName>Action()
という名前のアクションメソッドを持つ場合
この時、文字列のプロパティに紐づいたテキストフィールはそのメソッドに対応するボタンを内包します。
import java.util.regex.*;
class PatternFind {
String text;
String search;
void searchAction() {
var m = Pattern.compile(search).matcher(text);
if (m.find()) {
System.out.printf("found: %d%n", m.start());
} else {
System.out.println("not found");
}
}
}
org.autogui.swing.AutoGuiShell.showLive(new PatternFind())
Object tabbed-pane
ユーザー定義のオブジェクトがプロパティとして他のユーザー定義オブジェクト型のメンバーしか持たない場合、そのオブジェクトはタブペインに紐づきます。そしてメンバーのユーザー定義オブジェクトから生成されたペインが各タブに対応します。
class Hello {
Tab1 tab1 = new Tab1();
Tab2 tab2 = new Tab2();
}
class Tab1 {
String prop;
}
class Tab2 {
int prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
Embedded component
javax.swing.JComponent
のサブタイプはサブコンポーネントとして埋め込みのペインに紐づきます。
import java.awt.*;
import javax.swing.*;
class Hello {
private JComponent value;
JComponent getValue() {
if (value == null) {
value = new JPanel() {
{ setPreferredSize(new Dimension(300, 300)); }
protected void paintComponent(Graphics g) {
g.setColor(Color.white);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.blue);
g.drawString("hello, world", 30, 30);
}
};
}
return value;
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello());
ユーザー定義のUIコンポーネントを作る場合、Swingコンポーネントの規則に従う必要があります。すなわち、コンポーネントのコードは基本的にはイベント駆動スレッド上で実行される必要があります。 そして、フィールドの初期化はイベント駆動スレッドでないスレッド(例えばメインスレッド)で実行される可能性があり注意が必要です。
よって埋め込みコンポーネントのプロパティに対するユーザーコードはゲッターメソッドで定義し、返すコンポーネントをフィールドにキャッシュするというスタイルをとるべきです。 埋め込みコンポーネントのプロパティについて本ライブラリはイベント駆動スレッド内で実行します。プロパティから得られた埋め込みコンポーネントはオブジェクトペインのサブコンポーネントとして追加されます。
Swingアプリケーションでの埋め込み
本ライブラリによってオブジェクトから生成したGUIを別のSwingアプリケーションに埋め込みたい場合、org.autogui.swing.GuiSwingRootPane.createForObject(o)
.
が利用できます。
src/test/java/org/autogui/demo/ObjectEmbeddedDemo.java
を参照してください。
サンプル実行:
mvn test-compile exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=org.autogui.demo.ObjectEmbeddedDemo
Collection table
java.util.Collection<E>
のサブタイプもしくは配列型E[]
のプロパティはテーブルペインに紐づきます。
テーブルのカラムは型引数E
から生成されます。
import java.util.*;
class Hello {
List<String> prop = new ArrayList<>();
void add() {
prop.add("hello " + prop.size());
prop = new ArrayList<>(prop);
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
何らかのアクションメソッドからテーブルの表示を更新したい場合、リストのインスタンスを置き換える必要があります。 prop = new ArrayList<>(prop);
が最も簡単な方法です。
Object rows
Collection<E>
の型引数E
がユーザー定義のオブジェクトクラスの場合、テーブルのカラムはそのクラスのプロパティから生成されます。
import java.util.*;
class Hello {
List<Item> prop = new ArrayList<>();
void add() {
prop.add(new Item("hello " + prop.size(), (int) (Math.random() * 100)));
prop = new ArrayList<>(prop);
}
}
class Item {
String name;
int num;
Item(String n, int i) { this.name = n; this.num = i; }
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
コレクションテーブルの選択行に対するアクション
もし、型引数E
のコレクションプロパティを定義するオブジェクトクラスがさらに別のjava.util.List<E>
型の引数をとるメソッドを持つ場合、対応するテーブルはそのメソッドに紐づいたアクションボタンを持つツールバーを持つことになります。
このボタンはテーブル上で選択された行に対応する要素をリストとして引数に与えて
メソッドを実行します。
もし、コレクションの要素オブジェクトがアクションメソッドを持つ場合、その場合もテーブルがツールバーにメソッドに紐づいたボタンを持ちます。 このボタンはテーブル上で選択された行に対応するオブジェクトにそれぞれ対してメソッドを実行します。
import java.util.*;
class Hello {
List<Item> prop = new ArrayList<>();
void add() {
prop.add(new Item("hello " + prop.size(), (int) (Math.random() * 100)));
prop = new ArrayList<>(prop);
}
void remove(List<Item> selectedItems) {
prop.removeAll(selectedItems);
prop = new ArrayList<>(prop);
}
}
class Item {
String name;
int num;
Item(String n, int i) { this.name = n; this.num = i; }
void update() { ++num; }
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
テーブル要素の選択の管理
リストに対して生成されたテーブルは要素セルの選択を操作する機能として 現在、以下の2種類があります:
- 選択要素を変更するためのアクションになる特別なメソッドを定義する: メソッドは変更後の要素のリストを返すようにします。
@GuiListSelectionUpdater
アノーテーションにより特定されます。 - テーブルUIから選択された要素を受け取るコールバックとなる特別なメソッドを定義する:
@GuiListSelectionCallback
アノーテーションによって特定されます。
import java.util.*;
import org.autogui.*;
class Hello {
List<Item> prop = new ArrayList<>();
void add() {
prop.add(new Item("Hello " + prop.size()));
prop = new ArrayList<>(prop);
}
@GuiListSelectionUpdater
List<Item> selectTop() {
return Arrays.asList(prop.get(0));
}
@GuiListSelectionCallback
void selected(List<Item> items) {
System.out.println("selected: " + items);
}
}
class Item {
String name;
Item(String n) { this.name = n; }
public String toString() { return "Item(" + this.name + ")"; }
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
上記の例でList<Item> selectTop()
は@GuiListSelectionUpdater
を付加しています。このメソッドはアクションボタン”Select Top”を生成し、実行するとprop
テーブルの選択要素をリストの先頭の要素に(その要素のリストを返すことで)変更します。
また、void selected(List<Item> items)
は@GuiListSelectionCallback
を付加しており、引数に要素のリストを受け取ります。このメソッドもアクションボタンになりますが、”*Selected”と先頭にマークが付き、prop
テーブルの選択の変更により自動で実行されます。
これらの特別なメソッドの対象となるテーブルは、要素の型、すなわち List<E>
の型引数E
によって特定されます。もし2つ以上のテーブルが同一の要素型をこれらのメソッドで使っていたら、それらすべてのテーブルがメソッドにより操作されます。
追加の特別な機能として、これらのアノーテーションは真偽型の引数index
を受け取ることができます。このフラグのデフォルト値はfalseで、trueが指定されると、付加された特別なメソッドはList<Integer>
を取ることを前提するようになります。この整数値は要素型E
の代わりに行番号を扱うようになります。さらにList<int[]>
だった場合は{行番号,カラム番号}を意味します。
セルのサイズ変更
生成されたテーブルにはカラムと行の表示サイズを変更する機能があり、テーブルヘッダーのポップアップメニュー(右クリックで開くコンテキストメニュー)から利用できます。
- Set All Column Width to This : すべてのカラムのサイズを対象カラムと同じにします
- Auto Resize Column Width : 自動的にすべてのカラムサイズを設定します
- Row Height : 行の高さを設定を変更します
- Fixed Size : すべての行を特定の高さに設定します。
- Fit to Content : すべての行の高さをその行の内容から決定します。
名前の規則と@GuiIncluded(name=…)
プロパティやアクション、型の名前の表示はcamelCaseの規則により決定します。
class Hello {
String helloWorld; //=> "Hello World"
String helloWORLD; //=> "Hello WORLD"
}
@GuiIncluded(name=...)
アノーテーションにより任意の名前を設定することもできます。
class Hello {
@GuiIncluded(name="class") String klass;
}
@GuiIncluded(index=…)による表示順の調整
コンポーネントの表示順は@GuiIncluded(index=...)
アノーテーションにより調整できます。
指定がない場合、デフォルトではプロパティの名前から決定します。
import org.autogui.GuiIncluded;
class Hello {
@GuiIncluded(index=3) String prop1;
@GuiIncluded(index=2) String prop2;
private String prop3;
@GuiIncluded(index=1) String getProp3() { return prop3; }
void setProp3(String p) { prop3 = p; }
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
ゲッターやセッターのペアに付加されたアノーテーションの場合、片方につけられたものが適用されます。
@GuiIncluded(description=…)によるコンポーネントの説明文
@GuiIncluded(description=...)
アノーテーションによりメンバーのツールチップメッセージを設定することができます。
import org.autogui.GuiIncluded;
class Hello {
@GuiIncluded(description="component description") String prop;
}
@GuiIncluded(keyStroke=…)によるキー割り当て
本ライブラリは自動的にメンバー名からショートカットキーを設定します。@GuiIncluded(keyStroke=...)
アノーテーションによって明示的に設定することもできます。
keyStroke="none"
: 割り当てを行わないようにしますkeyStroke="<control>... <key>"
control ::= shift | alt
key ::= A | B | C |... | Z | 0 | 1 ... | 9
- 予約されたキー: これらのキーはコマンド(macOS)かコントロールキーとの組み合わせになります
Q
: 終了,W
: ウィンドウを閉じるshift R
: 表示更新A
: すべて選択,shift A
: 選択解除Z
: 取り消す,shift Z
: やり直すO
: 開く,S
: 保存X
: カット,C
: コピー,V
: ペーストalt O
: JSONを開く,alt S
: JSONを保存,alt X
: JSONとしてカット,alt C
: JSONとしてコピー,alt V
: JSONをペースト,
: 設定
import org.autogui.GuiIncluded;
class Hello {
@GuiIncluded(keyStroke="L") String prop;
@GuiIncluded(keyStroke="T") void action() {
System.out.println("action " + prop);
}
//"action2"の名前から自動的にキー割り当て: Shift+Cmd+A
void action2() {
System.out.println("action2");
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
アクションに対するショートカットキーの実行は対象となるアクションの実行になります。 プロパティに対するショートカットキーの実行はUI上で対象コンポーネントにフォーカスします。 また、コントロール + エンターキーはフォーカス中のコンポーネントのコンテキストメニューを表示します。
UI要素の能動的な更新
本ライブラリで生成されたGUIコンポーネントはデフォルトでは自動的に表示を更新します。これは、何かアクションが発生した後にプロパティにアクセスして、再表示が必要な変更があるかどうかを確認することで実現しています。
@GuiNotifierSetter
アノーテーションの機能を使うことで、特定のGUI要素の能動的な更新が可能になります。
これは更新用のRunnable
オブジェクトがユーザーコードに与えられます。
このアノーテーションはRunnable
を引数にとる特別なセッターメソッドに付加します。
ユーザーコードでは与えられたRunnable
を保持してrun()
メソッドを呼ぶことで対象GUI要素が必要なときに再表示を行うことができます。
この対象GUI要素はメソッドのシグニチャset<YourPropertyName>Notifier(Runnable r)
かアノーテーション引数@GuiNotifierSetter(target="yourPropertyName")
から決定します。
import java.util.concurrent.*;
import org.autogui.*;
import java.time.*;
class Hello implements AutoCloseable {
private ScheduledExecutorService service;
private ScheduledFuture<?> task;
String prop;
private Runnable updater;
@GuiNotifierSetter
void setPropUpdater(Runnable r) { updater = r; }
Hello() {
service = Executors.newScheduledThreadPool(1);
task = service.scheduleWithFixedDelay(this::update, 0, 3, TimeUnit.SECONDS);
}
private void update() {
prop = Instant.now().toString();
if (updater != null) {
updater.run();
}
}
public void close() {
task.cancel(true);
service.shutdownNow();
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
上記のサンプルはテキストフィールドを表示し、3秒ごとにそのテキスト表示を更新します。
更新はupdate()
メソッドで行われます。そこではprop
を時刻の文字列 Instant.now().toString()
で更新し、Runnable#run()
を呼び出すことでフィールドの変更を通知します。このRunnableはsetPropUpdater(Runnable)
で設定された、本ライブラリから与えられるオブジェクトをupdaterとして保持します。
このセッターはset<YourPropertyName>Updater
の名前の規則に従いprop
フィールドを更新対象として指定しています。
コンストラクタではupdate()
メソッドが定期的に実行されるようスケジュールされます。これはScheduledExecutorService#scheduleWithFixedDelay(this::update, ...)
によりScheduledFuture
タスクとして生成します。
初期設定の管理
生成されたウィンドウは内包するサブコンポーネントに紐づくプロパティ値を保存して再利用する機能を持ちます。
この機能はjava.util.prefs
を利用して実現します。
class Hello {
String prop;
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
本ライブラリでは、割り当てられたコンポーネントを通じて編集されたプロパティの値が初期設定として自動的に保存されます。 生成されたウィンドウの Object メニューにある Preferences… から保存された初期設定を編集することができます。
このメニューは上記のような初期設定のウィンドウを表示します。画面には保存された初期設定がリストされます。
このリストには以下の初期設定が表示されます:
- Current : デフォルトの(名前のない)初期設定です。この初期設定の値はUI操作で更新されます。
- Empty : リセットのために値がないことを表現する擬似的な初期設定
- 保存された初期設定 : 保存された初期設定で、ツールバーの Save アクションから追加できます。このアクションを実行すると現在のUIコンポーネントから値と設定を収集して新たな初期設定として記録します。リスト上でダブルクリックにより名前を変更できます。
初期設定のリストには”Apply at Launch”というチェックボックスのカラムがあります。チェックが入った初期設定が起動時に適用されます。
初期設定ウィンドウのツールバーには Delete と Duplicate アクションがあり、これらは選択した初期設定を削除/複製することができます。 Update アクションは選択した初期設定を最新のUIコンポーネントの値により上書きします。 Apply アクションは即時に選択された初期設定をUIコンポーネントに適用します。 Write To File… と Load From File… アクションは初期設定をJSONファイルに保存/読み込みすることができます。
本ライブラリの1.7以降では選択された初期設定を画面の右側のペインで直接編集することができます。
また、保存した初期設定は Object メニューの Apply Preferences メニューの項目にリストされ、選択することで適用できます。
GuiPreferencesLoader
クラスを利用することでGUIを使わないコードで初期設定の機能を利用することができます。
var h = new Hello();
org.autogui.base.mapping.GuiPreferencesLoader.get()
.withTypeBuilderRelaxed()
.withPrefsGetterByNameEquals("saved-prefs")
.apply(h);
h.prop //=> "Hello"
埋め込みコンポーネントのカスタム初期設定
カスタムした埋め込みコンポーネントを使う場合、本ライブラリの初期設定の機能を簡単に利用できます。
本機能を利用する場合、カスタムコンポーネントがGuiPreferences.PreferencesJsonSupport
インターフェースを実装する必要があります。
このインターフェースは2つのメソッドの実装を要求します。それぞれ初期設定の生成と設定をJSONオブジェクトとして扱います。
この機能はプロパティメソッドの静的な戻り値型がインターフェースを満たしている場合に有効になります。したがってカスタムした型をメソッド戻り値型に指定する必要があります。
import org.autogui.base.mapping.GuiPreferences;
import javax.swing.*;
import java.util.*;
class Hello { //オブジェクトペインの型
MyPane pane;
MyPane getPane() { //カスタムした型の埋め込みコンポーネントを返す
if (pane == null) pane = new MyPane();
return pane;
}
}
class MyPane extends JPanel
implements GuiPreferences.PreferencesJsonSupport {
JTextField data = new JTextField(20);
MyPane() {
add(data);
}
public Map<String,Object> getPrefsJson() { //初期設定ストアに書き出すための値
System.err.println("save " + data.getText());
return Map.of("myItem", data.getText());
}
@SuppressWarnings("unchecked")
public void setPrefsJson(Object v) { //初期設定ストアから復元されたデータを設定
if (v instanceof Map<?,?>) {
data.setText(((Map<String,String>) v).getOrDefault("myItem", ""));
}
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
getPrefsJson()
で返されるオブジェクトは単純なJSONの型の組み合わせでなければなりません(Map、List、String、Number、Boolean)。
また、初期設定ストアに保存できるように十分に小さいデータサイズである必要があります。
ロギング
生成されたウィンドウはステータスバーとログエントリを表示するリスト表示の機能を持っています。ロギングエントリは以下の種類があります:
- 文字列メッセージ
- 進捗バー
- 例外
文字列メッセージのログ
生成されたウィンドウは
System.err
と
System.out
を置き換えます。置き換えられたこれらのストリームからの出力はステータスバーとリストウィンドウにも表示されます。
この置き換えはGuiSwingLogManager#setupConsoleWithDefaultFlags()
の実行で読み取られるシステムプロパティにより設定にできます。
-Dautogui.log.replaceErr=false -Dautogui.log.replaceOut=false
のようなVMオプションを追加することで向こうにできます。
また、ユーザーコードからはGuiLogManager
を使って直接表示することもできます。
import org.autogui.base.log.*;
class Hello {
void action() {
System.err.println("message");
GuiLogManager.get().logString("string message");
GuiLogManager.get().log("composed message ", 1);
GuiLogManager.get().logFormat("format message %d", 2);
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello());
進捗のログ
GuiLogManager
からGuiLogEntryProgress
を作り出すことで進捗バーを表示し、操作することができます。
ファクトリメソッドlogProgress(n)
は最大カウントn
によりエントリを作成します。
addValue(i)
または
addValueP(p)
(0…1.0の値を取る)
によりカウントを更新できます。
進捗バーはsetMessage(s)
によってメッセージを表示することもできます。
import org.autogui.base.log.*;
class Hello {
void action() {
new Thread(this::m).start();
}
private void m() {
try (GuiLogEntryProgress p = GuiLogManager.get().logProgress(100)) {
for (int i = 0; i < 100; ++i) {
p.setMessage("next " + i).addValue(1);
Thread.sleep(1000);
}
} catch (InterruptedException ie) {
GuiLogManager.get().logError(ie);
}
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello());
進捗バーの右側に表示されるボタンによりlogProgress()
を呼び出したThread
に割り込みを入れることができます。
割り込みにより、スレッドがブロックする間InterruptedException
が発生します。このブロックはThread.sleep(n)
などで発生します。
ブロッキングなしの実行コードについてはコード上で明示的にThread.interrupted()
を呼び出して割り込みが発生したかどうか明示的に確認する必要があります。
例外のログ
生成されたウィンドウはキャッチされなかった例外に対するハンドラを設定し、ログリストに例外の表示を行うことができます。GuiLogManager.get().logError(e)
は明示的に与えられた例外オブジェクトを表示することができます。この表示はログリストで表示される要素となり、スタックトレースを展開することができます。
import org.autogui.base.log.*;
class Hello {
void action() {
GuiLogManager.get().logError(new RuntimeException("error1"));
throw new RuntimeException("error2");
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello());
長時間のアクションと進捗バー
GUIアクションの実行が極端に長くなった場合、(最大カウントのない)進捗バーを表示する独立したタスクとなります。停止ボタンは割り込みを発生させるので、
InterruptedException
もしくは
Thread.interrupted()
で確認できます。
class Hello {
void action() throws Exception {
for (int i = 0; i < 100; ++i) {
if (i % 10 == 0) System.err.println(i);
Thread.sleep(1000);
}
System.err.println("finish");
}
}
org.autogui.swing.AutoGuiShell.showLive(new Hello())
外観の設定
現在のSwing GUI (Java23) は近年のOSの自動的なダークモードの切り替えに対応していません。
しかし、カスタムルック&フィール(LAF)のライブラリを使うことでダークモードをサポートすることができます。具体的には darklaf や flatlafがあります。
本ライブラリのデフォルトの起動プロセスではLAFを自動的に設定します。
autogui.laf
プロパティにより設定するLAFを指定できます。JVMの起動オプションで-Dautogui.laf=...
で設定可能です。このプロパティは下記の値をとります。なお対応するコードは
UIManagerUtil.selectLookAndFeelFromSpecialName(String)です。
- 値設定なし、または空文字
""
かdefault
- 後述するflatlafロードを行を試す
- MacOSで外観がダークでなければ
system
(システムデフォルトのLAF: AquaLookAndFeel)を適用する - 上記以外の場合
nimbus-flat
を適用する
default-no-darklaf
:default
と同じだがflatlafロードは試さないmetal
:javax.swing.plaf.metal.MetalLookAndFeel
を適用するnimbus
:javax.swing.plaf.nimbus.NimbusLookAndFeel
を適用するnimbus-flat
: 本ライブラリ独自のNimbusLookAndFeelCustomFlat
を適用するnimbus-flat-light
: 本ライブラリ独自のNimbusLookAndFeelCustomFlat
のライトテーマを適用するnimbus-flat-dark
: 本ライブラリ独自のNimbusLookAndFeelCustomFlat
のダークテーマを適用するsystem
: 現在のOSごとに異なるLAFdarklaf
: 後述するflatlafロードを試すnone
: LAFの設定を行いません
NimbusLookAndFeelCustomFlat
nimbus-flat
で指定されたorg.autogui.swing.util.NimbusLookAndFeelCustomFlatは本ライブラリが独自に実装するLAF(1.7-)で、JDKに実装されているNimbusLookAndFeelをカスタムし、起動時のテーマによってライトテーマかダークテーマのどちらかを適用します。
テーマの判定はOSによって異なりますが、外部のコマンドを実行することで行います。なお、これらのスタイルはnimbus-flat-light
またはnimbus-flat-dark
の指定で明示的に指定できます。
nimbus-flat-light |
nimbus-flat-dark |
---|---|
![]() |
![]() |
MacOSではJVMのオプション-J-Dapple.awt.application.appearance=system
を追加することで、ダークモードのウィンドウタイトルバーを適用できます。
GNOME(デフォルトのUbuntuなど)でのHiDPI環境では環境変数GDK_SCALE=2
を指定することで適切な表示が得られる場合があります。
Loading Flatlaf
本ライブラリのデフォルトのLAF指定は暗黙のうちにflatlaf (com.formdev:flatlaf:3.4.1
により確認しています)が実行環境に含まれているかどうかをチェックし、OSの現在のテーマにあったルック&フィール(LAF)を自動的に適用します。このチェックと適用は単純にClass.forName
を使ってリフレクションAPIにより実現しています。