开发者

Java Graphics.fillPolygon: How to also render right and bottom edges?

开发者 https://www.devze.com 2023-04-11 22:18 出处:网络
When drawing polygons, Java2D leaves off the right and bottom edges.I understand why this is done.However, I would like to draw something that includes those edges.One thing that occurred to me was to

When drawing polygons, Java2D leaves off the right and bottom edges. I understand why this is done. However, I would like to draw something that includes those edges. One thing that occurred to me was to follow fillPolygon with drawPolygon with the same coordinates, but this appears to leave a gap. (See the little triangular image at the bottom.) There are two possibilities, but I can't tell which. To enable antialiasing, I'm doing this:

renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                  RenderingHints.VALUE_ANTIALIAS_ON);
renderHints.put(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHints(renderHints);

One possibility is that the antialiasing is not being done on the alpha channel, so the gap is caused by overdraw. In that case, if the alpha channel were what was being antialiased, the edges would abut properly. The other possibility is that there is just a gap here.

How can I fix this?

Also, I'm not sure, but it appears that the polygon outline may actually be TOO BIG. That is, it may be going further out than the right and bottom edges that I want to include.

Thanks.

Java Graphics.fillPolygon: How to also render right and bottom edges?

-- UPDATE --

Based on a very nice suggestion by Hovercraft Full of Eels, I have made a compilable example:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

public class polygon {
   private static final int WIDTH = 20;

   public static void main(String[] args) {
      BufferedImage img = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_INT_ARGB开发者_如何学JAVA);
      Graphics2D g2 = img.createGraphics();
      int[] xPoints = {WIDTH / 3, (2*WIDTH) / 3, WIDTH / 3};
      int[] yPoints = {0, WIDTH / 2, WIDTH};
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(Color.green);
      g2.drawLine(0, WIDTH-1, WIDTH, WIDTH-1);
      g2.drawLine(0, 0, WIDTH, 0);
      g2.drawLine(WIDTH/3, 0, WIDTH/3, WIDTH);
      g2.drawLine((2*WIDTH/3), 0, (2*WIDTH/3), WIDTH);
      g2.setColor(Color.black);
      g2.drawPolygon(xPoints, yPoints, xPoints.length);
      g2.setColor(Color.black);
      g2.fillPolygon(xPoints, yPoints, xPoints.length);
      g2.dispose();

      ImageIcon icon = new ImageIcon(img);
      JLabel label = new JLabel(icon);

      JOptionPane.showMessageDialog(null, label);
   }
}

If you leave the filled polygon red, you get the image below (zoomed by 500%), which shows that the polygon does not extend all the way to the right edge. That is, the vertical green line is corresponds to x=(2*WIDTH)/2, and although the red polygon includes that coordinate, it does not paint any pixels there.

Java Graphics.fillPolygon: How to also render right and bottom edges?

To see the gap problem, I changed red in the program to black. In this image, you can see a subtle gap on the lower right side, where the outline drawn by drawPolygon does not quite meet up with what was drawn with fillPolygon.

Java Graphics.fillPolygon: How to also render right and bottom edges?


