はじめに
一時期Java, Kotlinを利用したクライアントアプリ開発をやってましたが、UIフレームワークがSwingでした。
Swingは昔のUIフレームワークなので、今っぽい見た目を作るためにはJFrameをundecoratedにしてUIを自前でカスタマイズしていく必要があります。(intellijもそうしているみたいです。)
ただ、undecoratedにすると、同時にさまざまなウインドウ周辺の機能が落ち、単純な見た目のカスタマイズ以上の対応をしなくてはならないので今回は落ちた機能の再実装という苦行をやっていきます。
適当にアプリを作る
とりあえずそれっぽい業務アプリがあれば良いので、claude 3.7 sonnetに作ってもらいました。
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.*;
import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
private JFrame frame;
private JPanel sidebarPanel;
private JPanel mainPanel;
private JLabel statusLabel;
private JTable dataTable;
private DefaultTableModel tableModel;
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
// Set look and feel to system
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
new Main();
});
}
public Main() {
initialize();
}
private void initialize() {
// Create main frame
frame = new JFrame("業務管理システム");
frame.setSize(1200, 800);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
// Create menu bar
createMenuBar();
// Create toolbar
createToolBar();
// Create main content area with split pane
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setDividerLocation(250);
splitPane.setDividerSize(5);
// Create sidebar
createSidebar();
splitPane.setLeftComponent(sidebarPanel);
// Create main content
createMainContent();
splitPane.setRightComponent(mainPanel);
frame.getContentPane().add(splitPane, BorderLayout.CENTER);
// Create status bar
createStatusBar();
// Start status bar timer
startStatusBarTimer();
frame.setVisible(true);
}
private void createMenuBar() {
JMenuBar menuBar = new JMenuBar();
// File menu
JMenu fileMenu = new JMenu("ファイル");
fileMenu.add(new JMenuItem("新規作成"));
JMenuItem openItem = new JMenuItem("開く");
openItem.addActionListener(e -> openFileDialog());
fileMenu.add(openItem);
fileMenu.add(new JMenuItem("保存"));
fileMenu.addSeparator();
JMenuItem exitItem = new JMenuItem("終了");
exitItem.addActionListener(e -> System.exit(0));
fileMenu.add(exitItem);
// Edit menu
JMenu editMenu = new JMenu("編集");
editMenu.add(new JMenuItem("切り取り"));
editMenu.add(new JMenuItem("コピー"));
editMenu.add(new JMenuItem("貼り付け"));
// View menu
JMenu viewMenu = new JMenu("表示");
viewMenu.add(new JMenuItem("詳細"));
viewMenu.add(new JMenuItem("概要"));
// Tools menu
JMenu toolsMenu = new JMenu("ツール");
toolsMenu.add(new JMenuItem("設定"));
toolsMenu.add(new JMenuItem("カスタマイズ"));
// Help menu
JMenu helpMenu = new JMenu("ヘルプ");
helpMenu.add(new JMenuItem("ヘルプトピック"));
helpMenu.add(new JMenuItem("バージョン情報"));
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(viewMenu);
menuBar.add(toolsMenu);
menuBar.add(helpMenu);
frame.setJMenuBar(menuBar);
}
private void createToolBar() {
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
JButton newButton = new JButton("新規");
newButton.setFocusable(false);
toolBar.add(newButton);
JButton openButton = new JButton("開く");
openButton.setFocusable(false);
openButton.addActionListener(e -> openFileDialog());
toolBar.add(openButton);
JButton saveButton = new JButton("保存");
saveButton.setFocusable(false);
toolBar.add(saveButton);
toolBar.addSeparator();
JButton searchButton = new JButton("検索");
searchButton.setFocusable(false);
toolBar.add(searchButton);
JTextField searchField = new JTextField(20);
toolBar.add(searchField);
toolBar.addSeparator();
JButton reportButton = new JButton("レポート");
reportButton.setFocusable(false);
toolBar.add(reportButton);
JButton settingsButton = new JButton("設定");
settingsButton.setFocusable(false);
toolBar.add(settingsButton);
frame.getContentPane().add(toolBar, BorderLayout.NORTH);
}
private void createSidebar() {
sidebarPanel = new JPanel(new BorderLayout());
sidebarPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, Color.GRAY),
BorderFactory.createEmptyBorder(5, 5, 5, 5)));
// Create a label at the top of the sidebar
JLabel sidebarTitle = new JLabel("メニュー");
sidebarTitle.setFont(new Font("Dialog", Font.BOLD, 14));
sidebarTitle.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
sidebarPanel.add(sidebarTitle, BorderLayout.NORTH);
// Create a list of options
String[] options = {"ダッシュボード", "顧客管理", "売上管理", "製品管理", "在庫管理", "社員管理", "設定"};
JList<String> optionList = new JList<>(options);
optionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
optionList.setSelectedIndex(0);
optionList.setFont(new Font("Dialog", Font.PLAIN, 14));
// Add selection listener
optionList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
String selected = optionList.getSelectedValue();
updateMainContent(selected);
}
});
JScrollPane listScrollPane = new JScrollPane(optionList);
listScrollPane.setBorder(null);
sidebarPanel.add(listScrollPane, BorderLayout.CENTER);
}
private void createMainContent() {
mainPanel = new JPanel(new BorderLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Create a header panel
JPanel headerPanel = new JPanel(new BorderLayout());
JLabel titleLabel = new JLabel("顧客管理");
titleLabel.setFont(new Font("Dialog", Font.BOLD, 18));
headerPanel.add(titleLabel, BorderLayout.WEST);
// Add some action buttons to header
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
actionPanel.add(new JButton("新規登録"));
actionPanel.add(new JButton("一括編集"));
actionPanel.add(new JButton("エクスポート"));
headerPanel.add(actionPanel, BorderLayout.EAST);
headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
mainPanel.add(headerPanel, BorderLayout.NORTH);
// Create table for data display
createDataTable();
mainPanel.add(new JScrollPane(dataTable), BorderLayout.CENTER);
// Create pagination panel
JPanel paginationPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
paginationPanel.add(new JButton("<<"));
paginationPanel.add(new JButton("<"));
paginationPanel.add(new JLabel("1 / 10 ページ"));
paginationPanel.add(new JButton(">"));
paginationPanel.add(new JButton(">>"));
paginationPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
mainPanel.add(paginationPanel, BorderLayout.SOUTH);
}
private void createDataTable() {
String[] columnNames = {"ID", "顧客名", "会社名", "電話番号", "メール", "登録日", "最終購入日", "ステータス"};
// Sample data
Object[][] data = {
{"1001", "田中 太郎", "株式会社タナカ", "03-1234-5678", "tanaka@example.com", "2024-01-15", "2024-03-05", "アクティブ"},
{"1002", "佐藤 花子", "佐藤商事", "03-2345-6789", "sato@example.com", "2024-01-20", "2024-02-28", "アクティブ"},
{"1003", "鈴木 一郎", "鈴木工業", "03-3456-7890", "suzuki@example.com", "2024-01-25", "2024-03-01", "休眠"},
{"1004", "高橋 直子", "高橋建設", "03-4567-8901", "takahashi@example.com", "2024-02-01", "2024-02-15", "アクティブ"},
{"1005", "渡辺 健太", "渡辺製作所", "03-5678-9012", "watanabe@example.com", "2024-02-05", "2024-03-02", "アクティブ"},
{"1006", "伊藤 美加", "伊藤商店", "03-6789-0123", "ito@example.com", "2024-02-10", "2024-02-20", "休眠"},
{"1007", "山本 龍太郎", "山本電機", "03-7890-1234", "yamamoto@example.com", "2024-02-15", "", "新規"},
{"1008", "中村 由美", "中村食品", "03-8901-2345", "nakamura@example.com", "2024-02-20", "2024-03-10", "アクティブ"},
{"1009", "小林 俊介", "小林商事", "03-9012-3456", "kobayashi@example.com", "2024-02-25", "2024-03-08", "アクティブ"},
{"1010", "加藤 恵", "加藤工業", "03-0123-4567", "kato@example.com", "2024-03-01", "", "新規"}
};
tableModel = new DefaultTableModel(data, columnNames) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // Make table non-editable
}
};
dataTable = new JTable(tableModel);
dataTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
dataTable.setRowHeight(25);
dataTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
dataTable.getTableHeader().setReorderingAllowed(false);
// Add a row selection listener
dataTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting() && dataTable.getSelectedRow() != -1) {
int row = dataTable.getSelectedRow();
String customerId = (String) dataTable.getValueAt(row, 0);
String customerName = (String) dataTable.getValueAt(row, 1);
statusLabel.setText("選択: " + customerId + " - " + customerName);
}
});
}
private void createStatusBar() {
JPanel statusPanel = new JPanel(new BorderLayout());
statusPanel.setBorder(new CompoundBorder(
new MatteBorder(1, 0, 0, 0, Color.GRAY),
new EmptyBorder(3, 5, 3, 5)));
// Status message on the left
statusLabel = new JLabel("準備完了");
statusPanel.add(statusLabel, BorderLayout.WEST);
// Current date and time on the right
JLabel timeLabel = new JLabel();
updateTimeLabel(timeLabel);
statusPanel.add(timeLabel, BorderLayout.EAST);
frame.getContentPane().add(statusPanel, BorderLayout.SOUTH);
}
private void updateTimeLabel(JLabel timeLabel) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
timeLabel.setText(sdf.format(new Date()));
}
private void startStatusBarTimer() {
Timer timer = new Timer(1000, e -> {
JLabel timeLabel = (JLabel) ((JPanel) frame.getContentPane().getComponent(2)).getComponent(1);
updateTimeLabel(timeLabel);
});
timer.start();
}
/**
* ファイルダイアログを開く
*/
private void openFileDialog() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle("ファイルを開く");
// ファイルフィルターの設定(例:テキストファイルのみ)
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"テキストファイル (*.txt)", "txt");
fileChooser.addChoosableFileFilter(filter);
// すべてのファイルも選択可能に
fileChooser.setAcceptAllFileFilterUsed(true);
int result = fileChooser.showOpenDialog(frame);
if (result == JFileChooser.APPROVE_OPTION) {
// 選択されたファイル
java.io.File selectedFile = fileChooser.getSelectedFile();
// ファイルの処理(例:状態バーに表示)
statusLabel.setText("選択されたファイル: " + selectedFile.getAbsolutePath());
// ここにファイルを開く処理を追加
// 例: readFile(selectedFile);
}
}
private void updateMainContent(String selected) {
JLabel titleLabel = (JLabel) ((JPanel) mainPanel.getComponent(0)).getComponent(0);
titleLabel.setText(selected);
statusLabel.setText(selected + "を表示しています");
}
}
見た目はこんな感じです(macで見ているのでmacらしいデフォルトUIになっています。)
もっとリッチな見た目にするためにundecoratedにしよう。
このアプリの見た目をデザイナーのデザインに合わせるため、下記のようなことをしたい状況になったとします。
- タイトルバーは会社のイメージカラーのグラデーションにしたい
- 縮小、拡大、閉じるボタンをマルチプラットフォームでも同じ操作できるようにしたいので、windowsに合わせて右側にする
- ウインドウ自体を背景透過させたい
- 円形や多角形のウインドウにしたい
- ウインドウの周りの影を消したい
こうなると、デフォルトのJFrameでは対応できず、undecoratedにする必要があります。
undecoratedをつけてみるとどうなるか?
どこがどう変わったか?
タイトルバー消失
まず見た目でわかる通り、タイトルバーが消失しました。
タイトルバーが消失したということは、閉じる、拡大、縮小ボタンも消失しています。
タイトルバーを作ってもダブルクリックで拡大・縮小する動きも自分で作ってあげる必要があります。
ただタイトルバーをカスタマイズしたかっただけなのに...
ウインドウが四角い(Mac)
ドラッグしてウインドウを移動させられない
どれだけ引っ張っても全く動かなくてやばい
ウインドウの拡大縮小ができない
端っこを触ってもカーソルが↔︎にならない...
Windowsボタン+矢印キーでのウインドウ移動ができない(Windows)
今回はMacなので無視したいが、こういう問題がある
影が消える(Windows)
今回はMacでしかみてないからわからんけど多分Windowsだとウインドウ周辺の影も消えてる。
(これはまぁ一旦無視する)
...他にもあるかもしれませんが、ざっと挙げただけでもこんな問題があります。
今回はmac側の問題を解決しつつ、見た目をいい感じにしましょう。
落ちた機能を再実装する
タイトルバーを生やす
initializeの中で、下記のように記述してタイトルバーを実装しましょう。
JPanel titleBar = createCustomTitleBar();
createCustomTitleBar
関数を作ります。
private JPanel createCustomTitleBar() {
// タイトルバーパネルの作成
JPanel titleBar = new JPanel();
titleBar.setLayout(new BorderLayout());
titleBar.setBackground(new Color(51, 102, 153)); // 濃い青色
titleBar.setPreferredSize(new Dimension(frame.getWidth(), 30));
// タイトルラベル(左側)
JLabel titleLabel = new JLabel(" 業務管理システム");
titleLabel.setForeground(Color.WHITE);
titleLabel.setFont(new Font("Dialog", Font.BOLD, 14));
titleBar.add(titleLabel, BorderLayout.WEST);
// コントロールボタンパネル(右側)
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
controlPanel.setOpaque(false);
titleBar.add(controlPanel, BorderLayout.EAST);
// 作成したタイトルバーを返す
return titleBar;
}
ここまででビルドしてみるとこうなりました。
...なんか違いますね
メニューバーとタイトルバーが逆になってる問題を修正する
JFrameにsetJMenuBarでメニューバーを登録すると、必ず最も上のエリアに表示されてしまうので、
タイトルバーより下に表示するためにはメニューバーをJPanelに入れる必要があります。
下記のようなコードに修正します。
//frame.setJMenuBar(menuBar);
JPanel menuPanel = new JPanel(new BorderLayout());
menuPanel.add(menuBar, BorderLayout.NORTH);
拡大、縮小、閉じるボタンを追加する
拡大、縮小、閉じるボタンがタイトルバーとともに消えたのでこれも自分で作らなければいけません。
こんな感じでcreateCustomTitleBar
に入れて作る
// 最小化ボタン
JButton minimizeButton = createTitleBarButton("ー", new Color(204, 51, 51), e -> {
frame.setState(Frame.ICONIFIED);
});
// 最大化ボタン
JButton maximizeButton = createTitleBarButton("◻︎", new Color(204, 51, 51), e -> {
// 最大化/通常サイズの切り替え
if ((frame.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
frame.setExtendedState(frame.getExtendedState() | Frame.MAXIMIZED_BOTH);
} else {
frame.setExtendedState(frame.getExtendedState() & ~Frame.MAXIMIZED_BOTH);
}
});
// 閉じるボタン
JButton closeButton = createTitleBarButton("×", new Color(204, 51, 51), e -> {
frame.dispose();
System.exit(0);
});
controlPanel.add(minimizeButton);
controlPanel.add(maximizeButton);
controlPanel.add(closeButton);
拡大と縮小はframeのstateで取れているし、閉じるボタンもちゃんと動くようになりました。
※もしWindows対応で影を周りに入れる場合は拡大時に影分は計算しないようにして拡大しないと中途半端なサイズになるので注意
タイトルバーをドラッグしてウインドウを動かせるようにする
タイトルバーをドラッグして移動させる機能も落ちたので復活させましょう。
下記の記事を参考にした
こんな感じでMoveListener.java
を作ります。
public class MoveListener implements MouseListener, MouseMotionListener {
private Point pressedPoint;
private Rectangle frameBounds;
private Date lastTimeStamp;
@Override
public void mouseClicked(MouseEvent event) {
}
@Override
public void mousePressed(MouseEvent event) {
this.frameBounds = frame.getBounds();
this.pressedPoint = event.getPoint();
this.lastTimeStamp = new Date();
}
@Override
public void mouseReleased(MouseEvent event) {
moveJFrame(event);
}
@Override
public void mouseEntered(MouseEvent event) {
}
@Override
public void mouseExited(MouseEvent event) {
}
@Override
public void mouseDragged(MouseEvent event) {
moveJFrame(event);
}
@Override
public void mouseMoved(MouseEvent event) {
}
private void moveJFrame(MouseEvent event) {
Point endPoint = event.getPoint();
int xDiff = endPoint.x - pressedPoint.x;
int yDiff = endPoint.y - pressedPoint.y;
Date timestamp = new Date();
// One move action per 60ms to avoid frame glitching
if (Math.abs(timestamp.getTime() - lastTimeStamp.getTime()) > 60) {
if ((xDiff > 0 || yDiff > 0) || (xDiff < 0 || yDiff < 0)) {
frameBounds.x += xDiff;
frameBounds.y += yDiff;
System.out.println(frameBounds);
frame.setBounds(frameBounds);
}
this.lastTimeStamp = timestamp;
}
}
}
titleBarにイベントリスナーを追加します。
MoveListener listener = new MoveListener();
titleBar.addMouseListener(listener);
titleBar.addMouseMotionListener(listener);
これでドラッグして動かせるようになりました。
ただ、スクリーン間を移動する時、すごくガタガタした動きになってしまっています。
(解像度の違うスクリーン間移動時のスケールなどの細かいところを詰められていないせいだと思われます...)
うまいこと解決する手段を忘れてしまったので記事内での解決は諦めますが、intellij IDEAのこの辺のコードが参考になるはず...
タイトルバーダブルクリックで最大化↔︎標準サイズにする機能
ウインドウをダブルクリックして拡大/縮小する機能も無くなったのでつけましょう。
MoveListener
内でクリックイベントを付け加えてあげればOKです。
@Override
public void mouseClicked(MouseEvent event) {
// ダブルクリックの検出
if (event.getClickCount() == 2) {
// 最大化/通常サイズの切り替え
if ((frame.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
frame.setExtendedState(frame.getExtendedState() | Frame.MAXIMIZED_BOTH);
} else {
frame.setExtendedState(frame.getExtendedState() & ~Frame.MAXIMIZED_BOTH);
}
}
}
ウインドウの端をドラッグして拡大・縮小できるようにする
クリック領域を作るためにJFrame全体に小さいドラッグ用の領域を作ってあげて、その領域をドラッグして拡大・縮小できるようにしましょう。
まず、リサイズ用のResizeListener.java
を作成します。
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class ResizeListener implements MouseListener, MouseMotionListener {
private static final int RESIZE_BORDER_WIDTH = 5; // リサイズ用ボーダーの幅
private JFrame frame;
private int cursor;
private Point startPoint;
private Rectangle originalBounds;
public ResizeListener(JFrame frame) {
this.frame = frame;
this.cursor = Cursor.DEFAULT_CURSOR;
}
// フレームの枠を描画するためのメソッド
public static void drawResizableBorder(JFrame frame) {
JRootPane rootPane = frame.getRootPane();
rootPane.setBorder(BorderFactory.createLineBorder(new Color(70, 130, 180), RESIZE_BORDER_WIDTH));
}
// どの領域にマウスがあるかを判定し、カーソルを設定
private int detectCursor(Point point) {
int width = frame.getWidth();
int height = frame.getHeight();
// 左上隅
if (point.x <= RESIZE_BORDER_WIDTH && point.y <= RESIZE_BORDER_WIDTH) {
return Cursor.NW_RESIZE_CURSOR;
}
// 右上隅
else if (point.x >= width - RESIZE_BORDER_WIDTH && point.y <= RESIZE_BORDER_WIDTH) {
return Cursor.NE_RESIZE_CURSOR;
}
// 左下隅
else if (point.x <= RESIZE_BORDER_WIDTH && point.y >= height - RESIZE_BORDER_WIDTH) {
return Cursor.SW_RESIZE_CURSOR;
}
// 右下隅
else if (point.x >= width - RESIZE_BORDER_WIDTH && point.y >= height - RESIZE_BORDER_WIDTH) {
return Cursor.SE_RESIZE_CURSOR;
}
// 上辺
else if (point.y <= RESIZE_BORDER_WIDTH) {
return Cursor.N_RESIZE_CURSOR;
}
// 下辺
else if (point.y >= height - RESIZE_BORDER_WIDTH) {
return Cursor.S_RESIZE_CURSOR;
}
// 左辺
else if (point.x <= RESIZE_BORDER_WIDTH) {
return Cursor.W_RESIZE_CURSOR;
}
// 右辺
else if (point.x >= width - RESIZE_BORDER_WIDTH) {
return Cursor.E_RESIZE_CURSOR;
}
// 枠外
else {
return Cursor.DEFAULT_CURSOR;
}
}
// フレームをリサイズするメソッド
private void resizeFrame(MouseEvent e) {
if (startPoint == null) return;
// 縦横同時に変更するときの動きを改善
// setBounds を使わずに別々に操作する
Point currentPoint = e.getPoint();
int dx = currentPoint.x - startPoint.x;
int dy = currentPoint.y - startPoint.y;
// 元の値を取得
int x = frame.getX();
int y = frame.getY();
int width = frame.getWidth();
int height = frame.getHeight();
// 最小サイズ
int minWidth = 300;
int minHeight = 200;
boolean changed = false;
switch (cursor) {
case Cursor.N_RESIZE_CURSOR: // 上
if (height - dy >= minHeight) {
frame.setLocation(x, y + dy);
frame.setSize(width, height - dy);
changed = true;
}
break;
case Cursor.S_RESIZE_CURSOR: // 下
if (height + dy >= minHeight) {
frame.setSize(width, height + dy);
changed = true;
}
break;
case Cursor.W_RESIZE_CURSOR: // 左
if (width - dx >= minWidth) {
frame.setLocation(x + dx, y);
frame.setSize(width - dx, height);
changed = true;
}
break;
case Cursor.E_RESIZE_CURSOR: // 右
if (width + dx >= minWidth) {
frame.setSize(width + dx, height);
changed = true;
}
break;
case Cursor.NW_RESIZE_CURSOR: // 左上
if (width - dx >= minWidth && height - dy >= minHeight) {
frame.setLocation(x + dx, y + dy);
frame.setSize(width - dx, height - dy);
changed = true;
} else if (width - dx >= minWidth) {
// 幅のみ変更可能な場合
frame.setLocation(x + dx, y);
frame.setSize(width - dx, height);
changed = true;
} else if (height - dy >= minHeight) {
// 高さのみ変更可能な場合
frame.setLocation(x, y + dy);
frame.setSize(width, height - dy);
changed = true;
}
break;
case Cursor.NE_RESIZE_CURSOR: // 右上
if (width + dx >= minWidth && height - dy >= minHeight) {
frame.setLocation(x, y + dy);
frame.setSize(width + dx, height - dy);
changed = true;
} else if (width + dx >= minWidth) {
// 幅のみ変更可能な場合
frame.setSize(width + dx, height);
changed = true;
} else if (height - dy >= minHeight) {
// 高さのみ変更可能な場合
frame.setLocation(x, y + dy);
frame.setSize(width, height - dy);
changed = true;
}
break;
case Cursor.SW_RESIZE_CURSOR: // 左下
if (width - dx >= minWidth && height + dy >= minHeight) {
frame.setLocation(x + dx, y);
frame.setSize(width - dx, height + dy);
changed = true;
} else if (width - dx >= minWidth) {
// 幅のみ変更可能な場合
frame.setLocation(x + dx, y);
frame.setSize(width - dx, height);
changed = true;
} else if (height + dy >= minHeight) {
// 高さのみ変更可能な場合
frame.setSize(width, height + dy);
changed = true;
}
break;
case Cursor.SE_RESIZE_CURSOR: // 右下
if (width + dx >= minWidth && height + dy >= minHeight) {
frame.setSize(width + dx, height + dy);
changed = true;
} else if (width + dx >= minWidth) {
// 幅のみ変更可能な場合
frame.setSize(width + dx, height);
changed = true;
} else if (height + dy >= minHeight) {
// 高さのみ変更可能な場合
frame.setSize(width, height + dy);
changed = true;
}
break;
}
// 変更した場合はスタートポイントを更新
if (changed) {
startPoint = currentPoint;
originalBounds = frame.getBounds();
}
}
@Override
public void mousePressed(MouseEvent e) {
cursor = detectCursor(e.getPoint());
if (cursor != Cursor.DEFAULT_CURSOR) {
startPoint = e.getPoint();
originalBounds = frame.getBounds();
}
}
@Override
public void mouseReleased(MouseEvent e) {
startPoint = null;
originalBounds = null;
}
@Override
public void mouseDragged(MouseEvent e) {
if (cursor != Cursor.DEFAULT_CURSOR) {
resizeFrame(e);
}
}
@Override
public void mouseMoved(MouseEvent e) {
// カーソルの形状を更新
int newCursor = detectCursor(e.getPoint());
if (newCursor != cursor) {
cursor = newCursor;
frame.setCursor(Cursor.getPredefinedCursor(cursor));
}
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {
// フレームから出た時はカーソルをデフォルトに戻す
frame.setCursor(Cursor.getDefaultCursor());
}
}
Main.java
でリサイズリスナーを呼び出したり、枠を作ったりします。
// フレームの枠を作成(リサイズ用)
ResizeListener.drawResizableBorder(frame);
// リサイズリスナーを追加
ResizeListener resizeListener = new ResizeListener(frame);
frame.addMouseListener(resizeListener);
frame.addMouseMotionListener(resizeListener);
こちらもガクガクとした動きになってしまっているので調整は必要ですが、機能としてはできました。
ウインドウの端を丸くする
ウインドウの端を丸くするために、下記のようにMain.java内のボーダー描画のコードを修正します。
/**
* Macスタイルの丸い角をウィンドウに適用するメソッド
* @param frame 丸い角を適用するJFrame
* @param arcSize 角の丸みの大きさ(ピクセル単位)
*/
private void setRoundedCorners(JFrame frame, int arcSize) {
// 丸い角の形状を設定
frame.setShape(new java.awt.geom.RoundRectangle2D.Double(
0, 0, frame.getWidth(), frame.getHeight(), arcSize, arcSize
));
// ウィンドウサイズが変更されたときに丸い角を更新するリスナーを追加
frame.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
// サイズ変更時に形状を更新
frame.setShape(new java.awt.geom.RoundRectangle2D.Double(
0, 0, frame.getWidth(), frame.getHeight(), arcSize, arcSize
));
}
});
}
その他
ファイルダイアログをネイティブと合わせる。
Swingはマルチプラットフォーム対応の関係上、最新のファイルダイアログと同じ見た目を完全には追えていないです。
ダサいのでちゃんとネイティブに合わせたいと思います。
macだったら下記のようにAWTのFileDialogを利用することで良い感じになりますが、
Windows側のAWTのFileDialogはクソダサいです...
private void openFileDialog() {
FileDialog fileDialog = new FileDialog(frame, "ファイルを開く", FileDialog.LOAD);
fileDialog.setFilenameFilter((dir, name) -> name.endsWith(".txt") || name.endsWith(".csv"));
fileDialog.setVisible(true);
// 選択されたファイルを取得
if (fileDialog.getFile() != null) {
String selectedFilePath = fileDialog.getDirectory() + fileDialog.getFile();
// ファイルの処理(例:状態バーに表示)
statusLabel.setText("選択されたファイル: " + selectedFilePath);
// ここにファイルを開く処理を追加
// 例: readFile(new File(selectedFilePath));
}
}
Windowsでの見た目:
この記事にあるような見た目になります。
なので、Windowsでも良い感じの見た目にするためにJavaFXのファイルダイアログを局所的に利用する必要があります。
javafxのライブラリのimportを行う。
import javafx.stage.FileChooser;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.stage.Stage;
openFileDialogを修正
private void openFileDialog() {
// JavaFX環境を初期化
// JFXPanelを作成するだけでJavaFXツールキットが初期化される
final JFXPanel fxPanel = new JFXPanel();
// JavaFXのスレッドでファイルダイアログを実行
final java.io.File[] selectedFileRef = new java.io.File[1];
// 同期的に実行するためのセマフォ
final Object lock = new Object();
Platform.runLater(() -> {
try {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("ファイルを開く");
// フィルターを設定
FileChooser.ExtensionFilter txtFilter = new FileChooser.ExtensionFilter("テキストファイル", "*.txt");
FileChooser.ExtensionFilter csvFilter = new FileChooser.ExtensionFilter("CSVファイル", "*.csv");
fileChooser.getExtensionFilters().addAll(txtFilter, csvFilter);
// 新しいStageを作成してダイアログを表示
Stage stage = new Stage();
selectedFileRef[0] = fileChooser.showOpenDialog(stage);
} finally {
// 処理が完了したことを通知
synchronized (lock) {
lock.notify();
}
}
});
// ファイル選択が完了するまで待機
synchronized (lock) {
try {
lock.wait(10000); // タイムアウトは10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 選択されたファイルを処理
java.io.File selectedFile = selectedFileRef[0];
if (selectedFile != null) {
String selectedFilePath = selectedFile.getAbsolutePath();
// ファイルの処理(例:状態バーに表示)
statusLabel.setText("選択されたファイル: " + selectedFilePath);
// ここにファイルを開く処理を追加
// 例: readFile(selectedFile);
}
}
ビルドしてファイルダイアログ開いたらこうなりました。
MacなのでAWTと見た目は変化ないですが、Windowsで見たときは良い感じになっているはずです。
記事を書いた所感
現代のJavaの開発でSwingを使うことは流石にもうあまりないとは思いますが、
とっかかりの説明はできたので誰かの役に立てばと思います。
ただ、自分でも実装方法複雑すぎて忘れている部分があるので、そこは反省です。
今回作ったリポジトリ
maven使わずに直でライブラリ取り込んで作ってるので、各々の環境に合わせて修正してください
https://github.com/neletolus/java-swing-undecorated
20250313追記:
拘らなければFlatLafで良いかもしれない
Swingでは見た目や振る舞いを定義するデザインテンプレートが"Look and Feel"として提供されています。
もちろん2000年代で更新が終わっている古くてダサいLook and Feelが大多数ですが、
近年になって作成された"FlatLaf"というLook and Feelがあります。
導入するだけで良い感じの見た目になってくれるので、デザイン通りの見た目への拘りがなければ、これで良いかもしれません。