我正在创建一个应用程序,涉及在
BufferedImage
方法中使用 JComponent
将 Graphics2D
绘制到 paintComponent(Graphics)
。
我添加了放大和缩小图像的功能。我正在绘制的
JComponent
包含在 JScrollPane
中。如果图像放大到足够大,就会出现滚动条。
每当我滚动
JScrollPane
时,我都会看到奇怪的视觉伪影。
这是我的图像的缩小版本。
这是我的图像的放大版本,没有视觉伪影。
这是我向下滚动一点,然后向右滚动时的样子。
发现这一点后,我添加了一个按钮,仅触发相关
repaint()
上的 revalidate()
和 JComponent
调用。这样做解决了它。
按右上角的重绘按钮之前
按右上角的重绘按钮后
这是处理将
BufferedImage
绘制到 JComponent
的代码的相关部分。
@Override
protected void paintComponent(final Graphics dontUse)
{
super.paintComponent(dontUse);
if (!(dontUse instanceof Graphics2D g))
{
throw new RuntimeException("Unknown graphics type = " + dontUse);
}
DRAW_DRAWN_PIXELS:
{
final Rectangle rectangle = gui.drawingAreaScrollPane.getViewport().getViewRect();
//We are only drawing a subsection because we may be working with GIGANTIC images.
//If we attempt to draw the whole image, performance will drop like a rock.
CALCULATE_SUBSECTION_TO_DRAW:
{
final int x = rectangle.x;
final int y = rectangle.y;
final int width = Math.min(rectangle.width, drawingArea.width);
final int height = Math.min(rectangle.height, drawingArea.height);
g.setBackground(gui.transparencyColor);
g.clearRect(rectangle.x, rectangle.y, width, height);
}
DRAW_SUBSECTION_OF_IMAGE:
{
final int originalImageX;
final int zoomedInImageX;
final int originalImageY;
final int zoomedInImageY;
CALCULATE_ORIGINAL_POSITION:
{
final int quantizedX = quantize.applyAsInt(rectangle.x);
final int quantizedY = quantize.applyAsInt(rectangle.y);
zoomedInImageX = Math.max(quantizedX, 0);
originalImageX = zoomedInImageX / gui.screenToImagePixelRatio;
zoomedInImageY = Math.max(quantizedY, 0);
originalImageY = zoomedInImageY / gui.screenToImagePixelRatio;
}
final int zoomedInImageWidth;
final int originalImageWidth;
final int zoomedInImageHeight;
final int originalImageHeight;
CALCULATE_ORIGINAL_DIMENSION:
{
final int minWidth = Math.min(rectangle.width, drawingArea.width);
final int quantizedMinWidth = quantize.applyAsInt(minWidth);
final int potentialWidth = quantizedMinWidth + gui.screenToImagePixelRatio;
zoomedInImageWidth = Math.min(drawingArea.width, potentialWidth);
originalImageWidth = zoomedInImageWidth / gui.screenToImagePixelRatio;
final int minHeight = Math.min(rectangle.height, drawingArea.height);
final int quantizedMinHeight = quantize.applyAsInt(minHeight);
final int potentialHeight = quantizedMinHeight + gui.screenToImagePixelRatio;
zoomedInImageHeight = Math.min(drawingArea.height, potentialHeight);
originalImageHeight = zoomedInImageHeight / gui.screenToImagePixelRatio;
}
g.setPaint(gui.cursorColor);
System.out.println(zoomedInImageX + " -- " + zoomedInImageY + " -- " + zoomedInImageWidth + " -- " + zoomedInImageHeight + " ---- " + originalImageX + " -- " + originalImageY + " -- " + originalImageWidth + " -- " + originalImageHeight + " ----- " + rectangle + " - " + drawingArea);
g
.drawImage
(
gui.image,
zoomedInImageX,
zoomedInImageY,
zoomedInImageX + zoomedInImageWidth,
zoomedInImageY + zoomedInImageHeight,
originalImageX,
originalImageY,
originalImageX + originalImageWidth,
originalImageY + originalImageHeight,
//gui.transparencyColor,
null
)
;
}
DRAW_GRID_LINES:
{
if (gui.hasGridLines && gui.screenToImagePixelRatio > 1)
{
g.setPaint(gui.gridLinesColor);
g.setStroke(new java.awt.BasicStroke(1));
IntStream
.range(rectangle.y, rectangle.y + rectangle.height)
.forEach
(
eachIndex ->
{
if (eachIndex % gui.screenToImagePixelRatio == 0)
{
g
.drawLine
(
rectangle.x,
eachIndex,
rectangle.x + rectangle.width,
eachIndex
)
;
}
}
)
;
IntStream
.rangeClosed(rectangle.x, rectangle.x + rectangle.width)
.forEach
(
eachIndex ->
{
if (eachIndex % gui.screenToImagePixelRatio == 0)
{
g
.drawLine
(
eachIndex,
rectangle.y,
eachIndex,
rectangle.y + rectangle.height
)
;
}
}
)
;
}
}
}
// gui.drawingAreaScrollPane.repaint();
// gui.drawingAreaScrollPane.revalidate();
}
这是一个完整的可运行示例。
import javax.imageio.*;
import javax.imageio.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.colorchooser.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
public class GUI
{
private static final Function<String, Border> TITLED_BORDER =
title ->
BorderFactory
.createTitledBorder
(
null,
title,
TitledBorder.CENTER,
TitledBorder.TOP
)
;
private static final Color CLEAR = new Color(0, 0, 0, 0);
private static final Point OFF_SCREEN = new Point(-1, -1);
public static final int ARBITRARY_VIEW_BUFFER = 200;
private static final int MIN_PEN_SIZE = 1;
private static final int MAX_PEN_SIZE = 10;
private static final int MIN_SCREEN_TO_IMAGE_PIXEL_RATIO = 1;
private static final int MAX_SCREEN_TO_IMAGE_PIXEL_RATIO = 30;
private static final int DEFAULT_IMAGE_PIXEL_ROWS = 26;
private static final int DEFAULT_IMAGE_PIXEL_COLUMNS = 24;
private final JFrame frame;
private final JScrollPane drawingAreaScrollPane = new JScrollPane();
private BufferedImage image;
private Color transparencyColor = Color.WHITE;
private Color cursorColor = Color.BLACK;
private Color gridLinesColor = Color.GRAY;
private boolean hasGridLines = true;
private int penSize = 1;
private int screenToImagePixelRatio = 10;
public static void main(final String[] args)
{
final BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
IntStream
.range(0, 20)
.forEach(i -> image.setRGB(i, i, Color.RED.getRGB()))
;
new GUI(image);
}
public GUI()
{
this(DEFAULT_IMAGE_PIXEL_ROWS, DEFAULT_IMAGE_PIXEL_COLUMNS);
}
public GUI(final int numImagePixelRows, final int numImagePixelColumns)
{
this(new BufferedImage(numImagePixelColumns, numImagePixelRows, BufferedImage.TYPE_INT_ARGB));
}
public GUI(final BufferedImage image)
{
Objects.requireNonNull(image);
try
{
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
}
catch (Exception e)
{
throw new RuntimeException(e);
}
INITALIZING_METADATA_INSTANCE_FIELDS:
{
this.image = image;
}
this.frame = new JFrame();
this.frame.setTitle("Paint");
this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.frame.add(this.createMainPanel());
this.frame.pack();
this.frame.setLocationByPlatform(true);
this.frame.setVisible(true);
}
private JPanel createMainPanel()
{
final JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(this.createCenterPanel(), BorderLayout.CENTER);
return mainPanel;
}
private JPanel createCenterPanel()
{
final GUI gui = this; //useful when trying to differentiate between different this'.
final JPanel mainPanel;
CREATE_MAIN_PANEL:
{
mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
}
final JPanel drawingPanel;
final Runnable UPDATE_DRAWING_PANEL_BORDER_TEXT;
final Runnable RECREATE_DRAWING_AREA_FRESH;
final Runnable REPAINT_DRAWING_PANEL;
final BiFunction<Point, Integer, Point> originalToZoomed =
(original, ratio) ->
new Point(original.x * ratio, original.y * ratio)
;
final BiFunction<Point, Integer, Point> zoomedToOriginal =
(zoomed, ratio) ->
new Point
(
(zoomed.x - (zoomed.x % ratio)) / ratio,
(zoomed.y - (zoomed.y % ratio)) / ratio
)
;
CREATE_DRAWING_PANEL:
{
SET_UP_DRAWING_PANEL:
{
drawingPanel = new JPanel();
drawingPanel.setLayout(new BoxLayout(drawingPanel, BoxLayout.PAGE_AXIS));
drawingPanel.add(Box.createRigidArea(new Dimension(123, 456)));
}
REPAINT_DRAWING_PANEL =
() ->
{
drawingPanel.repaint();
drawingPanel.revalidate();
}
;
RECREATE_DRAWING_AREA_FRESH =
() ->
{
final var thingToFocus = drawingPanel;
drawingPanel.removeAll();
final Dimension drawingArea = gui.deriveDrawingAreaDimensions();
final IntUnaryOperator quantize =
num ->
num - (num % gui.screenToImagePixelRatio)
;
final Box.Filler box =
new Box.Filler(drawingArea, drawingArea, drawingArea)
{
@Override
protected void paintComponent(final Graphics dontUse)
{
super.paintComponent(dontUse);
if (!(dontUse instanceof Graphics2D g))
{
throw new RuntimeException("Unknown graphics type = " + dontUse);
}
DRAW_DRAWN_PIXELS:
{
final Rectangle rectangle = gui.drawingAreaScrollPane.getViewport().getViewRect();
//We are only drawing a subsection because we may be working with GIGANTIC images.
//If we attempt to draw the whole image, performance will drop like a rock.
CALCULATE_SUBSECTION_TO_DRAW:
{
final int x = rectangle.x;
final int y = rectangle.y;
final int width = Math.min(rectangle.width, drawingArea.width);
final int height = Math.min(rectangle.height, drawingArea.height);
g.setBackground(gui.transparencyColor);
g.clearRect(rectangle.x, rectangle.y, width, height);
}
DRAW_SUBSECTION_OF_IMAGE:
{
final int originalImageX;
final int zoomedInImageX;
final int originalImageY;
final int zoomedInImageY;
CALCULATE_ORIGINAL_POSITION:
{
final int quantizedX = quantize.applyAsInt(rectangle.x);
final int quantizedY = quantize.applyAsInt(rectangle.y);
zoomedInImageX = Math.max(quantizedX, 0);
originalImageX = zoomedInImageX / gui.screenToImagePixelRatio;
zoomedInImageY = Math.max(quantizedY, 0);
originalImageY = zoomedInImageY / gui.screenToImagePixelRatio;
}
final int zoomedInImageWidth;
final int originalImageWidth;
final int zoomedInImageHeight;
final int originalImageHeight;
CALCULATE_ORIGINAL_DIMENSION:
{
final int minWidth = Math.min(rectangle.width, drawingArea.width);
final int quantizedMinWidth = quantize.applyAsInt(minWidth);
final int potentialWidth = quantizedMinWidth + gui.screenToImagePixelRatio;
zoomedInImageWidth = Math.min(drawingArea.width, potentialWidth);
originalImageWidth = zoomedInImageWidth / gui.screenToImagePixelRatio;
final int minHeight = Math.min(rectangle.height, drawingArea.height);
final int quantizedMinHeight = quantize.applyAsInt(minHeight);
final int potentialHeight = quantizedMinHeight + gui.screenToImagePixelRatio;
zoomedInImageHeight = Math.min(drawingArea.height, potentialHeight);
originalImageHeight = zoomedInImageHeight / gui.screenToImagePixelRatio;
}
g.setPaint(gui.cursorColor);
System.out.println(zoomedInImageX + " -- " + zoomedInImageY + " -- " + zoomedInImageWidth + " -- " + zoomedInImageHeight + " ---- " + originalImageX + " -- " + originalImageY + " -- " + originalImageWidth + " -- " + originalImageHeight + " ----- " + rectangle + " - " + drawingArea);
g
.drawImage
(
gui.image,
zoomedInImageX,
zoomedInImageY,
zoomedInImageX + zoomedInImageWidth,
zoomedInImageY + zoomedInImageHeight,
originalImageX,
originalImageY,
originalImageX + originalImageWidth,
originalImageY + originalImageHeight,
//gui.transparencyColor,
null
)
;
}
DRAW_GRID_LINES:
{
if (gui.hasGridLines && gui.screenToImagePixelRatio > 1)
{
g.setPaint(gui.gridLinesColor);
g.setStroke(new java.awt.BasicStroke(1));
IntStream
.range(rectangle.y, rectangle.y + rectangle.height)
.forEach
(
eachIndex ->
{
if (eachIndex % gui.screenToImagePixelRatio == 0)
{
g
.drawLine
(
rectangle.x,
eachIndex,
rectangle.x + rectangle.width,
eachIndex
)
;
}
}
)
;
IntStream
.rangeClosed(rectangle.x, rectangle.x + rectangle.width)
.forEach
(
eachIndex ->
{
if (eachIndex % gui.screenToImagePixelRatio == 0)
{
g
.drawLine
(
eachIndex,
rectangle.y,
eachIndex,
rectangle.y + rectangle.height
)
;
}
}
)
;
}
}
}
// gui.drawingAreaScrollPane.repaint();
// gui.drawingAreaScrollPane.revalidate();
}
}
;
box.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
drawingPanel.add(Box.createHorizontalGlue());
drawingPanel.add(box);
drawingPanel.add(Box.createHorizontalGlue());
REPAINT_DRAWING_PANEL.run();
}
;
RECREATE_DRAWING_AREA_FRESH.run();
}
final JPanel drawingSettingsPanel;
CREATE_DRAWING_SETTINGS_PANEL:
{
drawingSettingsPanel = new JPanel();
drawingSettingsPanel.setLayout(new BoxLayout(drawingSettingsPanel, BoxLayout.LINE_AXIS));
final JSpinner screenToImagePixelRatioDropDownMenu;
SCREEN_TO_IMAGE_PIXEL_RATIO_DROP_DOWN_MENU:
{
screenToImagePixelRatioDropDownMenu =
new JSpinner
(
new SpinnerNumberModel
(
this.screenToImagePixelRatio,
MIN_SCREEN_TO_IMAGE_PIXEL_RATIO,
MAX_SCREEN_TO_IMAGE_PIXEL_RATIO,
1
)
)
;
screenToImagePixelRatioDropDownMenu
.addChangeListener
(
event ->
{
if (!(screenToImagePixelRatioDropDownMenu.getValue() instanceof Integer iii))
{
throw new IllegalStateException();
}
this.screenToImagePixelRatio = iii;
RECREATE_DRAWING_AREA_FRESH.run();
}
);
}
final JCheckBox hasGridLinesCheckBox;
HAS_GRID_LINES_CHECK_BOX:
{
hasGridLinesCheckBox = new JCheckBox();
hasGridLinesCheckBox.setText("Activate Grid Lines");
hasGridLinesCheckBox.setSelected(true);
hasGridLinesCheckBox
.addActionListener
(
event ->
{
this.hasGridLines = hasGridLinesCheckBox.isSelected();
REPAINT_DRAWING_PANEL.run();
}
)
;
}
final JButton repaint = new JButton("REPAINT");
repaint.addActionListener(event -> REPAINT_DRAWING_PANEL.run());
drawingSettingsPanel.add(Box.createHorizontalGlue());
drawingSettingsPanel.add(screenToImagePixelRatioDropDownMenu);
drawingSettingsPanel.add(new JLabel("SCREEN pixels = 1 IMAGE pixel"));
drawingSettingsPanel.add(Box.createHorizontalStrut(10));
drawingSettingsPanel.add(hasGridLinesCheckBox);
drawingSettingsPanel.add(Box.createHorizontalStrut(10));
drawingSettingsPanel.add(repaint);
drawingSettingsPanel.add(Box.createHorizontalGlue());
}
CREATE_CENTERED_DRAWING_PANEL:
{
final JPanel centeredDrawingPanel = new JPanel();
centeredDrawingPanel.setLayout(new BoxLayout(centeredDrawingPanel, BoxLayout.PAGE_AXIS));
centeredDrawingPanel.add(Box.createVerticalGlue());
centeredDrawingPanel.add(drawingPanel);
centeredDrawingPanel.add(Box.createVerticalGlue());
UPDATE_DRAWING_PANEL_BORDER_TEXT =
() ->
this.drawingAreaScrollPane
.setBorder
(
BorderFactory
.createCompoundBorder
(
TITLED_BORDER
.apply
(
"Drawing Area -- "
+ gui.image.getHeight()
+ " rows and "
+ gui.image.getWidth()
+ " columns"
),
BorderFactory.createLineBorder(Color.BLACK, 1)
)
)
;
UPDATE_DRAWING_PANEL_BORDER_TEXT.run();
this.drawingAreaScrollPane.setViewportView(centeredDrawingPanel);
}
mainPanel.add(drawingSettingsPanel, BorderLayout.NORTH);
mainPanel.add(this.drawingAreaScrollPane, BorderLayout.CENTER);
return mainPanel;
}
private static void print(final String text)
{
System.out.println(LocalDateTime.now() + " -- " + text);
}
private Dimension deriveDrawingAreaDimensions()
{
return
new Dimension
(
this.image.getWidth() * this.screenToImagePixelRatio,
this.image.getHeight() * this.screenToImagePixelRatio
)
;
}
}
忘记了我周围有一些旧代码(我在某处找到的),展示了如何使用 AffineTransform 来旋转/缩放/平移图像:
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.io.*;
import javax.imageio.*;
import java.awt.geom.*;
import java.awt.image.*;
public class RotateAndScale2 extends JPanel
{
private Image image;
private float scale = 10.0f;
public RotateAndScale2(Image image)
{
this.image = image;
}
@Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
// create the transform, note that the transformations happen
// in reversed order (so check them backwards)
AffineTransform at = new AffineTransform();
// 4. translate it to the center of the component
at.translate(getWidth() / 2, getHeight() / 2);
// 3. do the actual rotation
// at.rotate(Math.toRadians(45));
// 2. scale the image
at.scale(scale, scale);
// 1. translate the object to rotate around the center
at.translate(-image.getWidth(this) / 2, -image.getHeight(this) / 2);
// draw the image
Graphics2D g2 = (Graphics2D)g.create();
g2.drawImage(image, at, null);
g2.dispose();
// continue drawing other stuff (non-transformed)
//...
}
@Override
public Dimension getPreferredSize()
{
int width = image.getWidth(this);
int height = image.getHeight(this);
return new Dimension((int)(width * scale), (int)(height * scale));
}
private static void createAndShowGUI()
{
try
{
BufferedImage image = ImageIO.read(new File("splash.gif"));
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add( new JScrollPane( new RotateAndScale2(image)));
frame.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setLocationByPlatform( true );
frame.setVisible( true );
}
catch(Exception e) { System.out.println(e); }
}
public static void main(String[] args) throws Exception
{
java.awt.EventQueue.invokeLater( () -> createAndShowGUI() );
}
}
更好的实现是使用 setRotate(...) 和 setScale(...) 方法来调整图像的属性。然后,这两种方法都会调用 revalidate() 和 repaint() 以确保使用新属性绘制图像。