有时“图形笔从其经过的路径向右下方垂下”,有时则不然。我不清楚如何预测它何时会或不会,但我观察到 RenderingHints.VALUE_STROKE_PURE 有时可用于通过反复试验来改变行为。特别是,我发现如果您在程序中的 drawPolygon() 调用期间打开 STROKE_PURE,它会使它们与您的 fillPolygon() 调用匹配,如您所愿。
我做了一个小研究,展示了 STROKE_CONTROL 提示的效果,它是以下之一:
- STROKE_NORMALIZE(默认值,在我的系统上)
- STROKE_PURE
在以下电话中:
在两种抗锯齿模式下:
- ANTIALIASING_OFF
- ANTIALIASING_ON
还有一个更烦人的维度显然也很重要:
- 直接渲染到屏幕上的 JComponent
- 渲染为 BufferedImage。
以下是直接渲染到可见 JComponent 时的结果:

以下是渲染成 BufferedImage 时的结果:

(请注意两张图片不同的两种情况,即直接渲染与 BufferedImage 渲染不同:ANTIALIAS_OFF/STROKE_NORMALIZE/fillPolygon 和 ANTIALIAS_OFF/STROKE_PURE/drawPolygon。)
总的来说,整件事似乎没有太多押韵或理由。但我们可以根据以上图片做出以下具体观察:
观察 #1:如果您希望您的抗锯齿 drawPolygon()s 和抗锯齿 fillPolygon()s 匹配良好(原始问题),然后在抗锯齿 drawPolygon() 调用期间打开 STROKE_PURE。(在抗锯齿的 fillPolygon() 调用期间它是否打开并不重要。)
观察 #2:如果您希望您的抗锯齿 fillPolygon()s 和非抗锯齿 fillPolygon()s 匹配(因为,比如说,您的应用程序允许用户打开和关闭抗锯齿,而您不希望图片每次他们这样做时都会向西北和东南跳跃),然后在非抗锯齿的 fillPolygon() 调用期间打开 STROKE_PURE。
这是我用来生成上述图片的程序。我的结果来自在 linux 上使用 opensdk11 编译和运行它;我很想知道是否有人在不同的平台上得到任何不同的结果。
/** 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