You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让JPasswordField在不同平台下渲染长度保持一致?

Swing跨平台应用JPasswordField宽度不一致问题解决

问题描述

在Windows上用Swing开发跨平台桌面应用,登录表单中的自定义JPasswordField组件在Windows显示正常,但在Mac上运行同一份jar包时,密码字段渲染宽度比其他字段宽,需要实现跨平台显示效果一致。

Windows端显示效果:
Windows端密码字段显示效果

Mac端显示效果:
Mac端密码字段显示效果

自定义密码字段代码:

class PasswordTextField : JPasswordField(39) {
    private var hidePassword = true
    private val eyeIcon: SVGDocument
    private val eyeSlashIcon: SVGDocument

    companion object {
        private const val BORDER_RADIUS = 24
        private const val ICON_SIZE = 18.toFloat()
        private const val LABEL_TEXT = "Password"
    }

    init {
        isOpaque = false
        border = EmptyBorder(24, 16, 8, 33)
        foreground = Colors.TEXT_SECONDARY
        caretColor = Colors.TEXT_SECONDARY
        font = Font("Sans Serif", Font.PLAIN, 18)
        eyeIcon = requireNotNull(Icons.getEyeIcon())
        eyeSlashIcon = requireNotNull(Icons.getEyeSlashIcon())

        // 显示图标悬停监听
        addMouseMotionListener(object : MouseMotionAdapter() {
            override fun mouseMoved(e: MouseEvent?) {
                val x = width - 33
                cursor = if (e != null && Rectangle(x, 20, 18, 20).contains(e.point)) {
                    Cursors.HAND
                } else {
                    Cursors.TEXT
                }
            }
        })

        // 显示图标点击监听
        addMouseListener(object : MouseAdapter() {
            override fun mousePressed(e: MouseEvent?) {
                val x = width - 33
                if (e != null && Rectangle(x, 0, 18, 30).contains(e.point)) {
                    hidePassword = !hidePassword
                    echoChar = if (hidePassword) {
                        font = Font("Sans Serif", Font.PLAIN, 18)
                        0x2022.toChar()
                    } else {
                        font = Font("Poppins", Font.PLAIN, 14)
                        0.toChar()
                    }

                    repaint()
                }
            }
        })
    }


    override fun paintComponent(g: Graphics?) {
        val g2 = g as Graphics2D
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        // 绘制组件背景
        g2.color = Colors.TEXTBOX_FILL
        g2.fillRoundRect(0, 0, width, height, BORDER_RADIUS, BORDER_RADIUS)
        createShowHideIcon(g2)

        createLabelText(g2)

        super.paintComponent(g)
    }

    private fun createShowHideIcon(g2: Graphics2D) {
        val x = (width - 30).toFloat()
        val y = ((height - 16) / 2).toFloat()
        if (hidePassword) {
            eyeSlashIcon.render(this, g2, ViewBox(x, y, ICON_SIZE, ICON_SIZE))
        } else {
            eyeIcon.render(this, g2, ViewBox(x, y, ICON_SIZE, ICON_SIZE))
        }
    }

    private fun createLabelText(g2: Graphics2D) {
        g2.color = Colors.TEXT_MAIN
        g2.font = Font("Poppins", Font.PLAIN, 14)
        val ft = g2.fontMetrics
        val r2 = ft.getStringBounds(LABEL_TEXT, g2)

        val height = height.toDouble() - insets.top - insets.bottom
        val textY = ((height - r2.height) / 2.0) - 20
        val size = 2

        g2.drawString(LABEL_TEXT, insets.right - 16, (insets.top + textY + ft.ascent - size).toInt())
    }
}

解决方案

问题核心是不同平台的Swing外观(LaF)对组件尺寸、布局逻辑的默认处理不同,加上自定义属性放大了差异,可通过以下方法解决:

1. 统一组件尺寸,放弃列数依赖

构造函数中的JPasswordField(39)是按字符列数估算宽度,不同平台字体渲染逻辑差异会导致宽度不一致。直接指定固定尺寸或与其他输入组件对齐:

class PasswordTextField : JPasswordField() {
    init {
        // 假设其他输入组件首选宽度为300px,统一设置
        val preferredHeight = super.getPreferredSize().height
        preferredSize = Dimension(300, preferredHeight)
        minimumSize = Dimension(300, preferredHeight)
        maximumSize = Dimension(300, preferredHeight)
        
        // 原有初始化代码...
    }
}

2. 重写尺寸获取方法,强制统一宽度

通过重写getPreferredSize(),忽略平台LaF的自动调整:

override fun getPreferredSize(): Dimension {
    val originalSize = super.getPreferredSize()
    // 固定宽度,保留原有高度计算
    return Dimension(300, originalSize.height)
}

3. 统一字体与边框配置

确保所有输入组件使用完全一致的边框和字体,避免平台默认字体差异:

// 统一边框,和用户名输入框保持相同配置
border = EmptyBorder(24, 16, 8, 33)
// 使用自定义字体并打包进jar,避免依赖平台字体
val customFont = Font.createFont(Font.TRUETYPE_FONT, javaClass.getResourceAsStream("/fonts/Poppins-Regular.ttf"))
font = customFont.deriveFont(18f)

4. 通过布局管理器约束尺寸

如果使用布局管理器(如GridBagLayout),直接通过布局约束固定宽度,不依赖组件自身尺寸:

val gbc = GridBagConstraints()
gbc.gridx = 0
gbc.gridy = 1
gbc.fill = GridBagConstraints.HORIZONTAL
gbc.weightx = 0.0 // 禁止拉伸
gbc.ipadx = 300 // 固定宽度
container.add(passwordField, gbc)

内容的提问来源于stack exchange,提问作者ishaangupte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 22:37:12