ウィジェット・ツールキットの検討

JavaのSwingでGUIアプリを作成する際、既存のLook and Feelを混在させるとその制約を受けて自由度が低くなるケースが多々あるため、言語にとらわれず新たなウィジェット・ツールキットを検討しています。

C++であればFLTKあたりが比較的容易に導入できそうなので、勉強を兼ねてSwingで開発中のアプリを移植できるか試しにいじってみることにしました。そこそこの外観で処理速度アップが目標です。

Swingの使いこなしの方もまだまだですし、並行してスキルアップに努めます。SynthLookAndFeelを使えばデフォルトのLook and Feelを無効化できるので、アプリの処理速度にこだわらなければこれで好きなように描画可能です。

[Java]103 Look and Feelの外観

これまで様々なLook and Feelを扱ってきましたが、Swingで作ったアプリの外観は美的観点から問題があるように思います。

デフォルトであるクロスプラットフォームL&FのMetalは青色が幅を利かせています。WindowsのTabbedPaneはbackgroundが薄い青色で塗りつぶされて見栄えは最悪です。

これを避けるためにコードの頭でUIManager.setLookAndFeelにてL&FをシステムL&Fに変更してみました。しかしWindowsのシステムL&Fも今ひとつです。特にJSliderのつまみは何故か透明で見れたもんじゃないです。加えてJButtonのマージンは横方向が無駄に大きく、フォントサイズをかなり小さくしないと”…”と表示されてしまいます。ここまで酷いとJavaでWindowsアプリを作る気にはならないでしょう。

MacのシステムL&FであるAquaは私の使う範囲ではJButtonのデザイン以外特に問題ないのですが、Windowsの場合はMetalもシステムL&Fもダメなので、結局JComponentを継承したクラスを必要なComponentに応じて自製しそれぞれ描画させています。

Swingの機能自体は素晴らしいというのに、肝心の見た目が悪ければプログラマとユーザーは離れてしまいます。新たなGUIライブラリの出現に期待したいところです。

[Java]102 JButtonの継承

JButtonのToolTipTextの位置を調整するため、これを継承したJButtonExクラスを作成しました。

コンストラクタも自動的に継承されるものと思っていましたが、きちんと書かないとエラーになります。

public static class JButtonEx extends JButton {
		public JButtonEx(String str) {
			setText(str);
		}
		public JButtonEx() {
		}
		@Override
		public Point getToolTipLocation(MouseEvent e) {
			Point po = e.getPoint();
			po.x += 20;
			po.y -= 10;
			return po;
		}
	}

[Java]101 UIDefaultsの内容を出力

Look and Feelの自製を見据え、UIDefaultsの内容を出力してみました。

コードはGitHubにあったものに加筆してkeyだけでなくvalueも出力できるようにしました。

以下のようになります。

import java.util.Enumeration;
import java.util.TreeSet;
import javax.swing.UIDefaults;
import javax.swing.UIManager;

public class ListUIDefaultsKeys {
	public static void main(String args[]) throws Exception {
		UIManager.LookAndFeelInfo looks[] = UIManager.getInstalledLookAndFeels();

		for (UIManager.LookAndFeelInfo info : looks) {
			System.out.println("Installed L&F "+info.getName());
		}

		TreeSet<String> lafDefaultKeys = new TreeSet<>();
		UIDefaults defaults = UIManager.getDefaults();

		Enumeration<Object> newKeys = defaults.keys();
		while (newKeys.hasMoreElements()) {
				lafDefaultKeys.add(newKeys.nextElement().toString());
		}

		System.out.println("================== UIDefaults Keys & Values ==================");
		
		int i = 1;
		for (String key : lafDefaultKeys) {
				try {
						String value = defaults.get(key).toString();
						System.out.println(String.format("%03d", i) + " " + key );
						System.out.println("    " + value);
				} catch (Exception e){
						e.printStackTrace();
				}
				i++;
		}
	}
}
--------------------------------------------------

出力の一部
--------------------------------------------------
================== UIDefaults Keys & Values ==================
001 AbstractButton.click.textAndMnemonic
    クリック
002 AbstractDocument.addition.textAndMnemonic
    追加
003 AbstractDocument.deletion.textAndMnemonic
    削除
004 AbstractDocument.redo.textAndMnemonic
    やり直し
005 AbstractDocument.styleChange.textAndMnemonic
    スタイル変更