Show us your code for your drawing in a simple compilable runnable program. For instance when I try to imitate your image and used RenderingHints, it seemed to produce an appropriate sized image with complete right/bottom edges:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class Foo002 {
   private static final int WIDTH = 20;

   public static void main(String[] args) {
      BufferedImage img = new BufferedImage(WIDTH, WIDTH,
            BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2 = img.createGraphics();
      int[] xPoints = { WIDTH / 3, (2 * WIDTH) / 3, WIDTH / 3 };
      int[] yPoints = { 0, WIDTH / 2, WIDTH };
      g2.setColor(Color.black);
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setRenderingHint(RenderingHints.KEY_RENDERING,
            RenderingHints.VALUE_RENDER_QUALITY);
      g2.fillPolygon(xPoints, yPoints, xPoints.length);
      g2.dispose();

      ImageIcon icon = new ImageIcon(img);
      JLabel label = new JLabel(icon);
      label.setBorder(BorderFactory.createLineBorder(Color.black));
      JPanel panel = new JPanel();
      panel.add(label);

      JOptionPane.showMessageDialog(null, panel);
   }
}

If you can show us a similar program that reproduces your problem, then we can give you better help.


I like the convenience of ImageIcon, shown by @HFOE, but this variation may make it a little easier to see what's happening. From the Graphics API,

Operations that draw the outline of a figure operate by traversing an infinitely thin path between pixels with a pixel-sized pen that hangs down and to the right of the anchor point on the path. Operations that fill a figure operate by filling the interior of that infinitely thin path.

In contrast, Graphics2D must follow more complex rules for antialiasing, which allow it to "draw outside the lines."

Java Graphics.fillPolygon: How to also render right and bottom edges?

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;

/** @see http://stackoverflow.com/questions/7701097 */
public class PixelView extends JPanel {

    private static final int SIZE = 20;
    private static final int SCALE = 16;
    private BufferedImage img;

    public PixelView(Color fill) {
        this.setBackground(Color.white);
        this.setPreferredSize(new Dimension(SCALE * SIZE, SCALE * SIZE));
        img = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = img.createGraphics();
        int[] xPoints = {SIZE / 3, (2 * SIZE) / 3, SIZE / 3};
        int[] yPoints = {0, SIZE / 2, SIZE};
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(Color.green);
        g2.drawLine(0, SIZE - 1, SIZE, SIZE - 1);
        g2.drawLine(0, 0, SIZE, 0);
        g2.drawLine(SIZE / 3, 0, SIZE / 3, SIZE);
        g2.drawLine((2 * SIZE / 3), 0, (2 * SIZE / 3), SIZE);
        g2.setColor(Color.black);
        g2.drawPolygon(xPoints, yPoints, xPoints.length);
        g2.setColor(fill);
        g2.fillPolygon(xPoints, yPoints, xPoints.length);
        g2.dispose();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(img, 0, 0, getWidth(), getHeight(), null);
    }

    private static void display() {
        JFrame f = new JFrame("PixelView");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new GridLayout(1, 0));
        f.add(new PixelView(Color.black));
        f.add(new PixelView(Color.red));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                display();
            }
        });
    }
}


Sometimes "the graphics pen hangs down and to the right from the path it traverses", and sometimes it doesn't. I don't have any clear idea how to predict when it will or won't, but I have observed that RenderingHints.VALUE_STROKE_PURE can sometimes be used to alter the behavior, by trial and error. In particular, I found that if you turn on STROKE_PURE during your drawPolygon() calls in your program, it will make them match up with your fillPolygon() calls, as you desire.

I did a little study showing the effect of the STROKE_CONTROL hint, which is one of:

  • STROKE_NORMALIZE (the default, on my system)
  • STROKE_PURE

on the following calls:

  • drawLine()
  • drawPolygon()
  • fillPolygon()

in both antialiasing modes:

  • ANTIALIASING_OFF
  • ANTIALIASING_ON

And there is one more annoying dimension that apparently matters as well:

  • rendered directly to a JComponent on the screen
  • rendered to a BufferedImage.

Here are the results when rendering directly to a visible JComponent:

Java Graphics.fillPolygon: How to also render right and bottom edges?

And here are the results when rendering into a BufferedImage:

Java Graphics.fillPolygon: How to also render right and bottom edges?

(Notice the two cases in which the two pictures differ, i.e. in which direct rendering differs from BufferedImage rendering: ANTIALIAS_OFF/STROKE_NORMALIZE/fillPolygon and ANTIALIAS_OFF/STROKE_PURE/drawPolygon.)

Overall, there doesn't seem to be much rhyme or reason to the whole thing. But we can make the following specific observations based on the above pictures:

Observation #1: If you want your antialiased drawPolygon()s and antialiased fillPolygon()s to match up well (the original question), then turn on STROKE_PURE during the antialiased drawPolygon() calls. (It doesn't matter whether it's on during the antialiased fillPolygon() calls.)

Observation #2: If you want your antialiased fillPolygon()s and non-antialiased fillPolygon()s to match up (because, say, your app allows the user to switch antialiasing on and off, and you don't want the picture to jump northwest and southeast each time they do that), then turn on STROKE_PURE during the non-antialiased fillPolygon() calls.

Here is the program I used to generate the pictures above. My results are from compiling and running it it with opensdk11 on linux; I'd be interested to know if anyone gets any different results on different platforms.

