JFileChooserで詳細表示したときにファイル名の列幅が狭くなる問題の対策

Java

以前からどうしようかと悩んでいた問題が解決したのでメモとして。

問題

  • JFileChooserで詳細表示する
  • ディレクトリを移動すると、ファイル名の列幅が狭くなってしまう

正常な表示

ディレクトリ移動などすると、Name列が狭くなる

解析

どうなっているのかをソースでみてみました。この部分でファイル名列の幅を調整しているようです。

jdk17/src/java.desktop/share/classes/sun/swing/FilePane.java at 4afbcaf55383ec2f5da53282a1547bac3d099e9d · openjdk/jdk17
released 2021-09-14 - openjdk/jdk17
    private void fixNameColumnWidth(int viewWidth) {
        TableColumn nameCol = detailsTable.getColumnModel().getColumn(COLUMN_FILENAME);
        int tableWidth = detailsTable.getPreferredSize().width;

        if (tableWidth < viewWidth) {
            nameCol.setPreferredWidth(nameCol.getPreferredWidth() + viewWidth - tableWidth);
        }
    }
jdk17/src/java.desktop/share/classes/sun/swing/FilePane.java at master · openjdk/jdk17
released 2021-09-14 - openjdk/jdk17
        // Adjust width of first column so the table fills the viewport when
        // first displayed (temporary listener).
        scrollpane.addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                JScrollPane sp = (JScrollPane)e.getComponent();
                fixNameColumnWidth(sp.getViewport().getSize().width);
                sp.removeComponentListener(this);
            }
        });

ところが、これが実行されるのは1回のみで、ディレクトリ移動してしまうとJTableを作りなおしてしまうため、幅が狭くなってしまうという現象になっているようです。

ディレクトリ移動すると、setCurrentDirectory() →(省略)→updateDetails()・・・ここでColumeModelを作り直して設定してますが、ファイル名列の幅は未調整になっています。

jdk17/src/java.desktop/share/classes/sun/swing/FilePane.java at 4afbcaf55383ec2f5da53282a1547bac3d099e9d · openjdk/jdk17
released 2021-09-14 - openjdk/jdk17

・・・JDK本体に修正しないといけないの?と思ったところに解決方法がありました。

解決方法

以前さがしたときにはみつからなかったのですが、久々に検索してみると解決策の投稿を発見しました。

Resize column width in JFileChooser
I am using a JFileChooser which allows me to browse to an Excel file. The file chooser appears fine when it opens initia...

ほぼそのままですが、少しだけ変えて下記のようなコードになりました。色付けしたところがその箇所です。

	/**
	 * Detail表示をデフォルトにするファイルChooser
	 */
	private static final class CuFileChooser extends JFileChooser {
		private static final long serialVersionUID = 3081886805862237099L;

		private final JTable detailsTable;

		private CuFileChooser() {
			super();
			viewDetails();
			detailsTable = findChildComponent(this, JTable.class);
		}

		private void viewDetails() {
			var detailsAction = getActionMap().get("viewTypeDetails");
			if (detailsAction != null) {
				detailsAction.actionPerformed(null);
			}
		}

		@Override
		public void updateUI() {
			super.updateUI();
			viewDetails();
		}

		private <T> T findChildComponent(Container container, Class<T> cls) {
			for (var c : container.getComponents()) {
				if (cls.isInstance(c)) {
					return cls.cast(c);
				} else if (c instanceof Container ct) {
					var cc = findChildComponent(ct, cls);
					if (cc != null) {
						return cc;
					}
				}
			}
			return null;
		}

		@Override
		public void setCurrentDirectory(File dir) {
			super.setCurrentDirectory(dir);
			fixNameColumnWidth();
		}

		/**
		 * ディレクトリ移動で、Name列の幅が小さくなってしまう問題の対策.
		 */
		private void fixNameColumnWidth() {
			if (detailsTable != null) {
				int viewWidth = detailsTable.getParent().getSize().width;
				int tableWidth = detailsTable.getPreferredSize().width;
				if (tableWidth < viewWidth) {
					var nameCol = detailsTable.getColumnModel().getColumn(0);
					nameCol.setPreferredWidth(nameCol.getPreferredWidth() + viewWidth - tableWidth);
				}
			}
		}
	}

ただ、、、まだ問題あり

どうやらこれだけだと不足があるようです。

  1. UI変更した場合(ライト→ダーク)、幅修正がされなくなる
  2. リスト表示に変更、ディレクトリ移動、詳細表示に変更。とすると、列幅がおかしくなる(元の現象)
  3. WindowsUIに変更したあと、NullPointerExceptionが発生

最終的にはこんな感じになりました。

  1. fixNameColumnWidth(): JTableのオブジェクトを最初のときからもってしまうと、UI変更したときに追従できないので都度JTableを取得して処理するよう変更。と思いましたが、下記の3で解決。
  2. addViewTypeAction(): PropertyChangeListenerをつかって、詳細表示に変更したときにもファイル名列の修正を行う
  3. UI変更した場合は updateUI() ではなく、オブジェクトから再生成する
/*
 * Copyright (C) 2024 たんらる
 */

package jp.fourthline.mabiicco;

import java.awt.Container;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;

import javax.swing.JFileChooser;
import javax.swing.JTable;

import sun.swing.FilePane;

/**
 * Detail表示をデフォルトにするファイルChooser
 *   - UI変更した場合はオブジェクト生成からやり直すこと.
 */
public final class FixFileChooser extends JFileChooser implements PropertyChangeListener {
	private static final long serialVersionUID = -346484285443111970L;

	private static final String VIEW_TYPE_DETAILS = "viewTypeDetails";
	private static final String VIEW_TYPE = "viewType";

	private final JTable detailsTable;

	public FixFileChooser() {
		super();
		viewDetails();
		detailsTable = findChildComponent(this, JTable.class);

		// リスト表示から詳細表示に変更した場合にも名前列の幅修正を行う.
		addViewTypeAction();
	}

	private void viewDetails() {
		var detailsAction = getActionMap().get(VIEW_TYPE_DETAILS);
		if (detailsAction != null) {
			detailsAction.actionPerformed(null);
		}
	}

	private <T> T findChildComponent(Container container, Class<T> cls) {
		for (var c : container.getComponents()) {
			if (cls.isInstance(c)) {
				return cls.cast(c);
			} else if (c instanceof Container ct) {
				var cc = findChildComponent(ct, cls);
				if (cc != null) {
					return cc;
				}
			}
		}
		return null;
	}

	@Override
	public void setCurrentDirectory(File dir) {
		super.setCurrentDirectory(dir);
		fixNameColumnWidth();
	}

	/**
	 * ディレクトリ移動で、Name列の幅が小さくなってしまう問題の対策.
	 */
	private void fixNameColumnWidth() {
		if (detailsTable != null) {
			int viewWidth = detailsTable.getParent().getSize().width;
			int tableWidth = detailsTable.getPreferredSize().width;
			if (tableWidth < viewWidth) {
				var nameCol = detailsTable.getColumnModel().getColumn(0);
				nameCol.setPreferredWidth(nameCol.getPreferredWidth() + viewWidth - tableWidth);
			}
		}
	}

	private void addViewTypeAction() {
		var filePane = findChildComponent(this, FilePane.class);
		if (filePane != null) {
			filePane.addPropertyChangeListener(VIEW_TYPE, this);
		}
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		// リスト -> 詳細 変更時にも幅調整を行う.
		if (evt.getNewValue().equals(FilePane.VIEWTYPE_DETAILS)) {
			fixNameColumnWidth();
		}
	}
}