006 AbstractDocument.undo.textAndMnemonic
    元に戻す
007 AbstractUndoableEdit.redo.textAndMnemonic
    やり直し
008 AbstractUndoableEdit.undo.textAndMnemonic
    元に戻す
009 AuditoryCues.allAuditoryCues
    [Ljava.lang.Object;@4ac68d3e
010 AuditoryCues.cueList
    [Ljava.lang.Object;@4ac68d3e
011 AuditoryCues.noAuditoryCues
    [Ljava.lang.Object;@3339ad8e
<以下略>

参考サイト

[Java]100 VSCode:Extension Pack for Javaとjavacコマンドの比較

表題の件、Javaソースコードのコンパイルエラー時に得られる情報量に雲泥の差がありました。Extension Pack for Javaは設定を変えればjavacと同レベルになるのでしょうがデフォルトがあまりにもしょぼ過ぎました。

# VSCode Extension Pack for Java
--------------------------------------------------
Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
        Unreachable catch block for InvalidPropertiesFormatException. This exception is never thrown from the try statement body
        Unreachable catch block for FileNotFoundException. This exception is never thrown from the try statement body
        Unreachable catch block for IOException. This exception is never thrown from the try statement body
        at XMLtoSetProperty.main(XMLtoSetProperty.java:32)
--------------------------------------------------

# javacコマンド
--------------------------------------------------
XMLtoSetProperty.java:32: エラー: 例外FileNotFoundExceptionは対応するtry文の本体ではスローされません
		} catch (FileNotFoundException e){
		  ^
XMLtoSetProperty.java:34: エラー: 例外IOExceptionは対応するtry文の本体ではスローされません
		} catch (IOException e){
		  ^
XMLtoSetProperty.java:36: エラー: 例外InvalidPropertiesFormatExceptionはすでに捕捉されています
		} catch (InvalidPropertiesFormatException e){
		  ^
XMLtoSetProperty.java:58: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
				in.close();
				        ^
XMLtoSetProperty.java:54: エラー: 例外FileNotFoundExceptionは報告されません。スローするには、捕捉または宣言する必要があります
			in = new FileInputStream(file);
			     ^
XMLtoSetProperty.java:55: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
			settings.loadFromXML(in);
			                    ^
エラー6個
--------------------------------------------------

VSCodeの出力内容でコードを修正できる方はあまりいないでしょう。javacコマンドであれば直すべき箇所が一目瞭然で一発修正できます。

開発速度への影響が大きいのでVSCodeでの実行がうまくいかない時は即コマンドで確認すべきだと痛感しました。

[Java] 99 OS名とLinuxディストリビューション名の取得

自製アプリのクロスプラットフォーム化にあたりOSやディストリビューションによってフォントサイズを変える必要が生じたため、以下のメソッドを作成して対応しました。

UbuntuやLinux Mint以外で”cat /etc/issue”コマンドにてディストリビューション名が分かるかどうかは不明です。

言語仕様とは言え、変数(フォントサイズ)を一々配列に格納するのは面倒です。

// Windows,Macはサイズ8,Ubuntuは7,Linux Mintは10に設定

public static int font_size(){
	String OS = System.getProperty("os.name").toLowerCase();
	int[] font_sizes = new int[1];
	if (OS.contains("linux")){
		try{
			Process p = Runtime.getRuntime().exec("cat /etc/issue");
			try{
				p.waitFor();
				InputStream is = p.getInputStream();
				InputStreamReader inputStreamReader = new InputStreamReader(is);
				Stream<String> streamOfString= new BufferedReader(inputStreamReader).lines();
				String str = streamOfString.collect(Collectors.joining());
								
				if (str.contains("Ubuntu")){
					int font_size = 7;
					font_sizes[0] = font_size;
				}else if (str.contains("Mint")){
					int font_size = 10;
					font_sizes[0] = font_size;
				}else{
					int font_size = 8;
					font_sizes[0] = font_size;
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	} else {
		int font_size = 8;
		font_sizes[0] = font_size;
	}
	return font_sizes[0];
}

[Java] 98 JColorChooserのshowDialogメソッドを自製

前回の続きです。

前の記事のコードではJcolorChooserダイアログの取消ボタンが効かず、変更した色のRGB値が返されてしまいます。そこでActionListenerとしてokListenerとcancelListenerを追加しました。

それぞれのアクションとして戻り値のRGBA値を作成しますが、public修飾子が使えずこれらをActionListenerの外に渡す方法が分からなくて少々苦労しました。

結局、初期設定で作成しておいたint配列color_caseに移し替えるという何とも泥臭い方法で取り出しました。Javaの仕様ではこうするしかないようです。

okListenerとcancelListenerを今回のように使っているコード例はGoogle検索上位では見あたらなかったのでアップしておきます。

<メソッドのみ>

static int[] showDialog(Component component, String title, Color initialColor) throws HeadlessException{
		int[] color_case = new int[4];
		JColorChooser chooser = new JColorChooser(initialColor != null ? initialColor : Color.white);

		String swatches = UIManager.getString("ColorChooser.swatchesNameText");
		String hsv = UIManager.getString("ColorChooser.hsvNameText");
		String hsl = UIManager.getString("ColorChooser.hslNameText");
		String rgb = UIManager.getString("ColorChooser.rgbNameText");
		String cmyk = UIManager.getString("ColorChooser.cmykNameText");

		List<String> list = Arrays.asList(hsv,hsl,rgb);

		for (AbstractColorChooserPanel p : chooser.getChooserPanels()) {
			if (!list.contains(p.getDisplayName())) {
				chooser.removeChooserPanel(p);
			}
		}
		
		ColorTracker tracker = new ColorTracker(chooser,initialColor);

		ActionListener okListener = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				int[] colors = tracker.getColor();
				color_case[0] = colors[0];
				color_case[1] = colors[1];
				color_case[2] = colors[2];
				color_case[3] = colors[3];
			}
		};

		ActionListener cancelListener = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				int[] colors = tracker.getInitial();
				color_case[0] = colors[0];
				color_case[1] = colors[1];
				color_case[2] = colors[2];
				color_case[3] = colors[3];
			}
		};

		// ダイアログを作成
		JDialog dialog = JColorChooser.createDialog(component, title, true, chooser, okListener, cancelListener);
		dialog.setVisible(true);

		return color_case;
	}

	static class ColorTracker implements Serializable{
		JColorChooser chooser;
		Color color;
		int red;
		int green;
		int blue;
		int alpha;
		int[] colors;
	  
		ColorTracker(JColorChooser Chooser,Color initialColor){
			this.chooser = Chooser;
			this.color = initialColor;
			
		}
	  
		int[] getColor(){
			this.color = this.chooser.getColor();

			colors = new int[4];
			colors[0] = this.color.getRed();
			colors[1] = this.color.getGreen();
			colors[2] = this.color.getBlue();
			colors[3] = this.color.getAlpha();

			return colors;
		}
		int[] getInitial(){
			colors = new int[4];
			colors[0] = this.color.getRed();
			colors[1] = this.color.getGreen();
			colors[2] = this.color.getBlue();
			colors[3] = this.color.getAlpha();

			return colors;
		}
	}

[Java] 97 JColorChooserクラス

自製アプリに色調整の機能を追加するためダイアログを作成しようとしましたが、取り掛かる前にJColorChooserという便利なクラスを見つけました。機能的にはこれで十分です。

下記コードは色調整ダイアログを表示するボタンのアクションです。listを書き換えると表示タブを変更・並べ替えできます。今回は”swatches”以外の4つのタブを選択しています。またデフォルトにある下段のプレビュー部を消してみました。

デフォルトの5つのタブから取捨選択したChooserはcreateDialogでダイアログを作成する際に引数として使用します。

自製showDialogメソッドはJColorChooser.showDialogメソッドとは異なりOKボタンを押した時のRGB値を返してくれないので、これを追跡して返すColorTrackerクラスを用意しました。GitHubにあったコードに手を入れて作成しました。

思いのほか手間が掛かり半日を要しましたが、C/C++で書く苦労を考えれば大分マシでしょう。

<ライブラリは省略>

public class AdjustButtonAction{
    public static void main(){
      adj_btn.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e){
				if(e.getSource() == adj_btn){
					Color defaultColor = new Color(65,105,225);
					Color color = showDialog(parent, "色の調整", defaultColor);

					if(color == null) {
						System.out.println("選択されず");
					}else {
						System.out.println("R " + color.getRed());
						System.out.println("G " + color.getGreen());
						System.out.println("B " + color.getBlue());
        			}
				}
			}
		});
    }
	
	static Color showDialog(Component component, String title, Color initialColor) throws HeadlessException{
		JColorChooser chooser = new JColorChooser(initialColor != null ? initialColor : Color.white);
		
		// デフォルトの下段にあるプレビューを非表示にする
		chooser.setPreviewPanel(new JPanel());

		String swatches = UIManager.getString("ColorChooser.swatchesNameText");
		String hsv = UIManager.getString("ColorChooser.hsvNameText");
		String hsl = UIManager.getString("ColorChooser.hslNameText");
		String rgb = UIManager.getString("ColorChooser.rgbNameText");
		String cmyk = UIManager.getString("ColorChooser.cmykNameText");
		System.out.println("swatches " + swatches);
		System.out.println("hsv " + hsv);
		System.out.println("hsl " + hsl);
		System.out.println("rgb " + rgb);
		System.out.println("cmyk " + cmyk);

		// 色フォーマットのタブを選択
		List<String> list = Arrays.asList(hsv,hsl,rgb,cmyk);

		for (AbstractColorChooserPanel p : chooser.getChooserPanels()) {
			if (!list.contains(p.getDisplayName())) {
				chooser.removeChooserPanel(p);
			}
		}
	
		// ダイアログ作成
		ColorTracker ok = new ColorTracker(chooser);
		JDialog dialog = JColorChooser.createDialog(component, title, true, chooser, null, null);

		dialog.setVisible(true); 

		return ok.getColor();

	}
	static class ColorTracker implements ActionListener, Serializable{
		JColorChooser chooser;
		Color color;
	  
		ColorTracker(JColorChooser Chooser){
			this.chooser = Chooser;
		}
	  
		public void actionPerformed(ActionEvent e){
		}
	  
		Color getColor(){
			this.color = this.chooser.getColor();
			System.out.println("this.color " + this.color);
		return this.color;
		}
	}
}
--------------------------------------------------
出力
--------------------------------------------------
swatches サンプル(S)
hsv HSV(H)
hsl HSL(L)
rgb RGB(G)
cmyk CMYK
this.color java.awt.Color[r=65,g=105,b=225]
R 65
G 105
B 225