/** Study the effect of STROKE_PURE. */

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

@SuppressWarnings("serial")
public final class AntiAliasingStudy {

  // These can be fiddled with.
  final static int patchWidth = 24;  // keep this a multiple of 4 for sanity
  final static int patchHeight = 20;  // keep this a multiple of 4 for sanity
  final static int borderThickness = 4;
  final static int mag = 6;

  // derived quantities
  final static int totalWidth = 5*borderThickness + 4*patchWidth;
  final static int totalHeight = 4*borderThickness + 3*patchHeight;

  private static void drawLittleStudy(Graphics2D g2d,
                                      int x00, int y00,
                                      int patchWidth, int patchHeight, int borderThickness, int totalWidth, int totalHeight) {
    g2d.setColor(new java.awt.Color(240,240,240));
    g2d.fillRect(x00,y00,totalWidth, totalHeight);

    for (int row = 0; row < 3; ++row) {
      for (int col = 0; col < 4; ++col) {
        int x0 = x00 + borderThickness + col*(patchWidth+borderThickness);
        int y0 = y00 + borderThickness + row*(patchHeight+borderThickness);
        int x1 = x0 + patchWidth;
        int y1 = y0 + patchHeight;
        g2d.setColor(java.awt.Color.WHITE);
        g2d.fillRect(x0, y0, patchWidth, patchHeight);

        boolean antialias = (col >= 2);
        boolean pure = (col % 2 == 1);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, pure ? RenderingHints.VALUE_STROKE_PURE : RenderingHints.VALUE_STROKE_NORMALIZE);

        g2d.setColor(java.awt.Color.RED);
        if (row == 0) {
          // lines (drawLine)
          // diagonals
          g2d.drawLine(x0,y1, x1,y0);
          g2d.drawLine(x0,y0, x1,y1);
          // orthogonals
          g2d.drawLine((x0+patchWidth/4),y0, (x0+patchWidth*3/4),y0);
          g2d.drawLine((x0+patchWidth/4),y1, (x0+patchWidth*3/4),y1);
          g2d.drawLine(x0,(y0+patchHeight/4), x0,(y0+patchHeight*3/4));
          g2d.drawLine(x1,(y0+patchHeight/4), x1,(y0+patchHeight*3/4));
        } else if (row == 1) {
          // outlines (drawPolygon)
          // A stopsign
          g2d.drawPolygon(new int[] {x0+patchWidth/2-2, x0, x0, x0+patchWidth/2-2, x0+patchWidth/2+2, x1, x1, x0+patchWidth/2+2},
                          new int[] {y0, y0+patchHeight/2-2, y0+patchHeight/2+2, y1, y1, y0+patchHeight/2+2, y0+patchHeight/2-2, y0},
                          8);
        } else if (row == 2) {
          // fill (fillPolygon)
          // A stopsign
          g2d.fillPolygon(new int[] {x0+patchWidth/2-2, x0, x0, x0+patchWidth/2-2, x0+patchWidth/2+2, x1, x1, x0+patchWidth/2+2},
                          new int[] {y0, y0+patchHeight/2-2, y0+patchHeight/2+2, y1, y1, y0+patchHeight/2+2, y0+patchHeight/2-2, y0},
                          8);
        }
      }
    }
  }  // drawLittleStudy

  // Show a study, previously created by drawLittleStudy(), magnified and annotated.
  private static void showMagnifiedAndAnnotatedStudy(Graphics g,
                                              BufferedImage studyImage,
                                              int x00, int y00,
                                              int patchWidth, int patchHeight, int borderThickness, int totalWidth, int totalHeight, int mag,
                                              ImageObserver imageObserver) {
    // Magnify the image
    g.drawImage(studyImage,
                /*dst*/ x00,y00,x00+totalWidth*mag,y00+totalHeight*mag,
                /*src*/ 0,0,totalWidth,totalHeight,
                imageObserver);

    // Draw annotations on each picture in black,
    // in the highest quality non-biased mode
    // (now that we know what that is!)
    g.setColor(java.awt.Color.BLACK);
    ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
    for (int row = 0; row < 3; ++row) {
      for (int col = 0; col < 4; ++col) {
        int x0 = borderThickness + col*(patchWidth+borderThickness);
        int y0 = borderThickness + row*(patchHeight+borderThickness);
        int x1 = x0 + patchWidth;
        int y1 = y0 + patchHeight;
        if (false) {
          g.drawLine(x00+x0*mag,y00+y0*mag, x00+x1*mag,y00+y0*mag);
          g.drawLine(x00+x1*mag,y00+y0*mag, x00+x1*mag,y00+y1*mag);
          g.drawLine(x00+x1*mag,y00+y1*mag, x00+x0*mag,y00+y1*mag);
          g.drawLine(x00+x0*mag,y00+y1*mag, x00+x0*mag,y00+y0*mag);
        }
        if (row == 0) {
          // diagonals
          g.drawLine(x00+x0*mag,y00+y1*mag, x00+x1*mag,y00+y0*mag);
          g.drawLine(x00+x0*mag,y00+y0*mag, x00+x1*mag,y00+y1*mag);
          // orthogonals
          g.drawLine(x00+(x0+patchWidth/4)*mag,y00+y0*mag, x00+(x0+patchWidth*3/4)*mag,y00+y0*mag);
          g.drawLine(x00+(x0+patchWidth/4)*mag,y00+y1*mag, x00+(x0+patchWidth*3/4)*mag,y00+y1*mag);
          g.drawLine(x00+x0*mag,y00+(y0+patchHeight/4)*mag, x00+x0*mag,y00+(y0+patchHeight*3/4)*mag);
          g.drawLine(x00+x1*mag,y00+(y0+patchHeight/4)*mag, x00+x1*mag,y00+(y0+patchHeight*3/4)*mag);
        } else {  // row 1 or 2
          // A stopsign
          g.drawPolygon(new int[] {x00+(x0+patchWidth/2-2)*mag, x00+x0*mag, x00+x0*mag, x00+(x0+patchWidth/2-2)*mag, x00+(x0+patchWidth/2+2)*mag, x00+x1*mag, x00+x1*mag, x00+(x0+patchWidth/2+2)*mag},
                        new int[] {y00+y0*mag, y00+(y0+patchHeight/2-2)*mag, y00+(y0+patchHeight/2+2)*mag, y00+y1*mag, y00+y1*mag, y00+(y0+patchHeight/2+2)*mag, y00+(y0+patchHeight/2-2)*mag, y00+y0*mag},
                        8);
        }
      }
    }
    FontMetrics fm = g.getFontMetrics();
    {
      String[][] texts = {
          {"ANTIALIAS_OFF", "STROKE_NORMALIZE"},
          {"ANTIALIAS_OFF", "STROKE_PURE"},
          {"ANTIALIAS_ON", "STROKE_NORMALIZE"},
          {"ANTIALIAS_ON", "STROKE_PURE"},
      };
      for (int col = 0; col < 4; ++col) {
        int xCenter = borderThickness*mag + col*(patchWidth+borderThickness)*mag + patchWidth*mag/2;
        {
          int x = x00 + xCenter - fm.stringWidth(texts[col][0])/2;
          int y = y00 + 3*(patchHeight+borderThickness)*mag + fm.getAscent();
          g.drawString(texts[col][0], x,y);
          x = xCenter - fm.stringWidth(texts[col][1])/2;
          y += fm.getHeight();
          g.drawString(texts[col][1], x,y);
        }
      }
    }
    {
      String[] texts = {
        "drawLine",
        "drawPolygon",
        "fillPolygon",
      };
      for (int row = 0; row < 3; ++row) {
        int yCenter = y00 + borderThickness*mag + row*(patchHeight+borderThickness)*mag + patchHeight*mag/2;
        int x = x00 + 4*(patchWidth+borderThickness)*mag + 10;
        g.drawString(texts[row], x,yCenter);
      }
    }
  }  // showMagnifiedAndAnnotatedStudy


  private static Dimension figureOutPreferredSize(FontMetrics fm) {
    int preferredWidth = (totalWidth-borderThickness)*mag + 10 + fm.stringWidth("drawPolygon") + 9;
    int preferredHeight = fm.getHeight() + totalHeight + (totalHeight-borderThickness)*mag + 2*fm.getHeight() + 2;
    return new Dimension(preferredWidth, preferredHeight);
  }

  private static class IndirectExaminationView extends JComponent {
    public IndirectExaminationView() {
      setFont(new Font("Times", Font.PLAIN, 12));
      setPreferredSize(figureOutPreferredSize(getFontMetrics(getFont())));
    }
    @Override public void paintComponent(Graphics g) {
      FontMetrics fm = g.getFontMetrics();
      g.setColor(java.awt.Color.BLACK);
      g.drawString("through BufferedImage:", 0,fm.getAscent());
      // The following seem equivalent
      java.awt.image.BufferedImage studyImage = new java.awt.image.BufferedImage(totalWidth, totalHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
      //java.awt.image.BufferedImage studyImage = (BufferedImage)this.createImage(totalWidth, totalHeight);
      drawLittleStudy(studyImage.createGraphics(),
                      0,0,
                      patchWidth, patchHeight, borderThickness, totalWidth, totalHeight);
                    Graphics2D studyImageGraphics2D = studyImage.createGraphics();
      g.drawImage(studyImage,
                  /*dst*/ 0,fm.getHeight(),totalWidth,fm.getHeight()+totalHeight,
                  /*src*/ 0,0,totalWidth,totalHeight,
                  this);
      showMagnifiedAndAnnotatedStudy(g, studyImage,
                                     0,fm.getHeight()+totalHeight,
                                     patchWidth, patchHeight, borderThickness, totalWidth, totalHeight, mag, this);
    }
  }  // DirectExaminationView

  private static class DirectExaminationView extends JComponent {
    public DirectExaminationView() {
      setFont(new Font("Times", Font.PLAIN, 12));
      setPreferredSize(figureOutPreferredSize(getFontMetrics(getFont())));
    }
    private BufferedImage imgFromTheRobot = null;
    @Override public void paintComponent(Graphics g) {
      final FontMetrics fm = g.getFontMetrics();
      g.setColor(java.awt.Color.BLACK);
      g.drawString("direct to JComponent:", 0,fm.getAscent());
      drawLittleStudy((Graphics2D)g,
                      0,fm.getHeight(),
                      patchWidth, patchHeight, borderThickness, totalWidth, totalHeight);

      if (imgFromTheRobot != null) {
        System.out.println("              drawing image from robot");
        showMagnifiedAndAnnotatedStudy(g, imgFromTheRobot,
                                       0, fm.getHeight()+totalHeight,
                                       patchWidth, patchHeight, borderThickness, totalWidth, totalHeight, mag, this);
        imgFromTheRobot = null;
      } else {
        System.out.println("              scheduling a robot");
        g.drawString("*** SCREEN CAPTURE PENDING ***", 0, fm.getHeight()+totalHeight+fm.getHeight()+fm.getHeight());
        // Most reliable way to do it seems to be to put it on a timer after a delay.
        Timer timer = new Timer(1000/2, new ActionListener() {
          @Override public void actionPerformed(ActionEvent ae) {
            System.out.println("            in timer callback");
            Robot robot;
            try {
              robot = new Robot();
            } catch (AWTException e) {
              System.err.println("caught AWTException: "+e);
              throw new Error(e);
            }

            Point myTopLeftOnScreen = getLocationOnScreen();
            Rectangle rect = new Rectangle(
              myTopLeftOnScreen.x, myTopLeftOnScreen.y + fm.getHeight(),
              totalWidth,totalHeight);
            BufferedImage img = robot.createScreenCapture(rect);
            imgFromTheRobot = img;
            repaint();
            System.out.println("            out timer callback");
          }
        });
        timer.setRepeats(false);
        timer.start();
      }
    }
  }  // DirectExaminationView

  public static void main(String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      @Override public void run()
      {
        final JFrame directFrame = new JFrame("direct to JComponent") {{
          getContentPane().add(new DirectExaminationView());
          setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          pack();
          setLocation(0,0);
          setVisible(true);
        }};
        new JFrame("through BufferedImage") {{
          getContentPane().add(new IndirectExaminationView());
          setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          pack();
          setLocation(directFrame.getWidth(),0);
          setVisible(true);
        }};
      }
    });
  }
}  // class AntiAliasingStudy
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号