0

I am creating an application that involves drawing a BufferedImage to a JComponent using Graphics2D in a paintComponent(Graphics) method.

I added the ability to zoom in and out of the image. The JComponent I am drawing to is contained within a JScrollPane. If the image becomes zoomed in enough, scrollbars appear.

Whenever I scroll the JScrollPane, I get weird visual artifacts.

Here is the zoomed out version of my image.

Zoomed out version of the image

Here is the zoomed in version of my image, with no visual artifacts.

Zoomed in version of the image

And here is what it looks like when I scroll down a bit, and then to the right.

Zoomed in version of the image with weird visual artifacts

Upon discovering this, I added a button that just triggers a repaint() and revalidate() call on the relevant JComponent. Doing that fixed it.

BEFORE PRESSING THE REPAINT BUTTON ON THE TOP RIGHT

Before pressing REPAINT

AFTER PRESSING THE REPAINT BUTTON ON THE TOP RIGHT

After pressing REPAINT

Here is the relevant portion of the code that handles painting the BufferedImage to the 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();

}

And here is a complete runnable example.


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
         )
         ;
   
   }

}

26
  • 1
    "... I added a button that just triggers a repaint() ...", have it repaint whenever the JScrollPane view port is adjusted, or zoomed.
    – Reilas
    Commented Oct 24, 2023 at 0:31
  • 1
    gui.drawingAreaScrollPane.getViewport().getViewRect(); might be your first mistake, maybe, instead, look at the clipping rectangle of the Graphics context. if (!(dontUse instanceof Graphics2D g)) is also pointless, I think since 1.3 or 4 your guaranteed to get a Graphics2D instance Commented Oct 24, 2023 at 1:09
  • 1
    The clip rectangle is how Swing paints only a part of the component for efficiency. For example if you use repaint(?, ?, ?, ?) you can create your own clipping area. Compare the Rectangle you calculate with your original method with the clipping area used by Swing to see what the difference is. Note, when I tried the suggestion it still didn't work for me, but maybe I didn't change it correctly.
    – camickr
    Commented Oct 24, 2023 at 2:00
  • 1
    1) Yes, that is how I changed my code. 2) When I drag the vertical scrollbar down it seems to paint correctly. However when I drag it up I sometimes see a square where the top part of the square is not painted. Sometimes it self corrects when I do down/up again. Not sure what is happening. 3) I never really understand how zooming works so I have no suggestions. 4) I was just trying to state that even though you were calculating a rectangle the size of the viewport the clip rectangle is only a few pixels high so only a small area of the viewport is repainted. You still need to scale the image.
    – camickr
    Commented Oct 24, 2023 at 2:54
  • 2
    Maybe you can use the scale() and translate() methods of the Graphics2D class to simplify the code. Or maybe use an AffineTransform. I think that is what I see in other postings.
    – camickr
    Commented Oct 24, 2023 at 2:57

1 Answer 1

1

The more I look at your code the more confused I am. I must admit I don't recognize half of the coding constructs you are using.

However, from what I do understand, it appears to me you don't understand the basics of how custom painting works. You should read the Swing tutorial on Custom Painting for a basic example.

Some tips:

  1. the code for the painting should be self contained in a class.
  2. you pass parameters to the class to control the painting. Parameters such as rows, columns, image, zoom factor.
  3. the paintComponent method will directly reference the above parameters.
  4. you override the getPreferredSize() method to control the size of your component. This method would likely reference your image and the zoom factor. The way you currenlty set the size buy creating a new Box.Filler is unique to say the least.
  5. the code for your ActionListeners should not be recreating the drawing panel. Instead you just invoke a method on the panel to update a property and then invoke revalidate() and repaint().

Because you don't follow the standard coding conventions the code becomes overly complex (to me anyway). I can't think of any good reasons for the code to be structured this way.

I am still confused about your claim that using:

//final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
final Rectangle rectangle  = g.getClipBounds(); 

fixes the problem. As I stated earlier it did not fix the problem for me.

Did you try displaying the Rectangle in both cases to compare the values? Do you understand what the differences mean? I'm not understanding your discussion with MadProgrammer about the "context". In both cases the Rectangle uses the proper x/y location relative to (0, 0) of the components. The difference is that your approach uses a larger area since the width/height values will be the size of the viewport, not the size of the scroll distance.

In any case I downloaded your code again and retested. The strangest thing is that your original code now works (after I made a couple of coding changes since your syntax doesn't work for me with JDK11).

I also simplified the drawImage statement to just use:

g.drawImage(gui.image, 0, 0, image.getWidth() * gui.screenToImagePixelRatio, image.getHeight() * gui.screenToImagePixelRatio, null);

and it still works for me.

So for what its worth here is the version of the code I'm testing on Windows 11, JDK11 that works:

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);

                        }
*/
                        Graphics2D g = (Graphics2D)dontUse;

                        DRAW_DRAWN_PIXELS:
                        {

                           final Rectangle rectangle  = gui.drawingAreaScrollPane.getViewport().getViewRect();
//                           final Rectangle rectangle  = g.getClipBounds();

                           //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, 0, 0, image.getWidth() * gui.screenToImagePixelRatio, image.getHeight() * gui.screenToImagePixelRatio, null);
/*
                              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();

                     }
*/
                     int iii = ((Integer)screenToImagePixelRatioDropDownMenu.getValue()).intValue();
                     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
         )
         ;
   
   }

}

Note: just because it works doesn't mean I think you should use it. I believe the code should be restructured to better follow Swing custom painting conventions.

11
  • Hey, thank you for posting this. First off, I actually made a couple of mistakes. As you pointed out, while I "converted" the x and y coordinates to be equivalent across contexts, the height and width are different. In my surprise to see that the suggested solution worked, I forgot to check height and width. Commented Oct 25, 2023 at 22:22
  • As for the poor organization, you are right on the money. I am still not great at that, as you have seen from several of my other questions that you have answered. This is my first time delving into custom painting, so I will refactor my solution to follow the protocols for custom painting that you pointed out. Commented Oct 25, 2023 at 22:24
  • As for your simplification, I feel like I originally started out with that, but then switched away from it because of performance problems. I may have made a premature decision however. I will experiment with this and give it a shot. Commented Oct 25, 2023 at 22:27
  • 1
    I do not understand point 5 - how do you change the text/icon in a JLabel? Do you create a new JLabel? No, you invoke the setText(...) or setIcon(...) method. So when youi want to change the zoom factor do you create a new drawing panel? No, you invoke a method on the drawing panel to set the zoom parameter. See: stackoverflow.com/a/52571767/131872 as a simple example. It does a rotation, not a zoom, buy you should get the idea.
    – camickr
    Commented Oct 25, 2023 at 23:59
  • 1
    Note it is NOT a complete example because it does not implement the getPreferredSize() method to consider how the size of the image changes as the angle of rotation changes. It does demonstes the concept of responding to an event to change a property of the class.
    – camickr
    Commented Oct 25, 2023 at 23:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.