【发布时间】:2023-12-06 10:48:02
【问题描述】:
我正在尝试在 JScrollPane 中编写可缩放图像。
当图像完全缩小时,它应该水平和垂直居中。当两个滚动条都出现时,缩放应该总是相对于鼠标坐标发生,即在缩放事件之前和之后,图像的同一点应该在鼠标下方。
我几乎实现了我的目标。不幸的是,“scrollPane.getViewport().setViewPosition()”方法有时无法正确更新视图位置。在大多数情况下,调用该方法两次(hack!)解决了这个问题,但视图仍然闪烁。
我无法解释为什么会这样。不过我相信这不是数学问题。
下面是 MWE。要查看我的问题具体是什么,您可以执行以下操作:
- 放大直到出现一些滚动条(200% 左右)
- 点击滚动条滚动到右下角
- 将鼠标放在角落并放大两次。第二次您会看到滚动位置如何跳向中心。
如果有人能告诉我问题出在哪里,我将不胜感激。谢谢!
package com.vitco;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel test case
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
@Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
// IMPORTANT: This call doesn't always update the viewport position. If the call is made twice
// it works correctly. However the screen still "flickers".
scrollPane.getViewport().setViewPosition(newViewportPosition);
// debug
if (!newViewportPosition.equals(scrollPane.getViewport().getViewPosition())) {
System.out.println("Error: " + newViewportPosition + " != " + scrollPane.getViewport().getViewPosition());
}
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
注意:我也看过JScrollPane setViewPosition After "Zoom"这里的问题,但不幸的是问题和解决方案略有不同,不适用。
编辑
我已经通过使用 hack 解决了这个问题,但是我仍然没有更深入地了解根本问题是什么。发生的情况是,当调用 setViewPosition 时,某些内部状态更改会触发对 setViewPosition 的额外调用。这些额外的调用只是偶尔发生。当我阻止它们时,一切正常。
为了解决这个问题,我只是引入了一个新的布尔变量“blocked = false;”并替换了行
scrollPane = new JScrollPane(centerPanel);
和
scrollPane.getViewport().setViewPosition(newViewportPosition);
与
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
@Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
和
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
如果有人能理解这一点,我仍然非常感激!
为什么这个 hack 有效?有没有更简洁的方法来实现相同的功能?
【问题讨论】:
-
是否打算在视图被最大程度缩小时,第一次缩放操作已经缩放到与我的鼠标所在位置不同的位置?
-
编辑了示例并添加了 scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);这应该有助于最初的跳跃。
-
它不能解决最初的跳转,我也添加了这些以查看问题是否来自计算滚动条的大小到该位置(它不应该),但事实并非如此。已经很奇怪的一件事是您的图像没有恒定的大小。将框架设置为全屏尺寸,最大程度地放大,将框架调整为非常小的尺寸,然后缩小 - 你会得到一个很小的图像。这是故意的吗?
-
(1) 关于初始跳转:图像必须跳转到某种程度,因为缩小时它应该居中。一旦出现滚动条,缩放应该始终正常工作。添加滚动条(我之前的评论)意味着初始跳转不是“到中心”而是“边界”。所以它们会产生我想要的初始跳跃效果。 (2) 图像大小取决于变焦。您可以缩小的最大量取决于窗口大小(一旦滚动条消失,您就不能再进一步缩小)。所以行为符合预期。
-
如果您将帧大小设置为 200x200 而不是 600x600,则第一个缩放操作会按预期工作(编辑:取决于缩放位置是否离边界足够远)。问题似乎在于您的计算,而不是 setViewPosition 的行为。至于第(1)点,缩小一下就忽略了。无论帧大小和滚动条的可见性如何,这不是您想要放大鼠标所在的像素吗?
标签: java swing zooming jscrollpane centering