どうしても名前付き引数が欲しい!
今回は前回の予告を変更し、先に避けては通れないアノ問題に対峙していこうと思います。
前回もちょっと触れたのですが…
次はパディングです。これも
Widgetインターフェースで定義してあるメソッドです。ボタン内側の余白を作ります。SwiftUIでは上下左右のそれぞれに余白の値を設定することができますが、SwingUIでは現状、上下左右に同じ値の余白しか作りません。まぁその内、作るつもりです、たぶん…
これまで上下左右に同じパディングしか与えないWidget#padding(int)のみの定義だったのは、どうしても面倒が先に立って、それを乗り越える気力が湧かなかったのが原因でした。とにかく面倒なんです。何が面倒かと言うと…
ところで似たモノとして、コンポーネント周りの余白ではなく、コンポーネント間の余白を確保するSpacerがありました。
このSpacerに余白領域をサイズ指定するには固定値の幅と高さ、もしくは幅いっぱい/高さいっぱい、という可能な限り最大化する指定方法がありました。基本的にこれらのパターンなのですが、実際にメソッドとして対応するには…
1. Spacer.of(int width, int height); // 幅・高さで指定
2. Spacer.of(UISize width, int height) // 幅が領域いっぱい、高さは固定値で指定
3. Spacer.of(int width, UISize height) // 幅は固定値で、高さが領域いっぱい指定
4. Spacer.widthOnly(int width); // 幅だけ指定
5. Spacer.widthOnly(UISize width); // 幅だけ領域いっぱい指定
6. Spacer.heightOnly(int height); // 高さだけ指定
7. Spacer.heightOnly(UISize height); // 高さだけ領域いっぱい指定
8. Spacer.fill(); // 幅・高さを領域いっぱい指定
これだけの公開メソッドがありました。特にwidthOnly,heightOnlyですね。幅のみの指定とか、高さのみの指定、というメソッドが必要となり、これがメソッドを増やしてしまう原因なのです。たとえ幅だけの指定であってもof()で済ませたいんですよね。Spacer.of(width: 10)みたいな感じで。でもJavaには名前付き引数の機能がない(加えて引数の初期値指定ができない)ので困って苦し紛れに幅のみのメソッドとか作っちゃうんです。でもこれが上下左右のパディングなんてなったら大変なのは火を見るよりも明らか… これが上下左右別々のパディング指定を実現させることを遠ざけてた原因なんです。しかし、これはいつまでも引き延ばせないんです。だから今回、やりますよ!
今回の目標は、名前付き引数のように、見た目でどの箇所の余白を指定しているかが分かり、指定されなかった箇所はデフォルト値が適用されるです。
では実際にWidget#padding()で挑戦してみましょう。とは言えJavaに名前付き引数の機能がない以上、全く同じレベルの機能は作れません。ということで、こんな感じで実現してみるのはどうでしょう。
Button.of("おーい、ボタンですよ!")
.padding(Left.of(20), Right.of(40), Bottom.of(60))
このように、引数名の代わりにクラスで代替し区別すると、どの箇所にパディング指定するのか分かります。そして、上記には指定されていないTopに関してはデフォルト値が適用される、という仕様で、とりあえずOKとしましょう。
完ぺきではなくとも、割り切りましょう🤗
さて上記を実現するには、まず引数がいくつ指定されるか不明なので、可変長引数で対応することにします。ただ可変長引数は型が同じでないといけません。そのため、それぞれのクラスにはGapという親クラスを持たせ、その型で共通化することにします。
Gapクラスは余白のギャップ(間隔)を保持するだけのクラスです。これにTop,Bottom,Left,Rightのそれぞれのクラスが継承することで、Gap型として可変長引数の指定を可能にします。
public interface Widget<T extends JComponent>
{
/**
* パディングの設定をする。
*
* @param gaps 各サイド(left, top, right, bottom)のパディング
* @return 自身のインスタンス
*/
T padding(Gap... gaps);
//// ---------- 後略 ---------- ////
で、このWidget#padding(Gap...)をSwingUI用の各拡張コンポーネントが実装する訳なのですが…
public class ButtonWT extends JButton implements Widget<ButtonWT>
{
//// ---------- 中略 ---------- ////
@Override
public ButtonWT padding(Gap... gaps)
{
return WidgetHelper.padding(this, gaps);
}
//// ---------- 後略 ---------- ////
実装内容はコンポーネント共通なので、ヘルパークラスにお願いします。
public class WidgetHelper
{
/**
* 指定コンポーネントのパディングの設定をする。
*
* @param <T> JComponentの継承クラス
* @param target 対象コンポーネント
* @param gaps 四方(left, top, right, bottom)のパディング
* @return 対象コンポーネント
*/
public static <T extends JComponent> T padding(T target, Gap... gaps)
{
// 四方のパディング取得
AllSidesGap sides = AllSidesGap.of(gaps);
// パディング設定
target.setBorder
(
BorderFactory.createCompoundBorder
(
target.getBorder(),
BorderFactory.createEmptyBorder
(
sides.top.gap, sides.left.gap, sides.bottom.gap, sides.right.gap
)
)
);
return target;
}
}
重要なのはパディングを作る仕組み、ではなく、上下左右のパティングの値を決定する仕組みです。メソッドの最初にAllSidesGapを作成している箇所がありますが、このクラスによって上下左右のパディング値を決定します。
public class AllSidesGap
{
public final Left left; // 左側の間隔
public final Top top; // 上部の間隔
public final Right right; // 右側の間隔
public final Bottom bottom; // 下部の間隔
private AllSidesGap(Left left, Top top, Right right, Bottom bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
/**
* 指定された間隔値で {@code AllSidesGap} を生成する。
*
* @param gaps 間隔値(Left, Top, Right, Bottom)
* @return {@code AllSidesGap}
*/
public static AllSidesGap of(Gap... gaps)
{
// 四方のパディング決定
// ※デフォルト値を設定した後、指定された値で上書き
AllSidesGap paddings = defaults();
Left left = paddings.left;
Top top = paddings.top;
Right right = paddings.right;
Bottom bottom = paddings.bottom;
for(Gap gap : gaps)
{
if(gap instanceof Left) left = (Left)gap;
if(gap instanceof Top) top = (Top)gap;
if(gap instanceof Right) right = (Right)gap;
if(gap instanceof Bottom) bottom = (Bottom)gap;
}
return new AllSidesGap(left, top, right, bottom);
}
/**
* デフォルトの間隔値で {@code AllSidesGap} を生成する。
*
* @return {@code AllSidesGap}
*/
public static AllSidesGap defaults()
{
Left left = Left.of(UIDefaults.COMPONENT_GAP);
Top top = Top.of(UIDefaults.COMPONENT_GAP);
Right right = Right.of(UIDefaults.COMPONENT_GAP);
Bottom bottom = Bottom.of(UIDefaults.COMPONENT_GAP);
return new AllSidesGap(left, top, right, bottom);
}
//// ---------- 後略 ---------- ////
AllSidesGap.of(Gap...)を見てください。まず、上下左右のデフォルト値を設定し、可変長引数で指定された箇所のみ上書きします。これで上下左右のパディング値を決定することができました。簡単でしょ?面倒だけど…
現状の実装の問題点としては、同じ個所のパディング値を複数指定してもエラーが発生しないこと。やったとしても何事もなかったかのように後勝ちです。まぁ、良いのではないでしょうか。大目に見ましょう😅
ついでにやっておきましょう。
上記は上下左右のパディング値でしたが、SwiftUIでもそうであるように、パディングの指定は上下左右だけでなく、水平方向/垂直方向の指定も可能にします。
Button.of("牡丹じゃないよ、ボタンだよ")
.padding(Horizontal.of(30));
Horizontal/Verticalというクラスを使って左右、または上下のパディングを指定します。これらのクラスの親クラスとしてSymmetryを作成しました。ちなみに発音は"シンメトリー"ではなく、"シメトリー"です😝
これを基に、Widget#padding(Symmetry...)の定義を追加します。
public interface Widget<T extends JComponent>
{
/**
* パディングの設定をする。
*
* @param gaps 各サイド(left, top, right, bottom)のパディング
* @return 自身のインスタンス
*/
T padding(Gap... gaps);
/**
* 水平, 垂直方向のパディングの設定をする。
*
* @param symmetries 水平, 垂直方向のパディング
* @return 自身のインスタンス
*/
default T padding(Symmetry... symmetries)
{
AllSidesGap sides = AllSidesGap.of(symmetries);
return padding(sides.left, sides.top, sides.right, sides.bottom);
}
//// ---------- 後略 ---------- ////
AllSidesGapにはSymmetry用のメソッドも用意します。しかし、こちらは既に定義済みのWidget#padding(Gap...)を使ってのデフォルト実装です。AllSidesGap.of(Symmetry...)を追加して終わりです。
/**
* 指定された間隔値で {@code AllSidesGap} を生成する。
*
* @param gaps 間隔値(Horizontal, Vertical)
* @return {@code AllSidesGap}
*/
public static AllSidesGap of(Symmetry... symmetries)
{
// 四方のパディング決定
// ※デフォルト値を設定した後、指定された値で上書き
AllSidesGap paddings = defaults();
Left left = paddings.left;
Top top = paddings.top;
Right right = paddings.right;
Bottom bottom = paddings.bottom;
for(Symmetry symmetry : symmetries)
{
if(symmetry instanceof Horizontal)
{
Horizontal horizontal = (Horizontal)symmetry;
left = Left.of(horizontal.gap);
right = Right.of(horizontal.gap);
}
else if(symmetry instanceof Vertical)
{
Vertical vertical = (Vertical)symmetry;
top = Top.of(vertical.gap);
bottom = Bottom.of(vertical.gap);
}
}
return new AllSidesGap(left, top, right, bottom);
}
まぁSymmetryも似たようなもんです。これでとりあえず準備オッケーです。実際に実行してみましょう。
/**
* パディングのパターンをテストする。
*/
private void testPaddingPatterns()
{
Frame.of
(
"SwingUI Padding Sample",
(f) ->
{
f.setResizable(true); // 画面リサイズ可能
f.setSize(400, 300); // 初期画面サイズ指定
},
VStack.of
(
Spacing.of(24),
Spacer.fill(),
Button.of("Padding (←, ↑, →, ↓) : (20, none, 40, 60)")
.padding(Left.of(20), Right.of(40), Bottom.of(60))
.onClicked(self -> showInfoDialog(self, "Padding on all sides")),
Button.of("Padding (Horizontal, Vertical) : (30, none)")
.padding(Horizontal.of(30))
.onClicked(self -> showInfoDialog(self, "Padding on horizontal/vertical sides")),
Spacer.fill()
)
);
}
TopやVerticalが指定されてなくてもデフォルトのパディングがちゃんと取れてますね。願いが叶ったー、と言いつつ名前付き引数と比較したらアレですけど… まぁ良しとしましょう。使う側からしたら、より良くなったハズですから。。。
勢いで他も変えちゃう!
もうこうなったら勢いですからね。同じ問題を持つSpacerもWidget#frame(int, int)もやっちゃいましょう。
以下の説明はクラス名等の見直しにより、書き直しました。
既に書き直し前の説明を読んでしまった方は、記事の最後にある追記(2026.01.01)をご覧ください。
SpacerはwidthOnly(int)とかhightOnly(int)なんて言う、幅or高さのみを指定するのに専用メソッドを作っていたのが問題でした。なので、これをSpacer.of()に一本化します(Spacer.fill()除く)。
これまで列挙型として最大限の幅(もしくは高さ)を表していたUISizeという型がありましたが、これを廃止し、前出のGapのような役割のクラスUILengthを追加します。
子クラスにWidth,Heightを作り、これをSpacer.of()の引数とします。ちなみに最大限を表す定数InfiniteをWidthとHeightのそれぞれに定義し、これまでの列挙型UISize.InfiniteはUISize.Width.Infinite,UISize.Height.Infiniteの定数に変更です。
次に先に出てきたAllSidesGapのように、幅・高さを保持するクラスWxHSizeを作成します。ちなみにWxHは、よく製品のサイズ表記で使われてるアレをマネています。
WxHSize#of()ですが、先のパディングの例AllSidesGapではデフォルト値を自クラスのメソッドで生成できてたので良かったのですが、WxHSizeに関しては、これから対応しようとしているSpacerとWidget#frame()のデフォルト値が異なるため、これらデフォルト値を外部から渡す仕様になっています。
public class WxHSize
{
//// ---------- 中略 ---------- ////
/**
* デフォルト値と指定されたサイズで {@code WxHSize} を生成する。
*
* @param defaults 幅・高さのデフォルト値
* @param lengths 指定されたサイズ
* @return {@code WxHSize}
*/
public static WxHSize of(WxHSize defaults, UILength... lengths)
{
Width width = defaults.width;
Height height = defaults.height;
for(UILength length : lengths)
{
if(length instanceof Width) width = (Width)length;
if(length instanceof Height) height = (Height)length;
}
return new WxHSize(width, height);
}
}
それではSpacerに適用してみます。
public class Spacer
{
/**
* 指定した幅・高さのスペース領域を確保する。
*
* @param lengths 幅・高さのサイズ
* @return {@code PanelWT}
*/
public static PanelWT of(UILength... lengths)
{
// 幅・高さスペースの取得
WxHSize size = WxHSize.of(WxHSize.zero(), lengths);
// 幅・高さスペースの設定
if(isInfinite(size.width) || isInfinite(size.height))
{
// 幅または高さが最大限の場合、柔軟なスペース領域を確保
return flexible(new Dimension(size.width.length, size.height.length));
}
else
{
// 幅・高さに最大限の値を含まない場合、固定のスペース領域を確保
return fixed(new Dimension(size.width.length, size.height.length));
}
}
//// ---------- 後略 ---------- ////
Spacerの場合、デフォルト値は幅・高さ共にゼロなので、SxHSize#zero()を使ってデフォルト値を与えています。これにより、WxHSize.of()は最初に幅・高さのデフォルト値を確保し、第2引数以降で指定されたWidth/Heightの値が、必要に応じてそのデフォルト値を上書きしている、というパディングの時と同じ流れになります。
それではもうひとつ、UILengthを使ってWidget#frame(int, int)をWidget#frame(UILength...)に書き換えです。
public interface Widget<T extends JComponent>
{
//// ---------- 中略 ---------- ////
/**
* 自身のサイズの設定をする。
*
* @param lengths 幅・高さサイズ
* @return 自身のインスタンス
*/
T frame(UILength... lengths);
//// ---------- 後略 ---------- ////
このインターフェースの実装はWidgetを実装しているSwingUI用コンポーネントが対象です。先にもあったよう、ヘルパークラスにお願いです(ここではButtonWTを例に説明)。
public class ButtonWT extends JButton implements Widget<ButtonWT>
{
//// ---------- 中略 ---------- ////
@Override
public ButtonWT frame(UILength... lengths)
{
return WidgetHelper.frame(this, lengths);
}
//// ---------- 後略 ---------- ////
public class WidgetHelper
{
//// ---------- 中略 ---------- ////
/**
* 指定コンポーネントのサイズの設定をする。
*
* @param <T> JComponentの継承クラス
* @param target 対象コンポーネント
* @param lengths 幅・高さサイズ
* @return 対象コンポーネント
*/
public static <T extends JComponent> T frame(T target, UILength... lengths)
{
// 幅・高さ決定
WxHSize defaults = WxHSize.from(target.getPreferredSize());
WxHSize size = WxHSize.of(defaults, lengths);
// サイズ設定
target.setMaximumSize(new Dimension(size.width.length, size.height.length));
target.setMinimumSize(new Dimension(size.width.length, size.height.length));
target.setPreferredSize(new Dimension(size.width.length, size.height.length));
return target;
}
}
こちらの場合、デフォルト値はその対象コンポーネントの"適切な"サイズです。Swingの各コンポーネントは、それぞれの適切なサイズを持っていて、例えばボタンの場合、設定されるボタンの文字列のフォントサイズに応じて算出されます。で、この適切なサイズはJComponent#getPreferredSize()(返り値はDimension)で取得できるので、WxHSize#from(Dimension)を使ってデフォルト値を生成します。ちなみにDimensionはSwingで使われる、幅と高さを保持するクラスです。これでデフォルト値に対し、指定されたWidth/Heightで上書きする、というこれまでの流れと同じになりますね。
それでは、SpacerとWidget#frame(UILength...)を実際に呼び出して確認です。
/**
* {@link Spacer} / {@link Widget#frame(UISize...)} のパターンをテストする。
*/
private void testUISizePatterns()
{
Frame.of
(
"SwingUI Spacer Sample",
(f) ->
{
f.setResizable(true); // 画面リサイズ可能
f.setSize(400, 300); // 初期画面サイズ指定
},
VStack.of
(
Spacer.of(Height.Infinite),
text("── Width / Height Only ──"),
HStack.of
(
Button.of("Width: 100")
.frame(Width.of(100)),
Spacer.of(Width.of(30)),
Button.of("Height: 40")
.frame(Height.of(40))
),
Spacer.of(Height.Infinite),
text("── Fixed & Infinite ──"),
Button.of("Width: Infinite, Height: Fixed")
.frame(Width.Infinite, Height.of(60))
)
.padding(UIDefaults.COMPONENT_GAP)
);
}
SpacerがWidthのみ、HeightのみでもSpacer.of()で対応できていますね。
ところで一番下のボタンですが、その上のSpacerが高さが最大限取っているため、ウィンドウの高さを伸長してもウィンドウの下部に引っ付いています。また、ボタン自身の幅を最大限に設定しているため、ウィンドウの横幅を伸ばしても、その幅に合わせてボタンも伸びます。これでOKですね。
さいごに
今回は長らく後回しにしてきた問題に立ち向かいました。とりあえず当初の目標は達成ですが、実際のところ、やっぱり欲しい名前付き引数機能です。導入したところで問題あるんですかね?過去のバージョンは'名前なし'として新しいJREで対応できるでしょ?ま、SwingUIはJava8以上のサポートを想定しているので関係はないですけどね…
今回のソース一式はこちら
追記 (2026.01.01)
全く元日早々、修正なんてしたくなかったんですが、こればっかりは仕方ありません。
まず、幅と高さを保持するUISizeをUILengthにクラス名を変えました。これはSizeと名付けると、普通、幅と高さの2つの属性を持つクラスを想像するだろう、と考えた結果です。実のところ、属性としては長さだけなんで違和感があったんですね。
それからWxHSizeを追加しました。当初、SpacerやWidget#frame()はデフォルト値が違うし、AllSidesGapのようなクラスは作らず、それぞれの呼び出し側がデフォルト値とその上書き処理をやればイイじゃん、と思ってたんですが、冷静に考えると、それは違うな… と。つまりデフォルト値に対し、上書くロジックが分散するじゃないか…、と。
Don't Repeat Yourself...


