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


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(如 Area 或 Path2D),然后通过 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. 总结:不规则窗口实现的完整流程
- 初始化:
SkinWindow构造时加载背景图片,解码并替换透明色。 - 窗口设置:无边框、透明背景。
- 背景绘制:
BgPanel使用九宫格绘制背景,透明区域自然镂空。 - 窗口形状:子类重写
updateWindowShape,根据背景图片的非透明区域设置Shape,使窗口轮廓与图片一致。 - 边缘拖拽:
EdgeResizer监听玻璃面板鼠标事件,支持窗口缩放,缩放后触发控件重布局和形状更新。 - 控件自适应:通过
repositionControls和align属性,子控件随窗口缩放调整位置和尺寸。
这种设计将不规则窗口的视觉呈现(透明镂空、九宫格拉伸)与交互行为(拖拽、调整大小)完美结合,既保证了皮肤的美观,又提供了完整的窗口操作体验。
License
本项目仅供学习和个人使用。皮肤资源版权归原作者所有。
「获取方式」:后台回复 ttplayer


浙公网安备 33010602011771号