Java Swing Metal Look & Feel title bar printing bug fix
bug description
Swingの印刷機能を用いるとき、Mac OS X の VMでは、次の状況で、例外が発生する.
- MetalのLook&Feelを使っている
- JInternalFrame等、タイトルバーがウィンドウの内部に描画されている
- ウィンドウ内部を printAll() メソッド等で印刷しようとする
印刷物上の,タイトルバーの描画が不完全となる.さらに残りの部分が印刷されない.
同様の振る舞いは Apple のMLでも報告されている:http://lists.apple.com/archives/java-dev/2003/Jul/msg00069.html
stack trace
java.lang.NullPointerException at javax.swing.plaf.metal.BumpBuffer.fillBumpBuffer(MetalBumps.java:195) at javax.swing.plaf.metal.BumpBuffer.<init>(MetalBumps.java:160) at javax.swing.plaf.metal.MetalBumps.getBuffer(MetalBumps.java:78) at javax.swing.plaf.metal.MetalBumps.paintIcon(MetalBumps.java:109) at javax.swing.plaf.metal.MetalInternalFrameTitlePane.paintComponent(MetalInternalFrameTitlePane.java:454) at javax.swing.JComponent.paint(JComponent.java:1006) : :
cause (原因)
- タイトルバーに表現された凸凹(bump) を描画するためのバッファを作成するために、Swingは GraphicsConfiguration#createCompatibleImage(int,int.int) を呼ぶ
- MacOSX の Swingでは、このメソッドは、apple.awt.CPrinterGraphicsConfig#createCompatibleImage(int,int.int) であるが、このメソッドは常に null を返す
- javax.swing.plaf.metal.BumpBuffer.fillBumpBuffer は、null であるイメージオブジェクトに対してbumpの描画を試み、NullPointerExceptionが発生する
fix (回避方法)
後で示すコード例のように、印刷時に使う Graphics2D のクラスにラッパーをかぶせ、 createCompatibleImageの呼び出し時に nullではなく適切な BufferedImageを返すようにする.
validity (このfixの正当性)
互換性のあるカラーマップから BufferedImage を生成するため、タイトルバーが正しく描画されることを確認した.
確認用コード
/** * AppleのJVMで ・Look&Feelが Metalのとき ・JInternalFrameがウィンドウ内部にあるとき * ・内部フレームのタイトルバーの描画に失敗して例外が発生する という振る舞いを防止する。 printメソッドを参照のこと。 * * @author keigoi * */ public class OSXMetalPrintingBugTest extends JFrame implements Printable { private static final long serialVersionUID = -1L; /** * 印刷用メソッド。ここに細工する。 */ public int print(Graphics g_, PageFormat pf, int page) throws PrinterException { if (page > 0) { return Printable.NO_SUCH_PAGE; } Graphics2D g = (Graphics2D) g_; g.translate(pf.getImageableX(), pf.getImageableY()); double scale = Math.min(pf.getImageableWidth() / this.getWidth(), pf .getImageableHeight() / this.getHeight()); g.scale(scale, scale); // ここで proxy をかました Graphics2Dオブジェクトを渡す this.printAll(new MyG((Graphics2D) g)); // this.printAll(g); // <-- 例外発生! return Printable.PAGE_EXISTS; } /** * ウィンドウ内部に JInternalFrameをもつ JFrameを作る */ public OSXMetalPrintingBugTest() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JDesktopPane pane = new JDesktopPane(); pane.add(createIFrame()); setContentPane(pane); } /** * 印刷ボタンを含んだ JInternalFrameを作る。 * * @return */ JInternalFrame createIFrame() { JInternalFrame internal = new JInternalFrame("internal frame", true, true, true, true); internal.setLayout(new FlowLayout()); internal.getContentPane().add(createButton()); internal.pack(); internal.setSize(300, 200); internal.setVisible(true); return internal; } /** * 印刷ボタンを作る。 * * @return ボタン */ JButton createButton() { JButton button = new JButton("Let's print!"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { PrinterJob job = PrinterJob.getPrinterJob(); PageFormat pf = job.defaultPage(); pf.setOrientation(PageFormat.LANDSCAPE); job.setPrintable(OSXMetalPrintingBugTest.this, pf); boolean ok = job.printDialog(); if (ok) { try { job.print(); } catch (PrinterException ex) { // pass } } } }); return button; } public static void main(String[] args) throws Throwable { UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); final OSXMetalPrintingBugTest frame = new OSXMetalPrintingBugTest(); frame.setSize(320, 240); frame.setVisible(true); } } /** * Graphics2D の getDeviceConfiguration 関数をフックして、プロキシをかましたオブジェクトを返すように 修正したクラス。 * createで作成するオブジェクトにもプロキシをかましている。 * * @author keigoi */ class MyG extends Graphics2D { Graphics2D g; public MyG(Graphics2D g) { this.g = g; } @Override public Graphics create(int x, int y, int width, int height) { return new MyG((Graphics2D) g.create(x, y, width, height)); } @Override public Graphics create() { return new MyG((Graphics2D) g.create()); } @Override public GraphicsConfiguration getDeviceConfiguration() { return new MyGC(g.getDeviceConfiguration()); } @Override public void addRenderingHints(Map<?, ?> arg0) { g.addRenderingHints(arg0); } @Override public void clearRect(int arg0, int arg1, int arg2, int arg3) { g.clearRect(arg0, arg1, arg2, arg3); } @Override public void clip(Shape arg0) { g.clip(arg0); } @Override public void clipRect(int arg0, int arg1, int arg2, int arg3) { g.clipRect(arg0, arg1, arg2, arg3); } @Override public void copyArea(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { g.copyArea(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public void dispose() { g.dispose(); } @Override public void draw(Shape arg0) { g.draw(arg0); } @Override public void draw3DRect(int x, int y, int width, int height, boolean raised) { g.draw3DRect(x, y, width, height, raised); } @Override public void drawArc(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { g.drawArc(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public void drawBytes(byte[] data, int offset, int length, int x, int y) { g.drawBytes(data, offset, length, x, y); } @Override public void drawChars(char[] data, int offset, int length, int x, int y) { g.drawChars(data, offset, length, x, y); } @Override public void drawGlyphVector(GlyphVector arg0, float arg1, float arg2) { g.drawGlyphVector(arg0, arg1, arg2); } @Override public void drawImage(BufferedImage arg0, BufferedImageOp arg1, int arg2, int arg3) { g.drawImage(arg0, arg1, arg2, arg3); } @Override public boolean drawImage(Image arg0, AffineTransform arg1, ImageObserver arg2) { return g.drawImage(arg0, arg1, arg2); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, Color arg3, ImageObserver arg4) { return g.drawImage(arg0, arg1, arg2, arg3, arg4); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, ImageObserver arg3) { return g.drawImage(arg0, arg1, arg2, arg3); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, int arg3, int arg4, Color arg5, ImageObserver arg6) { return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, int arg3, int arg4, ImageObserver arg5) { return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, Color arg9, ImageObserver arg10) { return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); } @Override public boolean drawImage(Image arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, ImageObserver arg9) { return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); } @Override public void drawLine(int arg0, int arg1, int arg2, int arg3) { g.drawLine(arg0, arg1, arg2, arg3); } @Override public void drawOval(int arg0, int arg1, int arg2, int arg3) { g.drawOval(arg0, arg1, arg2, arg3); } @Override public void drawPolygon(int[] arg0, int[] arg1, int arg2) { g.drawPolygon(arg0, arg1, arg2); } @Override public void drawPolygon(Polygon p) { g.drawPolygon(p); } @Override public void drawPolyline(int[] arg0, int[] arg1, int arg2) { g.drawPolyline(arg0, arg1, arg2); } @Override public void drawRect(int x, int y, int width, int height) { g.drawRect(x, y, width, height); } @Override public void drawRenderableImage(RenderableImage arg0, AffineTransform arg1) { g.drawRenderableImage(arg0, arg1); } @Override public void drawRenderedImage(RenderedImage arg0, AffineTransform arg1) { g.drawRenderedImage(arg0, arg1); } @Override public void drawRoundRect(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { g.drawRoundRect(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public void drawString(AttributedCharacterIterator arg0, float arg1, float arg2) { g.drawString(arg0, arg1, arg2); } @Override public void drawString(AttributedCharacterIterator arg0, int arg1, int arg2) { g.drawString(arg0, arg1, arg2); } @Override public void drawString(String arg0, float arg1, float arg2) { g.drawString(arg0, arg1, arg2); } @Override public void drawString(String arg0, int arg1, int arg2) { g.drawString(arg0, arg1, arg2); } @Override public boolean equals(Object obj) { return g.equals(obj); } @Override public void fill(Shape arg0) { g.fill(arg0); } @Override public void fill3DRect(int x, int y, int width, int height, boolean raised) { g.fill3DRect(x, y, width, height, raised); } @Override public void fillArc(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { g.fillArc(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public void fillOval(int arg0, int arg1, int arg2, int arg3) { g.fillOval(arg0, arg1, arg2, arg3); } @Override public void fillPolygon(int[] arg0, int[] arg1, int arg2) { g.fillPolygon(arg0, arg1, arg2); } @Override public void fillPolygon(Polygon p) { g.fillPolygon(p); } @Override public void fillRect(int arg0, int arg1, int arg2, int arg3) { g.fillRect(arg0, arg1, arg2, arg3); } @Override public void fillRoundRect(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { g.fillRoundRect(arg0, arg1, arg2, arg3, arg4, arg5); } @Override public void finalize() { g.finalize(); } @Override public Color getBackground() { return g.getBackground(); } @Override public Shape getClip() { return g.getClip(); } @Override public Rectangle getClipBounds() { return g.getClipBounds(); } @Override public Rectangle getClipBounds(Rectangle r) { return g.getClipBounds(r); } @Override @SuppressWarnings("deprecation") public Rectangle getClipRect() { return g.getClipRect(); } @Override public Color getColor() { return g.getColor(); } @Override public Composite getComposite() { return g.getComposite(); } @Override public Font getFont() { return g.getFont(); } @Override public FontMetrics getFontMetrics() { return g.getFontMetrics(); } @Override public FontMetrics getFontMetrics(Font arg0) { return g.getFontMetrics(arg0); } @Override public FontRenderContext getFontRenderContext() { return g.getFontRenderContext(); } @Override public Paint getPaint() { return g.getPaint(); } @Override public Object getRenderingHint(Key arg0) { return g.getRenderingHint(arg0); } @Override public RenderingHints getRenderingHints() { return g.getRenderingHints(); } @Override public Stroke getStroke() { return g.getStroke(); } @Override public AffineTransform getTransform() { return g.getTransform(); } @Override public int hashCode() { return g.hashCode(); } @Override public boolean hit(Rectangle arg0, Shape arg1, boolean arg2) { return g.hit(arg0, arg1, arg2); } @Override public boolean hitClip(int x, int y, int width, int height) { return g.hitClip(x, y, width, height); } @Override public void rotate(double arg0, double arg1, double arg2) { g.rotate(arg0, arg1, arg2); } @Override public void rotate(double arg0) { g.rotate(arg0); } @Override public void scale(double arg0, double arg1) { g.scale(arg0, arg1); } @Override public void setBackground(Color arg0) { g.setBackground(arg0); } @Override public void setClip(int arg0, int arg1, int arg2, int arg3) { g.setClip(arg0, arg1, arg2, arg3); } @Override public void setClip(Shape arg0) { g.setClip(arg0); } @Override public void setColor(Color arg0) { g.setColor(arg0); } @Override public void setComposite(Composite arg0) { g.setComposite(arg0); } @Override public void setFont(Font arg0) { g.setFont(arg0); } @Override public void setPaint(Paint arg0) { g.setPaint(arg0); } @Override public void setPaintMode() { g.setPaintMode(); } @Override public void setRenderingHint(Key arg0, Object arg1) { g.setRenderingHint(arg0, arg1); } @Override public void setRenderingHints(Map<?, ?> arg0) { g.setRenderingHints(arg0); } @Override public void setStroke(Stroke arg0) { g.setStroke(arg0); } @Override public void setTransform(AffineTransform arg0) { g.setTransform(arg0); } @Override public void setXORMode(Color arg0) { g.setXORMode(arg0); } @Override public void shear(double arg0, double arg1) { g.shear(arg0, arg1); } @Override public String toString() { return g.toString(); } @Override public void transform(AffineTransform arg0) { g.transform(arg0); } @Override public void translate(double arg0, double arg1) { g.translate(arg0, arg1); } @Override public void translate(int arg0, int arg1) { g.translate(arg0, arg1); } } /** * Appleの 印刷用 GraphicsConfigurationのバグを修正するクラス。 createCompatibleImage のみ動作が異なる。 * 他はコンストラクタでセットしたオブジェクトに委譲する。 * * @author keigoi */ class MyGC extends GraphicsConfiguration { GraphicsConfiguration gc; public MyGC(GraphicsConfiguration gc) { this.gc = gc; } /** * Appleの印刷用 createCompatibleImage はいつも nullを返すので、そのかわりに BufferedImage を返す */ @Override public BufferedImage createCompatibleImage(int width, int height, int transparency) { ColorModel cm = gc.getColorModel(transparency); WritableRaster raster = cm .createCompatibleWritableRaster(width, height); return new BufferedImage(cm, raster, false, new Hashtable<Object, Object>()); } /** * Appleの印刷用 createCompatibleImage はいつも nullを返すので、そのかわりに BufferedImage を返す */ @Override public BufferedImage createCompatibleImage(int width, int height) { ColorModel cm = gc.getColorModel(); WritableRaster raster = cm .createCompatibleWritableRaster(width, height); return new BufferedImage(cm, raster, false, new Hashtable<Object, Object>()); } @Override public VolatileImage createCompatibleVolatileImage(int width, int height, ImageCapabilities caps, int transparency) throws AWTException { return gc.createCompatibleVolatileImage(width, height, caps, transparency); } @Override public VolatileImage createCompatibleVolatileImage(int width, int height, ImageCapabilities caps) throws AWTException { return gc.createCompatibleVolatileImage(width, height, caps); } @Override public VolatileImage createCompatibleVolatileImage(int width, int height, int transparency) { return gc.createCompatibleVolatileImage(width, height, transparency); } @Override public VolatileImage createCompatibleVolatileImage(int width, int height) { return gc.createCompatibleVolatileImage(width, height); } @Override public boolean equals(Object obj) { return gc.equals(obj); } @Override public Rectangle getBounds() { return gc.getBounds(); } @Override public BufferCapabilities getBufferCapabilities() { return gc.getBufferCapabilities(); } @Override public ColorModel getColorModel() { return gc.getColorModel(); } @Override public ColorModel getColorModel(int transparency) { return gc.getColorModel(transparency); } @Override public AffineTransform getDefaultTransform() { return gc.getDefaultTransform(); } @Override public GraphicsDevice getDevice() { return gc.getDevice(); } @Override public ImageCapabilities getImageCapabilities() { return gc.getImageCapabilities(); } @Override public AffineTransform getNormalizingTransform() { return gc.getNormalizingTransform(); } @Override public int hashCode() { return gc.hashCode(); } @Override public String toString() { return gc.toString(); } }