[Java] 96 Microsoft Storeアプリの申請

最初の申請から審査通過までに約1ヶ月掛かりました。諦めかけた時もありましたが、とても有用な記事の助けも借りてなんとか登録できました。

Visual Studioではなくコマンドラインで提出用appxパッケージを作成するのは、かなりハードルが高かったです。あれだけ苦労したAppleによる公証が楽勝に思えるレベルです。

以下、経過をメモしておきます。
————————————————–
2021年12月下旬:初回提出
8日後:不合格通知(クラッシュする可能性があるとのこと)

翌日:2回目提出
6日後:反応がないため催促メール送信
2日後:不合格通知

同日:3回目提出
翌日:クラッシュが起こる具体的条件を教えて欲しい旨をメールで伝えた
翌日:不合格通知受取、クラッシュする様子を撮影したスクショ動画とイベントビューアのログが添付されていた

同日:4回目提出(ここまではexeインストーラによるインストールおよび動作を確認して提出)
5日後:不合格通知受取、その後しばらくはラズパイをいじるなどして気分転換

8日後:5回目提出(ストアアプリとしてインストール後の挙動を確認した上で提出)
同日:合格通知受取、提出から3時間半で連絡あり
————————————————–

4回目まではappxパッケージに自己署名してからインストールするというやり方を知らなかったので、インストール後のストアアプリとしての動作確認が向こう任せになっていました。

Microsoftパートナーセンターの案内では最長3営業日かかるとありますが、結果が芳しくないとさらに放置されます。こちらから働きかけないと動いてくれません。なおメールは英語でのやりとりになります。

まあデベロッパー登録料が1847円(初期登録料のみで更新料無料)という格安サービスですから、一連のやりとりで十分元は取れました。

その後はバージョンアップの申請もあっさり通過して、すこぶる順調です。

参考記事

[Java] 95 プレビュー版classファイルの実行

プレビュー版classファイル(Java17の場合はver 17.65535)は正式なビルドはできませんが、以下コマンドで実行できます。

仮jarファイルをjpackageコマンドで正常なパッケージにはできません。正確に表現するとパッケージには一応できますがインストールしても起動しません。あくまでプロトタイプとしてのコードということでしょう。

# Testプロジェクトのbin/baseにあるメインクラスを実行
cd /Test/bin
java --enable-preview base.Main

# 仮jarの実行
java -jar --enable-preview test.jar