Java Swing 中背景图片的实现
效果见
可以看到面板中间是空的。
想要实现这样一个透明的背景,首先要明白 Java Swing 的画图逻辑
但是,如果是面板嵌套。比如
这个时候, 如果想让 A1 的背景也透明。
按照 Java Swing 的逻辑,需要让 AA 透明、 A1 也透明
因为绘图时,是有图层的概念的。
需要写在下面绘图,然后再在上面绘图。
在 panel
的层级结构中,即需要让
从 AA 开始到 想要透明背景的组件 都要是透明的才可以。
其基本的使用方式有两种
setOpaque
JComponent.setOpaque(false)
将背景设置为 Color(0,0,0,0)
Component.setColor()
特殊情况之重绘
当按照上面的方案重绘时,比如直接使用 repaint
对当前的组件进行绘制。
这个时候,会 clean 当前的画布。
即会将当前组件的背景置为一个 getBackground
的值。
这个时候如果使用的 opaque
或者 transparent
.
都会导致背景变成黑色。
见效果
所以,如果想要对透明背景进行重绘,那么必须从
【背景】→【…】→【组件】
这样一条链路都进行刷新。
特殊情况之 ScrollPanel
这里是遇到了一个特殊的需求。需要在 ScrollPanel
上进行透明背景。
经过阅读 ScrollPanel 的源码,发现。
他的结构如上。
当超过 viewport
的大小的时候。就会出现滚动条。
但是当滚动时,他为了保证性能是增量绘制的逻辑。
因此导致出现了 重绘 的同样的问题。
但是由于增量绘制的逻辑,我本身不能控制。因此考虑只有两条路。
1- 修改增量绘制中的部分逻辑
2- 将增量绘制改为全量绘制
增量绘制
阅读源码,发现逻辑集中在 javax.swing.JViewport#setViewPosition
中
if ((oldX != newX) || (oldY != newY)) {
if (!waitingForRepaint && isBlitting() && canUseWindowBlitter()) {
RepaintManager rm = RepaintManager.currentManager(this);
// The cast to JComponent will work, if view is not
// a JComponent, isBlitting will return false. JComponent jview = (JComponent)view;
Rectangle dirty = rm.getDirtyRegion(jview);
if (dirty == null || !dirty.contains(jview.getVisibleRect())) {
rm.beginPaint();
try {
Graphics g = JComponent.safelyGetGraphics(this);
flushViewDirtyRegion(g, dirty);
// 标记当前的位置
view.setLocation(newX, newY);
Rectangle r = new Rectangle(
0, 0, getWidth(), Math.min(getHeight(), jview.getHeight()));
g.setClip(r);
// Repaint the complete component if the blit succeeded
// and needsRepaintAfterBlit returns true.
// mark-增量绘制
repaintAll = (windowBlitPaint(g) &&
needsRepaintAfterBlit());
g.dispose();
rm.notifyRepaintPerformed(this, r.x, r.y, r.width, r.height);
rm.markCompletelyClean((JComponent)getParent());
rm.markCompletelyClean(this);
rm.markCompletelyClean(jview);
} finally {
rm.endPaint();
}
}
else {
// The visible region is dirty, no point in doing copyArea
view.setLocation(newX, newY);
repaintAll = false;
}
}
else {
scrollUnderway = true;
// This calls setBounds(), and then repaint().
view.setLocation(newX, newY);
repaintAll = false;
}
// we must validate the hierarchy to not break the hw/lw mixing
revalidate();
fireStateChanged();
}
void paintForceDoubleBuffered(Graphics g) {
RepaintManager rm = RepaintManager.currentManager(this);
Rectangle clip = g.getClipBounds();
rm.beginPaint();
setFlag(IS_REPAINTING, true);
try {
rm.paint(this, this, g, clip.x, clip.y, clip.width, clip.height);
} finally {
rm.endPaint();
setFlag(IS_REPAINTING, false);
}
}
protected void paintDoubleBuffered(JComponent c, Image image,
Graphics g, int clipX, int clipY,
int clipW, int clipH) {
Graphics osg = image.getGraphics();
int bw = Math.min(clipW, image.getWidth(null));
int bh = Math.min(clipH, image.getHeight(null));
int x,y,maxx,maxy;
try {
for(x = clipX, maxx = clipX+clipW; x < maxx ; x += bw ) {
for(y=clipY, maxy = clipY + clipH; y < maxy ; y += bh) {
osg.translate(-x, -y);
osg.setClip(x,y,bw,bh);
if (volatileBufferType != Transparency.OPAQUE
&& osg instanceof Graphics2D) {
final Graphics2D g2d = (Graphics2D) osg;
final Color oldBg = g2d.getBackground();
g2d.setBackground(c.getBackground());
// 清理之前的区域
g2d.clearRect(x, y, bw, bh);
g2d.setBackground(oldBg);
}
// 遍历所有的子组件,并绘制
c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);
g.setClip(x, y, bw, bh);
if (volatileBufferType != Transparency.OPAQUE
&& g instanceof Graphics2D) {
final Graphics2D g2d = (Graphics2D) g;
final Composite oldComposite = g2d.getComposite();
g2d.setComposite(AlphaComposite.Src);
// 将图片写入
g2d.drawImage(image, x, y, c);
g2d.setComposite(oldComposite);
} else {
g.drawImage(image, x, y, c);
}
osg.translate(x, y);
}
}
} finally {
osg.dispose();
}
}
这里【遍历所有的子组件,并绘制】也有一些特殊点,
- 他的绘制是先把面板中已经有的,进行偏移
- 然后将新组件渲染,并写入
综上,完全找不到对增量的处理点。所以这个思路放弃。
全量绘制
1- 全量绘制一
首先是绘制后,感知到变动,再次绘制
// 动态画图
scrollViewport.addChangeListener(e -> repaintAll());
然而这样有一个问题,那就是不实时。会变成先渲染出来黑色的, 再恢复正常
见录屏
这个方案被放弃
2- 全量绘制二
将增量录制屏蔽掉,然后直接执行全量绘制。
即将上文中 javax.swing.JViewport#setViewPosition
复写
直接执行
@Override
public void setViewPosition(Point p) {
setLocation(-p.x, -p.y);
repaintAll();
}
发现隐藏掉的组件并不会渲染。
**猜测:全量绘制一 为什么能执行成功?也许增量绘制一次后,再次执行全量就不会出现问题 **
3- 全量绘制三
基于上面的猜测,作出下面的调整
JViewport scrollViewport = new JViewport() {
/**
* 从而屏蔽掉 {@link RepaintManager.PaintManager#paintDoubleBuffered(JComponent, Image, Graphics, int, int, int, int)}
* * @return 创建一个不会实际画图的 Graphics
*/ @Override
public Graphics getGraphics() {
Graphics graphics = super.getGraphics();
return new FRGraphics2D((Graphics2D) graphics) {
@Override
public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
return true;
}
};
}
};
// 动态画图
scrollViewport.addChangeListener(e -> repaintAll());
- 执行增量绘制的逻辑,但屏蔽掉写入图片的逻辑
- 执行全量绘制的逻辑
从而让结果变得合适。
结论
很多时候,是需要理解原理才能理顺如何修改逻辑的。
就像这次这个问题,如果不知道如何绘制的原理,包括基础原理和针对 ScrollPanel 的绘制原理。
那么根本不可能解决这个问题。
所以,简单问题简单看,复杂问题从根源看。