JRootPane 默认按钮在调整大小时未使用 Mac OS X LAF 正确呈现

JRootPane Default Button not Rendered Correctly with Mac OS X LAF when Resized

目标: 使用 mac OS X LAF 创建默认按钮。为此,我在 JFrames JRootPane 上使用了 setDefaultButton(JButton) 方法。

问题: 当我将 JFrame 的大小调整为大于默认按钮的首选大小时,该按钮看起来像一个普通的 JButton。如果 JFrame 返回到其原始大小附近的某个位置,该按钮将恢复为默认按钮。 不会发生在任何其他默认 LAF 中。

示例 GIF

金属: Mac OS X:

问题: 我怎样才能拥有一个带有 Mac OS X LAF 的默认按钮,它可以在不改变外观的情况下调整大小?

代码: A JOptionPane 将要求您选择一个已安装的 LAF。选择一个,将显示带有该 LAF 的默认按钮。

import java.awt.EventQueue;
import java.util.ArrayList;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.UnsupportedLookAndFeelException;

public class ResizingDeafultButtonMCVE {

    public static void main(String[] args) {
        LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
        ArrayList<String> lafNames = new ArrayList<String>();
        for(int i = 0; i < lafs.length; i++) {
            lafNames.add(lafs[i].getName());
        }

        Object option = JOptionPane.showInputDialog(
                null, "Choose a LAF", "", JOptionPane.DEFAULT_OPTION, 
                null, lafNames.toArray(new String[0]), 0);

        if(option == null) {
            System.exit(0);
        }

        try {
            UIManager.setLookAndFeel(UIManager.getInstalledLookAndFeels()
                 [lafNames.indexOf(option)].getClassName());
        } catch (UnsupportedLookAndFeelException | ClassNotFoundException
                | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }

        EventQueue.invokeLater(() -> {
            final JFrame frame = new JFrame((String) option);

            JButton defaultButton = new JButton("Default Button");
            frame.getContentPane().add(defaultButton);
            frame.getRootPane().setDefaultButton(defaultButton);

            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }

}

了解以下示例为何有效的须知事项:

  • 一个AquaButtonBorder绘制了一个由AquaButtonUI
  • 控制的按钮的按钮部分
  • AquaBorder 的边框绘制代码由 AquaPainter 完成。具体来说,AquaSingleImagePainter 通过使用具有给定 JRSUIState.
  • JRSUIControl 将图像绘制到 Graphics2D 上下文中
  • 提供给 AquaSingleImagePainterJRSUIStateJRSUIControl 用来确定原生绘制内容的
  • A JRSUIState 通过 JRSUIState#set(Property)
  • 应用 Propertys 修改
  • OS X 上的默认按钮必须是 push buttonWidget.BUTTON_PUSHProperty
  • 按钮样式 属性 应用于 JRSUIState 用于在 AquaButtonBorder.Dynamic#doButtonPaint(AbstractButton, ButtonModel, Graphics, int, int, int, int)
  • 中绘制图像
  • 按钮的样式 Property (Widget) 由 AquaButtonBorder.Dynamic#getStyleForSize(AbstractButton, Size, int, int)
  • 按钮有最大尺寸
  • 上述方法 returns Widget.BUTTON_BEVEL_ROUND 用于按钮的最大高度以上的按钮(实现为 SizeConstants.fNormalButtonHeight + 3 = 32

因此,要使默认按钮无论大小如何始终显示为默认按钮,getStyleForSize(...) 对于默认按钮必须始终 return Widget.BUTTON_PUSH

解法:

  1. 子类 AquaButtonBorder.Dynamic 并将 getStyleForSize(...) 覆盖为 return Widget.BUTTON_PUSH 如果给定的 AbstractButton 是默认按钮(按钮的JRootPane的默认按钮就是自己),如下图:

    public class AlwaysDefaultAquaButtonBorder extends AquaButtonBorder.Dynamic {
    
        @Override
        protected JRSUIConstants.Widget getStyleForSize(final AbstractButton b, 
                                                    final JRSUIConstants.Size size,
                                                    final int width, 
                                                    final int height) {
            if(b != null && b.getRootPane() != null 
                && Objects.equals(b, b.getRootPane().getDefaultButton())) {
                return JRSUIConstants.Widget.BUTTON_PUSH;
            }
            return super.getStyleForSize(b, size, width, height);
        }
    
    }
    
  2. 将按钮的边框设置为此自定义边框的新实例将其添加到其父组件后,否则边框将重置为UI-AquaButtonUI 的默认值,因为在将其添加到其父级时发生层次结构更改

注意:生成的默认按钮边框不会填充按钮的边界,因为按钮具有最大高度。随着按钮高度的增加,绘制边框的高度将保持不变并垂直居中。

完整示例:

import apple.laf.JRSUIConstants;
import com.apple.laf.AquaButtonBorder;
import java.awt.EventQueue;
import java.util.Objects;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JFrame;

public class AquaAlwaysDefaultButtonExample {

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            final JFrame frame = new JFrame("Aqua Always Default Button Example");
            final JButton defaultButton = new JButton("Default Button");
            frame.getContentPane().add(defaultButton);
            defaultButton.setBorder(new AlwaysDefaultAquaButtonBorder());
            frame.getRootPane().setDefaultButton(defaultButton);
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }

    public static class AlwaysDefaultAquaButtonBorder extends AquaButtonBorder.Dynamic {

        @Override
        protected JRSUIConstants.Widget getStyleForSize(final AbstractButton b,
                                                        final JRSUIConstants.Size size,
                                                        final int width,
                                                        final int height) {
            if(b != null && b.getRootPane() != null
                    && Objects.equals(b, b.getRootPane().getDefaultButton())) {
                return JRSUIConstants.Widget.BUTTON_PUSH;
            }
            return super.getStyleForSize(b, size, width, height);
        }

    }

}