怀念经典,一步步开发一个ttplayer千千静听,不规则窗口处理

image

image

SkinWindow 不规则窗口处理详细解析

SkinWindow 是千千静听所有皮肤窗口的基类,它实现了基于背景图片的不规则窗口(又称异形窗口)的核心机制。不规则窗口允许窗口形状与背景图片的轮廓一致,透明区域(通常是洋红色)会被镂空,并且鼠标点击透明区域时事件会穿透到底层窗口。这种效果在经典播放器皮肤中非常常见。

下面从多个技术层面详细剖析 SkinWindow 如何实现不规则窗口。


1. 窗口基础设置:透明与无边框

setUndecorated(true);
setBackground(new Color(0, 0, 0, 0));
  • setUndecorated(true):移除窗口的标题栏、边框等系统装饰,使窗口完全由我们自己绘制。
  • setBackground(new Color(0, 0, 0, 0)):设置窗口背景完全透明(Alpha = 0)。这一步是 Swing 实现透明窗口的关键,配合后续的自定义绘制,才能让非透明区域显示出来,透明区域完全透空。

2. 透明色镂空(Color Keying)

皮肤图片通常使用一种特定颜色(如洋红 #FF00FF)作为透明标记。decodeBmp 方法负责将 BMP 数据解码为 BufferedImage,并将所有匹配透明色的像素转换为完全透明(Alpha = 0)。

public static BufferedImage decodeBmp(byte[] data, Color transp) {
    // 尝试用 ImageIO 解码,然后手动替换透明色
    // 如果 ImageIO 失败,则手动解析 BMP 格式
}

2.1 手动解析 BMP 时的透明处理

  • 对于 8位索引色:将调色板中等于透明色的索引对应的 ARGB 设置为 0x00FF00FF(完全透明)。
  • 对于 24位真彩色:逐像素检查 RGB,若匹配透明色则设置 Alpha = 0,否则设置为完全不透明。
  • 对于 32位带 Alpha 通道:直接修改 Alpha 通道,若 RGB 匹配透明色则 Alpha 置 0。

2.2 使用 ImageIO 时的透明处理

  • 先用 ImageIO.read() 读取图片。
  • 扫描图片左上角区域(50x50),检测是否存在透明色像素。
  • 若存在,则新建一个 TYPE_INT_ARGB 图像,逐个像素复制,并将匹配透明色的像素 Alpha 设为 0。

这样得到的 bgImage 已经包含了正确的透明信息,后续绘制时透明区域不会覆盖窗口背景,实现镂空效果。


3. 九宫格拉伸与背景绘制

当窗口大小变化时,背景图片不能简单拉伸(会导致边框变形),因此采用九宫格(9-patch)绘制方式。SkinWindow 从 XML 中读取 resize_rect 属性,定义四个边距(左、上、右、下),将图片分为 9 个区域:

  • 四个角(不拉伸)
  • 四条边(单向拉伸)
  • 中心区域(双向拉伸)

drawNinePatch 方法实现这一绘制:

public static void drawNinePatch(Graphics2D g, BufferedImage src,
                                  int dstW, int dstH,
                                  int nl, int nt, int nr, int nb) {
    // 分别绘制 9 个区域
    // 每个区域使用 drawImage 的源矩形和目标矩形参数进行拉伸
}

其中 nl, nt, nr, nb 分别表示左、上、右、下边距(从图片边缘到可拉伸区域的距离)。绘制时,根据当前窗口的宽高计算目标矩形,实现背景的无失真缩放。

背景面板 BgPanel 重写 paintComponent,调用 drawNinePatch 绘制背景,同时设置 setOpaque(false) 使面板本身透明,以便透明区域能真正透出。


4. 窗口形状设置(Region / Shape)

虽然 SkinWindow 本身没有实现 updateWindowShape(该方法为空),但子类(如 MainWindow)通常会覆盖它,根据背景图片的非透明区域构建一个 Shape(如 AreaPath2D),然后通过 setShape(Shape) 方法设置窗口轮廓。

setShape 是 Java 6 引入的 java.awt.Window 方法,它定义了窗口的几何形状,形状之外的部分会被裁剪掉,且鼠标事件只会发生在形状内部。因此,透明区域不仅视觉上消失,点击事件也会穿透。

典型的实现思路:

protected void updateWindowShape() {
    if (bgImage == null) return;
    // 创建一个 Area,遍历图片所有像素,将不透明像素合并为轮廓
    // 可以使用 Raster 或 PixelGrabber 构建一个 Path
    // 或者利用 Java 2D 的 getOutline 等方法
    // 然后 setShape(area);
}

由于构建精确的形状可能开销较大,也可以使用 AWTUtilities.setWindowShape 或简单的矩形(如果图片本身就是矩形)。在千千静听中,通常使用 Area 来聚合所有不透明像素,形成复杂的异形轮廓。


5. 边缘拖拽调整窗口大小(EdgeResizer)

不规则窗口仍然需要支持用户调整大小。SkinWindow 通过内部类 EdgeResizer 实现边缘拖拽:

  • 创建一个透明的玻璃面板(GlassPane),覆盖整个窗口。
  • 玻璃面板重写 contains 方法,使只有靠近窗口边缘(6px 内)的区域才捕获鼠标事件,其余区域透传。
  • 在鼠标移动时,根据鼠标位置判断是哪个边缘(左、右、上、下或角落),并改变光标形状。
  • 鼠标拖拽时,根据边缘类型计算新的窗口位置和尺寸,并调用 frame.setBounds()
  • 窗口尺寸变化会触发 ComponentListener.componentResized,进而调用 repositionControls() 重新布局子控件,并调用 updateWindowShape() 更新窗口形状(由子类实现)。

这种设计使得不规则窗口在保持异形外观的同时,依然具备标准的窗口调整行为。


6. 子控件布局与对齐(repositionControls)

当窗口缩放时,子控件需要根据对齐规则重新定位。SkinWindow 通过 repositionControls() 遍历所有注册的控件(controls 列表),根据每个控件的 align 属性计算新位置:

  • fill:控件的某一边或两边拉伸,以填满可用空间(类似 BorderLayout.CENTER)。
  • left/right/center/top/bottom/middle:相对于窗口边缘或中心对齐,保持固定的边距。
  • 这种布局机制使得按钮、进度条等控件在窗口缩放时能够自适应。

7. 总结:不规则窗口实现的完整流程

  1. 初始化SkinWindow 构造时加载背景图片,解码并替换透明色。
  2. 窗口设置:无边框、透明背景。
  3. 背景绘制BgPanel 使用九宫格绘制背景,透明区域自然镂空。
  4. 窗口形状:子类重写 updateWindowShape,根据背景图片的非透明区域设置 Shape,使窗口轮廓与图片一致。
  5. 边缘拖拽EdgeResizer 监听玻璃面板鼠标事件,支持窗口缩放,缩放后触发控件重布局和形状更新。
  6. 控件自适应:通过 repositionControlsalign 属性,子控件随窗口缩放调整位置和尺寸。

这种设计将不规则窗口的视觉呈现(透明镂空、九宫格拉伸)与交互行为(拖拽、调整大小)完美结合,既保证了皮肤的美观,又提供了完整的窗口操作体验。

License

本项目仅供学习和个人使用。皮肤资源版权归原作者所有。

「获取方式」:后台回复 ttplayer

image

posted @ 2026-08-28 11:42  tomj2ee  阅读(3)  评论(0)    收藏  举报