The Shape interface is implemented by objects of Java 2D (Arc2D, Area, CubicCurve2D, Ellipse2D, GeneralPath etc..).
Some of the concrete objects are marked as Serializable and can be stored and restored using object serialization, but others like Area do not implement the interface and throw errors.
But since we are constantly warned that such naive serialization is not necessarily stable across Java implementations or versions, I'd prefer to use some form of serialization that is.
That leads us to storing/restoring from XML using XMLEncoder and XMLDecoder, but that is capable of handling even less of the Java 2D Shape objects.
Some results for both can be seen below. We start with 6 shapes, and attempt to store/restore them via object serialization and standard XML serialization.
How would we store all Shape objects correctly via XML?
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.TitledBorder;
public class Serialize2D {
private JPanel ui;
Serialize2D() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
ui = new JPanel(new GridLayout(0, 1));
int[] xpoints = {205, 295, 205, 295};
int[] ypoints = {5, 25, 25, 45};
Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length);
ArrayList<Shape> shapes = new ArrayList<Shape>();
int w = 45;
shapes.add(new Rectangle2D.Double(5, 5, 90, 40));
shapes.add(new Ellipse2D.Double(105, 5, 90, 40));
shapes.add(polygon);
shapes.add(new GeneralPath(new Rectangle2D.Double(5, 55, 90, 40)));
shapes.add(new Path2D.Double(new Rectangle2D.Double(105, 55, 90, 40)));
shapes.add(new Area(new Rectangle2D.Double(205, 55, 90, 40)));
addTitledLabelToPanel(shapes, "Original Shapes");
addTitledLabelToPanel(
serializeToFromObject(shapes), "Serialize via Object");
addTitledLabelToPanel(
serializeToFromXML(shapes), "Serialize via XML");
}
public JComponent getUI() {
return ui;
}
public ArrayList<Shape> serializeToFromObject(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
for (Shape shape : shapes) {
try {
oos.writeObject(shape);
} catch (Exception ex) {
System.err.println(ex.toString());
}
}
oos.flush();
oos.close();
System.out.println("length Obj: " + baos.toByteArray().length);
ByteArrayInputStream bais = new ByteArrayInputStream(
baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = null;
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
while (o != null) {
shps.add((Shape) o);
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
return shps;
} catch (IOException ex) {
ex.printStackTrace();
}
return shps;
}
public ArrayList<Shape> serializeToFromXML(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XMLEncoder xmle = new XMLEncoder(baos);
for (Shape shape : shapes) {
xmle.writeObject(shape);
}
xmle.flush();
xmle.close();
System.out.println("length XML: " + baos.toByteArray().length);
ByteArrayInputStream bais
= new ByteArrayInputStream(baos.toByteArray());
XMLDecoder xmld = new XMLDecoder(bais);
Shape shape = (Shape) xmld.readObject();
while (shape != null) {
shps.add(shape);
try {
shape = (Shape) xmld.readObject();
} catch (ArrayIndexOutOfBoundsException aioobe) {
// we've read last object
shape = null;
}
}
xmld.close();
} catch (Exception ex) {
ex.printStackTrace();
}
return shps;
}
private final static String getType(Object o) {
String s = o.getClass().getName();
String[] parts = s.split("\\.");
s = parts[parts.length - 1].split("\\$")[0];
return s;
}
public static void drawShapesToImage(
ArrayList<Shape> shapes, BufferedImage image) {
Graphics2D g = image.createGraphics();
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
for (Shape shape : shapes) {
String s = getType(shape);
g.setColor(Color.GREEN);
g.fill(shape);
g.setColor(Color.BLACK);
g.draw(shape);
Rectangle r = shape.getBounds();
int x = r.x + 5;
int y = r.y + 16;
if (r.width * r.height != 0) {
g.drawString(s, x, y);
}
}
g.dispose();
}
private void addTitledLabelToPanel(ArrayList<Shape> shapes, String title) {
int w = 300;
int h = 100;
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
drawShapesToImage(shapes, bi);
JLabel l = new JLabel(new ImageIcon(bi));
l.setBorder(new TitledBorder(title));
ui.add(l);
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
Serialize2D ss = new Serialize2D();
JOptionPane.showMessageDialog(null, ss.getUI());
}
};
SwingUtilities.invokeLater(r);
}
}
Unfortunately, naive encoding/decoding of a Shape to XML using XMLEncoder/Decoder often destroys all the vital information of the Shape!
So to do this, still using the above mentioned classes, we serialize and restore properly constructed beans that represent the parts of the shape as obtained from a PathIterator. These beans are:
PathBean which stores the collection of PathSegment objects that form the shape of the Java-2D Shape.
PathSegment which stores the details of a particular part of the path (segment type, winding rule & coords).
SerializeShapes GUI
A GUI to demonstrate storing and restoring shapes.
Click the Ellipse (Ellipse2D), Rectangle (Rectangle2D) or Face (Area) buttons a couple of times.
Exit the GUI. The shapes will be serialized to disk.
Restart the GUI. The randomly drawn shapes from last time will be restored from disk & reappear in the GUI.
The selected shape will be filled in green, other shapes in red.
package serialize2d;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
/** A GUI to make it easy to add/remove shapes from a canvas.
It should persist the shapes between runs. */
public class SerializeShapes {
JPanel ui;
JPanel shapePanel;
Random rand;
JPanel shapeCanvas;
DefaultListModel<Shape> allShapesModel;
ListSelectionModel shapeSelectionModel;
RenderingHints renderingHints;
SerializeShapes() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
renderingHints = new RenderingHints(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
renderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
renderingHints.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE);
ui = new JPanel(new BorderLayout(4, 4));
ui.setBorder(new EmptyBorder(4, 4, 4, 4));
JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
ui.add(controls, BorderLayout.PAGE_START);
shapeCanvas = new ShapeCanvas();
ui.add(shapeCanvas);
rand = new Random();
allShapesModel = new DefaultListModel<Shape>();
JList<Shape> allShapes = new JList<Shape>(allShapesModel);
allShapes.setCellRenderer(new ShapeListCellRenderer());
shapeSelectionModel = allShapes.getSelectionModel();
shapeSelectionModel.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
ListSelectionListener shapesSelectionListener
= new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent e) {
shapeCanvas.repaint();
}
};
allShapes.addListSelectionListener(shapesSelectionListener);
JScrollPane shapesScroll = new JScrollPane(
allShapes,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
// TODO fix this hack..
shapesScroll.getViewport().setPreferredSize(new Dimension(60, 200));
ui.add(shapesScroll, BorderLayout.LINE_START);
Action addEllipse = new AbstractAction("Ellipse") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h);
addShape(ellipse);
}
};
addEllipse.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E);
Action addRectangle = new AbstractAction("Rectangle") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Rectangle2D rectangle = new Rectangle2D.Double(x, y, w, h);
addShape(rectangle);
}
};
addRectangle.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
final int faceStart = 128513;
final int faceEnd = 128528;
final int diff = faceEnd - faceStart;
StringBuilder sb = new StringBuilder();
for (int count = faceStart; count <= faceEnd; count++) {
sb.append(Character.toChars(count));
}
final String s = sb.toString();
Vector<Font> compatibleFontList = new Vector<Font>();
GraphicsEnvironment ge
= GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] fonts = ge.getAllFonts();
for (Font font : fonts) {
if (font.canDisplayUpTo(s) < 0) {
compatibleFontList.add(font);
}
}
JComboBox fontChooser = new JComboBox(compatibleFontList);
ListCellRenderer fontRenderer = new DefaultListCellRenderer() {
#Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(
list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Font font = (Font) value;
l.setText(font.getName());
return l;
}
};
fontChooser.setRenderer(fontRenderer);
final ComboBoxModel<Font> fontModel = fontChooser.getModel();
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();
final FontRenderContext fontRenderContext = g.getFontRenderContext();
Action addFace = new AbstractAction("Face") {
#Override
public void actionPerformed(ActionEvent e) {
int codepoint = faceStart + rand.nextInt(diff);
String text = new String(Character.toChars(codepoint));
Font font = (Font) fontModel.getSelectedItem();
Area area = new Area(
font.deriveFont(80f).
createGlyphVector(fontRenderContext, text).
getOutline());
Rectangle bounds = area.getBounds();
float x = rand.nextInt(
shapeCanvas.getWidth() - bounds.width) - bounds.x;
float y = rand.nextInt(
shapeCanvas.getHeight() - bounds.height) - bounds.y;
AffineTransform move = AffineTransform.
getTranslateInstance(x, y);
area.transform(move);
addShape(area);
}
};
addFace.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
Action delete = new AbstractAction("Delete") {
#Override
public void actionPerformed(ActionEvent e) {
int idx = shapeSelectionModel.getMinSelectionIndex();
if (idx < 0) {
JOptionPane.showMessageDialog(
ui,
"Select a shape to delete",
"Select a Shape",
JOptionPane.ERROR_MESSAGE);
} else {
allShapesModel.removeElementAt(idx);
shapeCanvas.repaint();
}
}
};
delete.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
controls.add(new JButton(addEllipse));
controls.add(new JButton(addRectangle));
controls.add(new JButton(addFace));
controls.add(fontChooser);
controls.add(new JButton(delete));
try {
ArrayList<Shape> shapes = deserializeShapes();
for (Shape shape : shapes) {
allShapesModel.addElement(shape);
}
} catch (Exception ex) {
System.err.println("If first launch, this is as expected!");
ex.printStackTrace();
}
}
private void addShape(Shape shape) {
allShapesModel.addElement(shape);
int size = allShapesModel.getSize() - 1;
shapeSelectionModel.addSelectionInterval(size, size);
}
class ShapeCanvas extends JPanel {
ShapeCanvas() {
setBackground(Color.WHITE);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Stroke stroke = new BasicStroke(1.5f);
g2.setStroke(stroke);
int idx = shapeSelectionModel.getMinSelectionIndex();
Shape selectedShape = null;
if (idx > -1) {
selectedShape = allShapesModel.get(idx);
}
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
if (shape.equals(selectedShape)) {
g2.setColor(new Color(0, 255, 0, 191));
} else {
g2.setColor(new Color(255, 0, 0, 191));
}
g2.fill(shape);
g2.setColor(new Color(0, 0, 0, 224));
g2.draw(shape);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(500, 300);
}
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
SerializeShapes se = new SerializeShapes();
JFrame f = new JFrame("Serialize Shapes");
f.addWindowListener(new SerializeWindowListener(se));
f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
f.setContentPane(se.getUI());
f.setResizable(false);
f.pack();
f.setLocationByPlatform(true);
f.setVisible(true);
}
};
SwingUtilities.invokeLater(r);
}
public void serializeShapes() throws FileNotFoundException {
ArrayList<Shape> shapes
= new ArrayList<Shape>();
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
shapes.add(shape);
}
ShapeIO.serializeShapes(shapes, this.getClass());
try {
Desktop.getDesktop().open(
ShapeIO.getSerializeFile(this.getClass()));
} catch (Exception e) {
e.printStackTrace();
}
}
public ArrayList<Shape> deserializeShapes() throws FileNotFoundException {
return ShapeIO.deserializeShapes(this.getClass());
}
class ShapeListCellRenderer extends DefaultListCellRenderer {
#Override
public Component getListCellRendererComponent(
JList<? extends Object> list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Shape shape = (Shape) value;
ShapeIcon icon = new ShapeIcon(shape, 40);
l.setIcon(icon);
l.setText("");
return l;
}
}
class ShapeIcon implements Icon {
Shape shape;
int size;
ShapeIcon(Shape shape, int size) {
this.shape = shape;
this.size = size;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Rectangle bounds = shape.getBounds();
int xOff = -bounds.x;
int yOff = -bounds.y;
double xRatio = (double) bounds.width / (double) size;
double yRatio = (double) bounds.height / (double) size;
double ratio = xRatio > yRatio ? xRatio : yRatio;
AffineTransform scale = AffineTransform.getScaleInstance(1 / ratio, 1 / ratio);
AffineTransform shift = AffineTransform.getTranslateInstance(xOff, yOff);
AffineTransform totalTransform = new AffineTransform();
totalTransform.concatenate(scale);
totalTransform.concatenate(shift);
Area b = new Area(shape).createTransformedArea(totalTransform);
bounds = b.getBounds();
g2.setColor(Color.BLACK);
g2.fill(b);
}
#Override
public int getIconWidth() {
return size;
}
#Override
public int getIconHeight() {
return size;
}
}
}
class SerializeWindowListener extends WindowAdapter {
SerializeShapes serializeShapes;
SerializeWindowListener(SerializeShapes serializeShapes) {
this.serializeShapes = serializeShapes;
}
#Override
public void windowClosing(WindowEvent e) {
try {
serializeShapes.serializeShapes();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}
}
ShapeIO
Performs the I/O to/from XML.
package serialize2d;
import java.awt.Shape;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
public class ShapeIO {
/** Save the list of shapes to the file system. */
public static void serializeShapes(
ArrayList<Shape> shapes, Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLEncoder xmle = new XMLEncoder(new FileOutputStream(f));
ArrayList<PathBean> pathSegmentsCollection = new ArrayList<>();
for (Shape shape : shapes) {
ArrayList<PathSegment> pathSegments =
BeanConverter.getSegmentsFromShape(shape);
PathBean as = new PathBean(pathSegments);
pathSegmentsCollection.add(as);
}
xmle.writeObject(pathSegmentsCollection);
xmle.flush();
xmle.close();
}
/** Load the list of shapes from the file system. */
public static ArrayList<Shape> deserializeShapes(Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLDecoder xmld = new XMLDecoder(new FileInputStream(f));
ArrayList<PathBean> pathSegmentsCollection
= (ArrayList<PathBean>) xmld.readObject();
ArrayList<Shape> shapes = new ArrayList<Shape>();
for (PathBean pathSegments : pathSegmentsCollection) {
shapes.add(BeanConverter.getShapeFromSegments(pathSegments));
}
return shapes;
}
/** Provide an unique, reproducible & readable/writable path for a class. */
public static File getSerializeFile(Class serializeClass) {
File f = new File(System.getProperty("user.home"));
String[] nameParts = serializeClass.getCanonicalName().split("\\.");
f = new File(f, "java");
for (String namePart : nameParts) {
f = new File(f, namePart);
}
f.mkdirs();
f = new File(f, nameParts[nameParts.length-1] + ".xml");
return f;
}
}
BeanConverter
Obtains a PathIterator from the Shape and converts it to a serializable bean. Converts the bean back into a GeneralPath.
package serialize2d;
import java.awt.Shape;
import java.awt.geom.*;
import java.util.ArrayList;
/** Utility class to convert bean to/from a Shape. */
public class BeanConverter {
/** Convert a shape to a serializable bean. */
public static ArrayList<PathSegment> getSegmentsFromShape(Shape shape) {
ArrayList<PathSegment> shapeSegments = new ArrayList<PathSegment>();
for (
PathIterator pi = shape.getPathIterator(null);
!pi.isDone();
pi.next()) {
double[] coords = new double[6];
int pathSegmentType = pi.currentSegment(coords);
int windingRule = pi.getWindingRule();
PathSegment as = new PathSegment(
pathSegmentType, windingRule, coords);
shapeSegments.add(as);
}
return shapeSegments;
}
/** Convert a serializable bean to a shape. */
public static Shape getShapeFromSegments(PathBean shapeSegments) {
GeneralPath gp = new GeneralPath();
for (PathSegment shapeSegment : shapeSegments.getPathSegments()) {
double[] coords = shapeSegment.getCoords();
int pathSegmentType = shapeSegment.getPathSegmentType();
int windingRule = shapeSegment.getWindingRule();
gp.setWindingRule(windingRule);
if (pathSegmentType == PathIterator.SEG_MOVETO) {
gp.moveTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_LINETO) {
gp.lineTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_QUADTO) {
gp.quadTo(coords[0], coords[1], coords[2], coords[3]);
} else if (pathSegmentType == PathIterator.SEG_CUBICTO) {
gp.curveTo(
coords[0], coords[1], coords[2],
coords[3], coords[4], coords[5]);
} else if (pathSegmentType == PathIterator.SEG_CLOSE) {
gp.closePath();
} else {
System.err.println("Unexpected value! " + pathSegmentType);
}
}
return gp;
}
}
PathBean
Stores a collection of path segments in a seriallizable bean.
package serialize2d;
import java.awt.geom.*;
import java.util.ArrayList;
/** PathBean stores the collection of PathSegment objects
that constitute the path of a Shape. */
public class PathBean {
public ArrayList<PathSegment> pathSegments;
public PathBean() {}
public PathBean(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
public ArrayList<PathSegment> getPathSegments() {
return pathSegments;
}
public void setPathSegments(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
for (PathSegment pathSegment : pathSegments) {
sb.append(" \n\t");
sb.append(pathSegment.toString());
}
sb.append(" \n");
sb.append("}");
return "PathSegments: " + sb.toString();
}
}
PathSegment
Stores the path segment of one part of the entire path.
package serialize2d;
import java.util.Arrays;
/** PathSegment bean stores the detail on one segment of the path
that constitutes a Shape. */
public class PathSegment {
public int pathSegmentType;
public int windingRule;
public double[] coords;
public PathSegment() {}
public PathSegment(int pathSegmentType, int windingRule, double[] coords) {
this.pathSegmentType = pathSegmentType;
this.windingRule = windingRule;
this.coords = coords;
}
public int getPathSegmentType() {
return pathSegmentType;
}
public void setPathSegmentType(int pathSegmentType) {
this.pathSegmentType = pathSegmentType;
}
public int getWindingRule() {
return windingRule;
}
public void setWindingRule(int windingRule) {
this.windingRule = windingRule;
}
public double[] getCoords() {
return coords;
}
public void setCoords(double[] coords) {
this.coords = coords;
}
#Override
public String toString() {
String sC = (coords != null ? "" : Arrays.toString(coords));
String s = String.format(
"PathSegment: Path Segment Type:- %d \t"
+ "Winding Rule:- %d \tcoords:- %s",
getPathSegmentType(), getWindingRule(), sC);
return s;
}
}
Notes
This is intended as a proof of concept as opposed to a polished approach.
XML serialized data becomes big real fast, it would normally be zipped. Zip compression might shave 30-40% off the byte size of a serialized object or a class file, but 80-95% off XML. In any case, zip works well for the next point as well.
For the type of project where we wish to offer to serialize and restore shapes, we'll probably also want to include more details of the shapes (e.g. fill color or texture and draw color or stroke etc.) as well as other data like images or fonts. This is also where Zip comes in handy, since we can put them all in the same archive, each with best levels of compression (e.g. standard for the XML and none for images).
A zip archive of the source files in this answer can be downloaded from my cloud drive.
A custom PersistenceDelegate can be used with XMLEncoder to serialize a Path2D or GeneralPath to XML.
Consider the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
<void method="moveTo">
<float>1.0</float>
<float>1.0</float>
</void>
<void method="lineTo">
<float>2.0</float>
<float>0.0</float>
</void>
<void method="lineTo">
<float>0.0</float>
<float>3.0</float>
</void>
<void method="closePath"/>
</object>
</java>
When read by an XMLEncoder instance, the following commands will be executed ...
Path2D.Float object = new Path2D.Float();
object.setWindingRule(0); // Note: 0 => Path2D.WIND_EVEN_ODD
object.moveTo(1.0, 1.0);
object.lineTo(2.0, 0.0);
object.lineTo(0.0, 3.0);
object.closePath();
... and a closed triangle object will be returned by XMLDecoder.readObject().
Based on this, we can conclude that XMLDecoder can already deserialize a Path2D shape, if it is properly encoded. What does the XMLEncoder do for us now?
Path2D.Float path = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 10);
path.moveTo(1, 1);
path.lineTo(2, 0);
path.lineTo(0, 3);
path.closePath();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.writeObject(path);
}
This produces the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
</object>
</java>
Not great, but not too bad. We're just missing the path data. So we just need to extend the DefaultPersistenceDelegate to add the required path commands to the output.
public class Path2DPersistenceDelegate extends DefaultPersistenceDelegate {
#Override
protected void initialize(Class<?> cls, Object oldInstance, Object newInstance, Encoder out) {
super.initialize(cls, oldInstance, newInstance, out);
Shape shape = (Shape) oldInstance;
float coords[] = new float[6];
Float pnt0[] = new Float[0];
Float pnt1[] = new Float[2];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6];
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = pnt0;
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = pnt1;
break;
case PathIterator.SEG_LINETO:
cmd = "lineTo";
pnts = pnt1;
break;
case PathIterator.SEG_QUADTO:
cmd = "quadTo";
pnts = pnt2;
break;
case PathIterator.SEG_CUBICTO:
cmd = "curveTo";
pnts = pnt3;
break;
default:
throw new IllegalStateException("Unexpected segment type: " + type);
}
for (int i = 0; i < pnts.length; i++) {
pnts[i] = coords[i];
}
out.writeStatement(new Statement(oldInstance, cmd, pnts));
iterator.next();
}
}
}
Then, we just register this persistence delegate with the XMLEncoder, and it will produce the XML shown at the top of this post.
Path2DPersistenceDelegate path2d_delegate = new Path2DPersistenceDelegate();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.setPersistenceDelegate(Path2D.Float.class, path2d_delegate);
xml.writeObject(path);
}
Since Path2D.Float is the parent class of GeneralPath, a GeneralPath will also be encoded properly. If you want properly encode Path2D.Double shapes, you will need to modify the delegate to use double values and Double objects.
Update:
To construct the Path2D.Float object with the proper windingRule property instead of setting the property afterwards, add the following constructor to the Path2DPersistenceDelegate:
public Path2DPersistenceDelegate() {
super(new String[] { "windingRule" });
}
The XML will then read:
...
<object class="java.awt.geom.Path2D$Float">
<int>0</int>
<void method="moveTo">
...
This loses some human-readable context information in the XML; a human would need to read the documentation to determine that with the Path2D.Float(int) constructor, the int parameter is the windingRule property.
Update 2:
The Polygon persistence delegate is fairly simple:
public class PolygonPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Polygon polygon = (Polygon) oldInstance;
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { polygon.xpoints, polygon.ypoints, polygon.npoints });
}
}
Since Area Constructive Area Geometry object is more complex, it cannot be created by moveTo and lineTo type methods, but rather only by adding, subtracting, or exclusive-or-ing Shape objects. But the constructor takes a Shape object, and a Path2D.Double can be constructed from an Area object, so the persistence delegate actually can be written quite simply as well:
public class AreaPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Area area = (Area) oldInstance;
Path2D.Double p2d = new Path2D.Double(area);
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { p2d });
}
}
Since we are using Path2D.Double internally, we would need to add both persistent delegates to the XMLEncoder:
try (XMLEncoder encoder = new XMLEncoder(baos)) {
encoder.setPersistenceDelegate(Area.class, new AreaPersistenceDelegate());
encoder.setPersistenceDelegate(Path2D.Double.class, new Path2DPersistenceDelegate.Double());
encoder.writeObject(area);
}
Update 3:
A project with the PersistenceDelegate for Area, Path2D and GeneralPath has been created on GitHub.
Notes:
The persistence delegate for Polygon was removed, as it seems to be unnecessary for Java 1.7
Update 4:
For Java 1.7, the pnts array must be allocated for each new Statement(); it cannot be allocated once and reused. Thus, the Path2D delegates must be changed as follows:
float coords[] = new float[6];
/* Removed: Float pnt0[] = new Float[0];
Float pnt1[] = new Float[0];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6]; */
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = new Float[0]; // Allocate for each segment
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = new Float[2]; // Allocate for each segment
break;
/* ... etc ...*/
Related
I have multiple games running on 60 frame per second rate. I need to capture each frame and clone into four different screen with specific filter.
Software will work on dual screen monitor where primary and secondary screen will be responsible for running game and displaying four filter view respectively.
Limitation on applying filter:
Filter will work on image only.
Filter is written in java which almost impossible to rewrite.
So I am applying logic something like below:
import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import com.sun.jna.platform.win32.GDI32Util;
import com.sun.jna.platform.win32.WinDef.HWND;
import java.awt.image.BufferedImage;
public class MultiFrameApplet implements Runnable
{
public JFrame currentFrame = null;
public PanelPaint currentCanvas = null;
public BufferedImage screenshotImage = null;
com.sun.jna.platform.win32.User32 user32 = null;
HWND hwnd = null;
Thread th = null;
String CurrentFrameText = "";
private long lastTime;
private double fps; //could be int or long for integer values
public MultiFrameApplet(int filtertype)
{
main(null,filtertype);
}
public void main(String[] argv,int filtertype)
{
try
{
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] gd = ge.getScreenDevices();
//First Screen
GraphicsConfiguration gcFirst = gd[0].getDefaultConfiguration();
Toolkit toolkit = Toolkit.getDefaultToolkit();
if (toolkit == null)
{
return;
}
Rectangle screenRectFirst = gcFirst.getBounds();
Insets screenInsetsFirst = toolkit.getScreenInsets(gcFirst);
screenRectFirst.x = screenInsetsFirst.left;
screenRectFirst.y = screenInsetsFirst.top;
Robot robot = new Robot(gcFirst.getDevice());
//Second Screen
GraphicsConfiguration gcSecond = gd[1].getDefaultConfiguration();
Rectangle screenRectSecond = gcSecond.getBounds();
Insets screenInsetsSecond = Toolkit.getDefaultToolkit().getScreenInsets(gcSecond);
Rectangle effectiveScreenArea = new Rectangle();
/*Remove start bar area*/
effectiveScreenArea.x = screenRectSecond.x + screenInsetsSecond.left;
effectiveScreenArea.y = screenRectSecond.y + screenInsetsSecond.top;
effectiveScreenArea.height = screenRectSecond.height - screenInsetsSecond.top - screenInsetsSecond.bottom;
effectiveScreenArea.width = screenRectSecond.width - screenInsetsSecond.left - screenInsetsSecond.right;
//Scaling will decide capture image needs to shrink or not.!
double xscaling = 0;
double yscaling = 0;
screenshotImage = robot.createScreenCapture(screenRectFirst);
int differenceWidth = screenRectSecond.width / screenRectFirst.width;
int differenceheight = screenRectSecond.height / screenRectFirst.height;
xscaling = differenceWidth / 2.0;
yscaling = differenceheight / 2.0;
yscaling = yscaling - 0.018;
currentFrame = new JFrame();
currentFrame.setSize((int)effectiveScreenArea.width/2, (int)effectiveScreenArea.height/2);
if(filtertype == 0)
{
currentFrame.setLocation(effectiveScreenArea.x, effectiveScreenArea.y);
currentFrame.setTitle("First");
CurrentFrameText = "First";
}
else if(filtertype == 1)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2), effectiveScreenArea.y);
currentFrame.setTitle("Second");
CurrentFrameText = "Second";
}
else if(filtertype == 2)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2), effectiveScreenArea.y);
currentFrame.setTitle("Third");
CurrentFrameText = "Third";
}
else if(filtertype == 3)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2),effectiveScreenArea.y + ((int)effectiveScreenArea.height/2));
currentFrame.setTitle("Forth");
CurrentFrameText = "Forth";
}
currentCanvas = new PanelPaint((int)effectiveScreenArea.width/2,(int)effectiveScreenArea.height/2,xscaling,yscaling,CurrentFrameText);
currentCanvas.xpos = (int)effectiveScreenArea.width/2;
currentCanvas.ypos = (int)effectiveScreenArea.height/2;
currentFrame.getContentPane().add(currentCanvas);
currentFrame.setUndecorated(true);
currentFrame.setVisible(true);
user32 = com.sun.jna.platform.win32.User32.INSTANCE;
hwnd = user32.GetDesktopWindow();
th = new Thread(this);
th.start();
}
catch (AWTException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void close()
{
currentFrame.dispose();
currentFrame = null;
currentCanvas.close();
currentCanvas = null;
screenshotImage = null;
user32 = null;
hwnd = null;
th = null;
}
#Override
public void run()
{
while(true)
{
lastTime = System.nanoTime();
screenshotImage = GDI32Util.getScreenshot(hwnd);
///screenshotImage = screenshotImage.Convert(); //Place where filter applied
currentCanvas.setImg(screenshotImage,fps);
java.awt.EventQueue.invokeLater(currentCanvas::repaint);
fps = 1000000000.0 / (System.nanoTime() - lastTime); //one second(nano) divided by amount of time it takes for one frame to finish
lastTime = System.nanoTime();
}
}
}
#SuppressWarnings("serial")
class PanelPaint extends javax.swing.JPanel
{
public int xpos = 0;
public int ypos = 0;
BufferedImage img = null;
java.awt.Graphics2D gc = null;
Font currentFont = null;
double fps = 0;
String CurrentFrameText;
PanelPaint(int xpos,int ypos,double sx,double sy,String argCurrentFrameText)
{
img = new BufferedImage(xpos, ypos, BufferedImage.TYPE_INT_ARGB);
gc = img.createGraphics();
gc.scale(sx, sy);
currentFont = new Font("default", Font.BOLD, 30);
CurrentFrameText = argCurrentFrameText;
gc.setFont(currentFont);
gc.setColor(Color.RED);
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(xpos, ypos);
}
public void setImg(BufferedImage img,double argfps)
{
gc.drawImage(img, 0, 0, null);
gc.drawString(CurrentFrameText + ": " + (int)fps, 25, 25);
fps = argfps;
}
#Override
public void paint(java.awt.Graphics g)
{
g.drawImage(img, 0, 0, null);
}
public void close()
{
img = null;
gc = null;
currentFont = null;
}
}
Above is slower for high contrast images and taking time for "g.drawImage(img, 0, 0, null);" code.
Can performance for draw image can be improve?
You can use an OpenGL fragment shader to filter and then blit the source image as a texture to the 4 screens as screen quads (orthonormal projections onto a quad poly).
This is by far the optimal solution to your requirement. If your game is OpenGL based, it will be even faster since you wont have to use the Robot package as an intermediate - you can simply render the game texture directly (image conversion not required).
This will also allow full control over the filter in the fragment shader. See ShaderToy.com for many excellent filter shaders.
I'm using the area generation and drawing code from Smoothing a jagged path to create collision areas for tile sprites in a JRPG game I'm making in java - Its not a major full blown game, more just a proof-of-concept for me to learn from.
The area generation works fine except for straight lines:
http://puu.sh/7wJA2.png
http://puu.sh/7wJGF.png
These images are of the collision outline opened in photoshop (in the background) vs the area that the java code is generating based on the red color (in the foreground). As can be seen, the large red line in the first image won't be draw unless I have a red pixel under it every 2 pixels, as shown in the second image.
I have made no changes to the code (other than adding comments in an attempt to better understand it) except to remove
Graphics2D g = imageOutline.createGraphics();
//...
g.dispose();
from the areaOutline draw code, replacing it with reference to my own Graphics2D variable. I tested the code both with and without those two lines and I couldn't see any difference.
I'm a bit stuck on what could be causing the issue - its not a major game-breaking issue, as I can fix it a couple of ways, but it does mean I have to explain this issue to anyone when showing them how to use it with their own sprite textures.
I've created a SSCCE of the issue - This shows the areas generated have vertical lines generated fine, but that horizontal ones are not. It takes two images - both a square 30x30 pixels, but one has internal pixels every 2 second pixel in which causes a more accurate area to be generated.
When you press a key the program will switch between the two images (and their areas), allowing for direct comparison.
The following two images must be put in the root folder of the program.
You can start the program by running the "main" method.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.io.*;
import javax.imageio.*;
public class AreaOutlineCanvas extends JFrame implements KeyListener
{
private ImagePanel imagePanel;
/**
* Constructor for objects of class Skeleton
*/
public AreaOutlineCanvas()
{
// initialise instance variables
addKeyListener( this );
initUI();
}
public static void main( String[] args )
{
for( String s: args )
{
System.out.println( s );
}
SwingUtilities.invokeLater( new Runnable()
{
#Override
public void run()
{
AreaOutlineCanvas aOC = new AreaOutlineCanvas();
aOC.pack();
aOC.setVisible( true );
aOC.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
}
} );
}
private void initUI()
{
imagePanel = new ImagePanel();
setPreferredSize( new Dimension( 100, 100 ) );
setLocationRelativeTo( null );
add( imagePanel );
imagePanel.setDoubleBuffered( true );
pack();
}
#Override
public void keyTyped( KeyEvent e )
{
}
#Override
public void keyPressed( KeyEvent e )
{
//#TODO: switch statment checking the menu Skeleton is in (currentMenu)
if( imagePanel != null )
{
imagePanel.bDrawingFirstArea = !imagePanel.bDrawingFirstArea;
imagePanel.repaint();
}
}
#Override
public void keyReleased(KeyEvent e)
{
//System.out.println( "Key released: " + e.getKeyChar() + " (" + e.getKeyCode() + ")" );
}
public class ImagePanel extends JPanel
{
/** Path for the location of this charactors sprite sheet */
private String firstPath = "square1.png";
private String secondPath = "square2.png";
private BufferedImage firstImage;
private BufferedImage secondImage;
private Area firstImageArea;
private Area secondImageArea;
public boolean bDrawingFirstArea = true;
/**
* Constructor for objects of class Character
*/
public ImagePanel()
{
loadImages();
generateAreas();
}
private void loadImages()
{
try
{
firstImage = ImageIO.read( new File( firstPath ) );
secondImage = ImageIO.read( new File( secondPath ) );
}
catch( IOException e )
{
e.printStackTrace();
}
}
private void generateAreas()
{
Color c = new Color( 255, 0, 0 );
firstImageArea = getOutline( c, firstImage );
secondImageArea = getOutline( c, secondImage );
}
public Area getOutline(Color target, BufferedImage bi) {
// construct the GeneralPath
GeneralPath gp = new GeneralPath();
boolean cont = false;
int targetRGB = target.getRGB();
for (int xx=0; xx<bi.getWidth(); xx++) {
for (int yy=0; yy<bi.getHeight(); yy++) {
if (bi.getRGB(xx,yy)==targetRGB) {
if (cont) {
gp.lineTo(xx,yy);
gp.lineTo(xx,yy+1);
gp.lineTo(xx+1,yy+1);
gp.lineTo(xx+1,yy);
gp.lineTo(xx,yy);
} else {
gp.moveTo(xx,yy);
}
cont = true;
} else {
cont = false;
}
}
cont = false;
}
gp.closePath();
// construct the Area from the GP & return it
return new Area(gp);
}
#Override
public void paintComponent( Graphics g )
{
super.paintComponent( g );
drawImages(g);
}
private void drawImages( Graphics g )
{
Graphics2D g2d = (Graphics2D) g;
if( bDrawingFirstArea )
{
g2d.drawImage( firstImage, 50, 0, this );
g2d.draw( firstImageArea );
}
else
{
g2d.drawImage( secondImage, 50, 0, this );
g2d.draw( secondImageArea );
}
Toolkit.getDefaultToolkit().sync();
}
}
}
For convenience I've posted the code from Smoothing a jagged path question here
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
/* Gain the outline of an image for further processing. */
class ImageOutline {
private BufferedImage image;
private TwoToneImageFilter twoToneFilter;
private BufferedImage imageTwoTone;
private JLabel labelTwoTone;
private BufferedImage imageOutline;
private Area areaOutline = null;
private JLabel labelOutline;
private JLabel targetColor;
private JSlider tolerance;
private JProgressBar progress;
private SwingWorker sw;
public ImageOutline(BufferedImage image) {
this.image = image;
imageTwoTone = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB);
}
public void drawOutline() {
if (areaOutline!=null) {
Graphics2D g = imageOutline.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());
g.setColor(Color.RED);
g.setClip(areaOutline);
g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());
g.setColor(Color.BLACK);
g.setClip(null);
g.draw(areaOutline);
g.dispose();
}
}
public Area getOutline(Color target, BufferedImage bi) {
// construct the GeneralPath
GeneralPath gp = new GeneralPath();
boolean cont = false;
int targetRGB = target.getRGB();
for (int xx=0; xx<bi.getWidth(); xx++) {
for (int yy=0; yy<bi.getHeight(); yy++) {
if (bi.getRGB(xx,yy)==targetRGB) {
if (cont) {
gp.lineTo(xx,yy);
gp.lineTo(xx,yy+1);
gp.lineTo(xx+1,yy+1);
gp.lineTo(xx+1,yy);
gp.lineTo(xx,yy);
} else {
gp.moveTo(xx,yy);
}
cont = true;
} else {
cont = false;
}
}
cont = false;
}
gp.closePath();
// construct the Area from the GP & return it
return new Area(gp);
}
public JPanel getGui() {
JPanel images = new JPanel(new GridLayout(2,2,2,2));
JPanel gui = new JPanel(new BorderLayout(3,3));
JPanel originalImage = new JPanel(new BorderLayout(2,2));
final JLabel originalLabel = new JLabel(new ImageIcon(image));
targetColor = new JLabel("Target Color");
targetColor.setForeground(Color.RED);
targetColor.setBackground(Color.WHITE);
targetColor.setBorder(new LineBorder(Color.BLACK));
targetColor.setOpaque(true);
JPanel controls = new JPanel(new BorderLayout());
controls.add(targetColor, BorderLayout.WEST);
originalLabel.addMouseListener( new MouseAdapter() {
#Override
public void mouseEntered(MouseEvent me) {
originalLabel.setCursor(
Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
#Override
public void mouseExited(MouseEvent me) {
originalLabel.setCursor(Cursor.getDefaultCursor());
}
#Override
public void mouseClicked(MouseEvent me) {
int x = me.getX();
int y = me.getY();
Color c = new Color( image.getRGB(x,y) );
targetColor.setBackground( c );
updateImages();
}
});
originalImage.add(originalLabel);
tolerance = new JSlider(
JSlider.HORIZONTAL,
0,
255,
104
);
tolerance.addChangeListener( new ChangeListener() {
public void stateChanged(ChangeEvent ce) {
updateImages();
}
});
controls.add(tolerance, BorderLayout.CENTER);
gui.add(controls,BorderLayout.NORTH);
images.add(originalImage);
labelTwoTone = new JLabel(new ImageIcon(imageTwoTone));
images.add(labelTwoTone);
images.add(new JLabel("Smoothed Outline"));
imageOutline = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);
labelOutline = new JLabel(new ImageIcon(imageOutline));
images.add(labelOutline);
updateImages();
progress = new JProgressBar();
gui.add(images, BorderLayout.CENTER);
gui.add(progress, BorderLayout.SOUTH);
return gui;
}
private void updateImages() {
if (sw!=null) {
sw.cancel(true);
}
sw = new SwingWorker() {
#Override
public String doInBackground() {
progress.setIndeterminate(true);
adjustTwoToneImage();
labelTwoTone.repaint();
areaOutline = getOutline(Color.BLACK, imageTwoTone);
drawOutline();
return "";
}
#Override
protected void done() {
labelOutline.repaint();
progress.setIndeterminate(false);
}
};
sw.execute();
}
public void adjustTwoToneImage() {
twoToneFilter = new TwoToneImageFilter(
targetColor.getBackground(),
tolerance.getValue());
Graphics2D g = imageTwoTone.createGraphics();
g.drawImage(image, twoToneFilter, 0, 0);
g.dispose();
}
public static void main(String[] args) throws Exception {
int size = 150;
final BufferedImage outline =
new BufferedImage(size,size,BufferedImage.TYPE_INT_RGB);
Graphics2D g = outline.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,size,size);
g.setRenderingHint(
RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Polygon p = new Polygon();
p.addPoint(size/2, size/10);
p.addPoint(size-10, size-10);
p.addPoint(10, size-10);
Area a = new Area(p);
Rectangle r = new Rectangle(size/4, 8*size/10, size/2, 2*size/10);
a.subtract(new Area(r));
int radius = size/10;
Ellipse2D.Double c = new Ellipse2D.Double(
(size/2)-radius,
(size/2)-radius,
2*radius,
2*radius
);
a.subtract(new Area(c));
g.setColor(Color.BLACK);
g.fill(a);
ImageOutline io = new ImageOutline(outline);
JFrame f = new JFrame("Image Outline");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(io.getGui());
f.pack();
f.setResizable(false);
f.setLocationByPlatform(true);
f.setVisible(true);
}
}
class TwoToneImageFilter implements BufferedImageOp {
Color target;
int tolerance;
TwoToneImageFilter(Color target, int tolerance) {
this.target = target;
this.tolerance = tolerance;
}
private boolean isIncluded(Color pixel) {
int rT = target.getRed();
int gT = target.getGreen();
int bT = target.getBlue();
int rP = pixel.getRed();
int gP = pixel.getGreen();
int bP = pixel.getBlue();
return(
(rP-tolerance<=rT) && (rT<=rP+tolerance) &&
(gP-tolerance<=gT) && (gT<=gP+tolerance) &&
(bP-tolerance<=bT) && (bT<=bP+tolerance) );
}
public BufferedImage createCompatibleDestImage(
BufferedImage src,
ColorModel destCM) {
BufferedImage bi = new BufferedImage(
src.getWidth(),
src.getHeight(),
BufferedImage.TYPE_INT_RGB);
return bi;
}
public BufferedImage filter(
BufferedImage src,
BufferedImage dest) {
if (dest==null) {
dest = createCompatibleDestImage(src, null);
}
for (int x=0; x<src.getWidth(); x++) {
for (int y=0; y<src.getHeight(); y++) {
Color pixel = new Color(src.getRGB(x,y));
Color write = Color.BLACK;
if (isIncluded(pixel)) {
write = Color.WHITE;
}
dest.setRGB(x,y,write.getRGB());
}
}
return dest;
}
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight());
}
public Point2D getPoint2D(
Point2D srcPt,
Point2D dstPt) {
// no co-ord translation
return srcPt;
}
public RenderingHints getRenderingHints() {
return null;
}
}
I am trying to simulate a production system. To explain what I intend to do briefly, I will to create a Panel where I will have some tables to save values (for the proprieties of the several working stations and the job types (see pic. below)). When I run it, those values should be stored for further processing.
On a previous question I was recommended using TreeMaps to store those values, so I created something like:
Station[num][type][avg_time][posx][posy][state]
Part[num][type][state]
Heres is my code so far:
L.java
import java.awt.*;
import javax.swing.*;
public class L extends JFrame {
public static final int ww = 1000;
public static final int wh = 600;
public static final int bgw = (ww - 30);
public static final int bgh = (wh - 80);
public static final String wt = "Teste";
Color new_piece = new Color(255,0,0);
Color progress_piece = new Color(255,215,0);
Color ready_piece = new Color(173,255,47);
Container pane = getContentPane();
Dimension appletSize = pane.getSize();
int wHeight = appletSize.height;
int wWidth = appletSize.width;
DrawRectangle rectangle = new DrawRectangle();
public TMap t;
public L() {
setSize(ww,wh);
this.setTitle(wt);
// Sim(int nparts);
JButton startButton = new JButton("Start");
JButton stopButton = new JButton("Stop");
//... Add Listeners
// startButton.addActionListener(new StartAction());
//stopButton.addActionListener(new StopAction());
//... Layout inner panel with two buttons horizontally
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT,10,10));
buttonPanel.add(startButton);
buttonPanel.add(stopButton);
this.setLayout(new BoxLayout(pane,BoxLayout.Y_AXIS));
this.add(rectangle);
this.add(buttonPanel);
//Sim();
t = new TMap();
test();
//pane.add(rectangle);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}
public void addRectangle(int px, int py, int pw, int ph, Color pc, String state) {
this.rectangle.addRectangle( px, py, pw, ph, pc, state);
}
public void Sim() {
addRectangle(20,20,10,10,Color.green,"new");
/*for (int i=0;i<=nparts;i++) {
addRectangle(200,200,50,Color.green);
}*/
}
public void test() {
// First Station Proprieties
t.put("num",1);
t.put("type",1);
t.put("avg_time",5);
t.put("posx",100);
t.put("posy",20);
t.put("state",0);
// Second Station Proprieties
t.put("num",2);
t.put("type",2);
t.put("avg_time",7);
t.put("posx",200);
t.put("posy",20);
t.put("state",0);
/*System.out.println("Now the tree map Keys: " + t.St.keySet());
System.out.println("Now the tree map contain: " + t.St.values());*/
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
L l = new L();
//System.out.println("Entryset: " + t.keySet());
//System.out.println("Entryset: " + t.Station() + "\n");
}
});
}
}
DrawRectangle.java
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
public class DrawRectangle extends JPanel implements ActionListener {
private java.util.List<Rectangle2D> squares;
private java.util.List<Color> colors;
private long seconds = 1;
private int anim_interval = (int) TimeUnit.SECONDS.toMillis(seconds);
private Timer sim_timer;
private Timer idle_timer;
int px = 10, velx = 2;
String state;
Color pc;
public DrawRectangle(){
//this.setBounds(10, 10, 10, 10);
this.setMinimumSize(new Dimension(100,100));
this.setPreferredSize(new Dimension(100,L.bgh));
this.setMinimumSize(new Dimension(500,L.bgh));
setBackground(Color.gray);
setDoubleBuffered(true);
setBorder(BorderFactory.createLineBorder(Color.black, 1));
squares = new ArrayList<Rectangle2D>();
colors = new ArrayList<Color>();
sim_timer = new Timer(anim_interval,this);
sim_timer.start();
}
public void addRectangle(int px, int py, int pw, int ph, Color pc, String state) { // square
squares.add( new Rectangle2D.Double(px, py, pw, ph) ) ;
pc = pc;
//System.out.println(state);
//this.a = a;
//this.startX = startX;
//this.startY = startY;
}
public void actionPerformed(ActionEvent e) {
/*if (px < 0 || px > 990) {
velx = -velx;
}*/
//System.out.println(px);
if (px == 20) {
sim_timer.stop();
state = "idle";
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
Logger.getLogger(DrawRectangle.class.getName()).log(Level.SEVERE, null, ex);
}
sim_timer.start();
state = "going";
}
else if (px == 50) {
sim_timer.stop();
state = "done";
seconds = 2;
}
//if (state != "idle") {
px = px + velx;
repaint();
//}
}
private void idlestate() throws InterruptedException {
Thread.sleep(5000);
state = "idle";
}
private void goingstate() {
state = "going";
}
private void donestate() {
state = "done";
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g1 = (Graphics2D) g.create();
Graphics2D g2 = (Graphics2D) g.create();
//System.out.println("D1");
/*for( Rectangle2D rect : squares ) {
System.out.println(colors);
//g1.setPaint(colors);
g1.fill(rect);
}*/
//for(int i=0;i<squares.size();i++) {
//System.out.println("D2");
//g1.setColor(colors.get(i));
if (state == "going") { g1.setColor(Color.orange); }
else if (state == "idle") { g1.setColor(Color.yellow); }
else if (state == "done") { g1.setColor(Color.green); }
else { g1.setColor(Color.red); }
//g1.fill(squares.get(i));
g1.fillRect(px, 10, 10, 10);
g2.setColor(Color.blue);
g2.fillRect(px,40,10,10);
//g1.dispose();
//}
}
public void setColor(Color newColor) {
pc = newColor;
}
}
TMap.java
import java.util.*;
public class TMap {
public TreeMap <String, Integer> St;
public int num_atrib = 6;
public TMap () {
St = new TreeMap <>();
}
public Set<String> getKeySet() {
return St.keySet();
}
public Integer get(String s) {
return St.get(s);
}
public void put (String s, int i) {
St.put(s,i);
System.out.println("Now the tree map Keys: " + St.keySet());
System.out.println("Now the tree map contain: " + St.values());
}
public TreeMap<String, Integer> Station(String s,int i) {
}
}
The DrawRectangle.java code is here just for those who may want to compile the code but has nothing to do with the actual problem so far. The TMap.java is where I create the map, and have the methods to deal with the data. Everything is working fine, my problems is the following:
Most likely, when simulating, I will have more than one station, so I would need to store info in such a matter:
Station[num][type][avg_time][posx][posy][state]
Station[1][1][5][100][20][0]
Station[2][2][7][200][20][0]
Station[3][3][4][300][20][0]
Thing is, when I put new data to the treemap, it will overwrite the previous data stored so if I add info twice, the output will come as:
Now the tree map Keys: [avg_time, num, posx, posy, type]
Now the tree map contain: [5, 1, 100, 20, 1]
Now the tree map Keys: [avg_time, num, posx, posy, state, type]
Now the tree map contain: [7, 2, 200, 20, 0, 2]
The simulation will be limited to 6 working stations, so my question is, what's the best practice to deal with this? The only thing I could come up with is creating 6 TreeMaps and only use those that'll be needed, but i'm pretty sure there has got to be an easier and more efficient way to store the data.
Thanks in advance for your help!
I am not sure I understand what you are trying to achieve but this is the way a map works. It stores pairs of keys / values, making sure keys are unique.
So when you write:
map.put("abc",100);
map.put("abc",200);
the map only has one entry after the second line, which is key = "abc" and value = 200.
You may want to store your stations in a list instead, where each station is a map of key/value pairs holding the related information:
List<TMap> stations = new ArrayList<TMap> ();
TMap station1 = new TMap();
station1.put("num",1);
station1.put("type",1);
...
stations.add(station1);
TMap station2 = new TMap();
station2.put("num",2);
station2.put("type",2);
...
stations.add(station2);
ps: I am not sure I understand the point of having that TMap class which is a TreeMap really. So you could also use a list of maps:
List<Map<String, Integer>> stations = new ArrayList<Map<String, Integer>> ();
Map<String, Integer> station1 = new TreeMap<String, Integer> ();
Single keys (in code KeyboardButtons) extends JComponent. When i'm trying to add the to main JFrame, i can do that for single key, when trying to add another one, the first one is not showing.
Can you please look at the code a tell me where the problem is?
MainFrame.java:
package keyboard;
import java.awt.*;
import javax.swing.JFrame;
public class MainFrame extends JFrame {
public MainFrame() throws HeadlessException {
setTitle("Keyboard");
setSize(1024, 768);
}
public static void main(String[] args) throws InterruptedException {
JFrame mainWindow = new MainFrame();
mainWindow.setDefaultCloseOperation(EXIT_ON_CLOSE);
Point left5 = new Point(210, 210);
Point left4 = new Point(410, 110);
Point left3 = new Point(580, 120);
Point left2 = new Point(680, 200);
Point left1 = new Point(800, 500);
Keyboard kb = new Keyboard(left1, left2, left3, left4, left5);
KeyboardButton[] buttons = kb.registerKeys();
Container c = mainWindow.getContentPane();
c.add(buttons[0]);
c.add(buttons[1]);
mainWindow.setVisible(true);
}
}
KeyboardButton.java:
package keyboard;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Polygon;
import java.awt.event.*;
import javax.swing.JComponent;
public class KeyboardButton extends JComponent implements MouseListener {
Polygon polygon;
boolean isActive;
final Color ACTIVE_COLOR = Color.red;
final Color INACTIVE_COLOR = Color.blue;
public KeyboardButton(Polygon p) {
polygon = p;
addMouseListener(this);
}
private void checkMousePosition(MouseEvent e) {
if (polygon.contains(e.getX(), e.getY())) {
setState(true);
}
}
public void mouseClicked(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
checkMousePosition(e);
System.out.println(this + " pressed");
}
public void mouseReleased(MouseEvent e) {
setState(false);
System.out.println(this + " released");
}
#Override
public void paint(Graphics g) {
super.paint(g);
g.setColor(isActive ? ACTIVE_COLOR : INACTIVE_COLOR);
g.drawPolygon(polygon);
}
void setState(boolean state) {
isActive = state;
repaint();
}
}
Keyboard.java:
package keyboard;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import javax.swing.JComponent;
public class Keyboard extends JComponent {
Point[] leftFingers;
Point leftCenter = new Point(300, 600);
public Keyboard(Point left1, Point left2, Point left3, Point left4, Point left5) {
leftFingers = new Point[5];
leftFingers[0] = left1;
leftFingers[1] = left2;
leftFingers[2] = left3;
leftFingers[3] = left4;
leftFingers[4] = left5;
}
public KeyboardButton[] registerKeys() {
Polygon[] polygons = generateKeyPolygons(calculateBordersOfKeys(calculateCentersBetweenEachTwoFingers(leftFingers)));
KeyboardButton[] buttons = new KeyboardButton[5];
for (int i = 0; i < polygons.length; i++) {
buttons[i] = new KeyboardButton(polygons[i]);
}
return buttons;
}
private Point[] calculateBordersOfKeys(Point[] fingers) {
Point[] centers = calculateCentersBetweenEachTwoFingers(fingers);
Point[] result = new Point[6];
result[0] = calculateCentralSymmetry(centers[0], fingers[0]);
System.arraycopy(centers, 0, result, 1, centers.length);
result[5] = calculateCentralSymmetry(centers[3], fingers[4]);
return result;
}
#Override
public void paint(Graphics g) {
super.paint(g);
g.setColor(Color.red);
g.drawOval(leftCenter.x - 25, leftCenter.y - 25, 50, 50);
for (int i = 0; i < leftFingers.length; i++) {
g.drawOval(leftFingers[i].x, leftFingers[i].y, 10, 10);
}
}
private Polygon[] generateKeyPolygons(Point[] borders) {
Polygon[] polygons = new Polygon[5];
for (int i = 0; i < borders.length - 1; i++) {
Polygon p = new Polygon();
p.addPoint(leftCenter.x, leftCenter.y);
p.addPoint(borders[i].x, borders[i].y);
p.addPoint(borders[i + 1].x, borders[i + 1].y);
polygons[i] = p;
}
return polygons;
}
private Point[] calculateCentersBetweenEachTwoFingers(Point[] fingers) {
Point[] centers = new Point[4];
for (int i = 0; i < fingers.length - 1; i++) {
centers[i] = new Point(((fingers[i].x + fingers[i + 1].x) / 2), ((fingers[i].y + fingers[i + 1].y) / 2));
}
return centers;
}
private Point calculateCentralSymmetry(Point toReflected, Point center) {
Point reflection = new Point();
if (toReflected.x > center.x) {
reflection.x = center.x - Math.abs(center.x - toReflected.x);
} else {
reflection.x = center.x + Math.abs(center.x - toReflected.x);
}
if (toReflected.y > center.y) {
reflection.y = center.y - Math.abs(center.y - toReflected.y);
} else {
reflection.y = center.y + Math.abs(center.y - toReflected.y);
}
return reflection;
}
}
Try to use another LayoutManager, or for what it seems to me it looks like you are trying to manualy paint shapes on the screen, i'd suggest painting them all on one layer. (have one JComponent's paintComponent() method which calls to KeyboardButton.paint() and other painting methods, then you can just add that one JComponent)
I have a JRadioButton with some text, containing html code. When I set it to disabled, color of text doesn't changed to gray or something else. How can I set it to default disabled component text color?
I can set text color directly in text like:
<html><font color=someColor>...
but how can I get default color for disabled component text?
Also I've tried to override paint method and use something like this:
Graphics2D g2 = (Graphics2D) g.create();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.35f));
super.paintComponent(g2);
g2.dispose();
but I didn't got expected result - it became gray, but not identical to default disabled component text color.
So, the solution may be to get disabled color from UIManager.getColor("ComboBox.disabledForeground"); cause this property available in all Os. And here is the code:
import javax.swing.*;
import java.awt.*;
public class HTMLJRadio extends JRadioButton {
static String prefixEnabled = "<html><body style='color: black;'>";
String text;
String prefixDisabled;
HTMLJRadio(String text) {
super(prefixEnabled + text);
this.text = text;
Color c = UIManager.getColor("ComboBox.disabledForeground");
String color = Integer.toHexString(c.getRed()) +
Integer.toHexString(c.getGreen()) +
Integer.toHexString(c.getBlue());
prefixDisabled = "<html><body style='color: #" + color + ";'>";
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (enabled) {
setText(prefixEnabled + text);
} else {
setText(prefixDisabled + text);
}
}
public static void showButtons() {
String htmlText = "<h1>Laf</h1><p>Ha Ha!";
JPanel p = new JPanel(new GridLayout(0, 1, 3, 3));
HTMLJRadio button1 = new HTMLJRadio(htmlText);
p.add(button1);
HTMLJRadio button2 = new HTMLJRadio(htmlText);
button2.setEnabled(false);
p.add(button2);
JRadioButton button3 = new JRadioButton("Non html disabled");
button3.setEnabled(false);
p.add(button3);
JOptionPane.showMessageDialog(null, p);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
showButtons();
}
});
}
}
On ubuntu it looks like:
So the disabled radio button with html inside looks very similar to disabled radio button without html inside.
Also you can use solution from answer, with some magic with images.
Edit: (by A.T.) Previously this question was marked as 'correct'. By mutual agreement, the OP withdrew the correct marking, since the offered answer does not cover the subtleties shown in the screen shot. It is particularly the 'embossed' effect that is around the text in the lower button, that sets it apart from the disabled HTML formatted button.
Further suggestions as to how to achieve that effect would be appreciated (by both of us).
From How to Use HTML in Swing Components:
...Note also that when a button is disabled, its HTML text unfortunately remains black, instead of becoming gray. (Refer to bug #4783068 to see if this situation changes.)
Maybe you can get by starting with something like this.
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;
class HTMLButton extends JButton {
static String prefixEnabled = "<html><body style='color: black;'>";
String text;
String prefixDisabled;
HTMLButton(String text) {
super(prefixEnabled + text);
this.text = text;
Color c = determineDisabledColorByWitchCraft();
//UIManager.getColor("Button.disabledText");
String color =
Integer.toHexString(c.getRed()) +
Integer.toHexString(c.getGreen()) +
Integer.toHexString(c.getBlue());
prefixDisabled = "<html><body style='color: #" + color + ";'>";
}
private static String getHex(int n) {
return Integer.toHexString(n);
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (enabled) {
setText(prefixEnabled + text);
} else {
setText(prefixDisabled + text);
}
}
public static Color determineDisabledColorByWitchCraft() {
// this is little 'block' character..
JButton b = new JButton(String.valueOf('\u2586'));
b.setSize(b.getPreferredSize());
b.setEnabled(false);
BufferedImage biDisabled = new BufferedImage(
b.getWidth(),
b.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics g2 = biDisabled.getGraphics();
b.paint(g2);
// get the middle pixel..
int x = b.getWidth()/2;
int y = b.getHeight()/2;
return new Color(biDisabled.getRGB(x,y));
}
public static void showButtons() {
String htmlText = "<h1>Laf</h1><p>Ha Ha!";
JPanel p = new JPanel(new GridLayout(0,1,3,3));
HTMLButton button1 = new HTMLButton(htmlText);
p.add(button1);
HTMLButton button2 = new HTMLButton(htmlText);
button2.setEnabled(false);
p.add(button2);
JButton button3 = new JButton("Hi there!");
button3.setEnabled(false);
p.add(button3);
JOptionPane.showMessageDialog(null, p);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
showButtons();
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch(Exception e) {
}
showButtons();
}
});
}
}
Screenshot of disabled button using default disabled color
Update
(Failed) attempt to get the effect using CSS positioning. Just thought I'd report the latest failure, to save other people the trouble of wondering whether it might suffice.
This HTML:
<html>
<head>
<style type='text/css'>
body {
font-size: 16px;
}
.main {
position: fixed;
top: -16px;
left: 0px;
color: black;
}
.emboss {
position: fixed;
top: 0px;
left: 1px;
color: red;
}
</style>
</head>
<body>
<p class='emboss'><b>The quick brown fox jumped over the lazy dog.</b>
<p class='main'><b>The quick brown fox jumped over the lazy dog.</b>
</body>
</html>
Which renders in a browser (e.g. FF) much like this:
..renders in JEditorPane like this:
:-(
I think I've been working for it for 3 hours (no school :D). And the result is good! Here is a screenshot of it in Ubuntu:
I discarded the usage of setting the body color. As you can see on the image, you can choose colors and when they are disabled everything grays out.
And here is the HTMLJRadio class, working Ubuntu version:
package so_6648578;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.View;
import sun.swing.SwingUtilities2;
public class HTMLJRadio extends JRadioButton
{
private static final String HTML_PREFIX = "<html>";
private static Dimension size = new Dimension();
private static Rectangle viewRect = new Rectangle();
private static Rectangle iconRect = new Rectangle();
private static Rectangle textRect = new Rectangle();
private String myText;
public HTMLJRadio(String text)
{
super(HTML_PREFIX + text);
this.myText = text;
}
#Override
public void setEnabled(boolean enabled)
{
super.setEnabled(enabled);
setText(HTML_PREFIX + myText);
}
private View clearSuperText()
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, null);
View v = (View) getClientProperty(BasicHTML.propertyKey);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method remove = arrayTableClass.getMethod("remove", Object.class);
remove.setAccessible(true);
remove.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey);
return v;
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
return null;
}
}
private void restoreSuperText(View v)
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, HTML_PREFIX + myText);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method put = arrayTableClass.getMethod("put", Object.class, Object.class);
put.setAccessible(true);
put.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey, v);
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
}
}
#Override
protected void paintComponent(Graphics g)
{
// Paint the icon
View v = clearSuperText();
super.paintComponent(g);
restoreSuperText(v);
// Paint the HTML
paintHTML(g);
}
public Icon getDefaultIcon()
{
return UIManager.getIcon("RadioButton.icon");
}
/**
* paint the radio button
* StackOverflow.com: Copied and modified from Oracle Java API:
*
*/
public synchronized void paintHTML(Graphics g)
{
Font f = getFont();
g.setFont(f);
FontMetrics fm = SwingUtilities2.getFontMetrics(this, g, f);
Insets i = getInsets();
size = getSize(size);
viewRect.x = i.left;
viewRect.y = i.top;
viewRect.width = size.width - (i.right + viewRect.x);
viewRect.height = size.height - (i.bottom + viewRect.y);
iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
textRect.x = textRect.y = textRect.width = textRect.height = 0;
Icon altIcon = getIcon();
String text = SwingUtilities.layoutCompoundLabel(
this, fm, getText(), altIcon != null ? altIcon : getDefaultIcon(),
getVerticalAlignment(), getHorizontalAlignment(),
getVerticalTextPosition(), getHorizontalTextPosition(),
viewRect, iconRect, textRect,
getText() == null ? 0 : getIconTextGap());
// fill background
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, size.width, size.height);
}
// Draw the Text
if (text != null) {
View v = (View) getClientProperty(BasicHTML.propertyKey);
if (v != null) {
if (!isEnabled()) {
// Perpared the grayed out img
BufferedImage img = new BufferedImage(textRect.width + 1, textRect.height + 1, BufferedImage.TYPE_4BYTE_ABGR_PRE);
Graphics2D gg = (Graphics2D) img.createGraphics();
Rectangle imgRect = new Rectangle(0, 0, textRect.width, textRect.height);
gg.setClip(imgRect);
v.paint(gg, imgRect);
int brighter = getBackground().brighter().getRGB() & 0x00FFFFFF;
int darker = getBackground().darker().getRGB() & 0x00FFFFFF;
gg.dispose();
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | brighter;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x + 1, textRect.y + 1, this);
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | darker;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x, textRect.y, this);
} else {
v.paint(g, textRect);
}
} else {
throw new IllegalStateException("The given text isn't HTML!!");
}
}
}
public static void showButtons()
{
String htmlText = "<h1>Laf</h1><p>Ha Ha!<p style='color: green; text-decoration: underline;'>Green?";
JPanel p = new JPanel(new GridLayout(0, 1, 3, 3));
HTMLJRadio button1 = new HTMLJRadio(htmlText);
p.add(button1);
HTMLJRadio button2 = new HTMLJRadio(htmlText);
button2.setEnabled(false);
p.add(button2);
JRadioButton button3 = new JRadioButton("Non html disabled");
button3.setEnabled(false);
p.add(button3);
JOptionPane.showMessageDialog(null, p);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
showButtons();
}
});
}
}
Approach 2
Second try to make it work in Windows. This works in Ubuntu as well:
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.View;
import sun.swing.SwingUtilities2;
public class HTMLJRadio extends JRadioButton
{
private static final String HTML_PREFIX = "<html>";
private static Dimension size = new Dimension();
private static Rectangle viewRect = new Rectangle();
private static Rectangle iconRect = new Rectangle();
private static Rectangle textRect = new Rectangle();
private String myText;
public HTMLJRadio(String text)
{
super(HTML_PREFIX + text);
this.myText = text;
}
#Override
public void setEnabled(boolean enabled)
{
super.setEnabled(enabled);
setText(HTML_PREFIX + myText);
}
private View clearSuperText()
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, null);
View v = (View) getClientProperty(BasicHTML.propertyKey);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method remove = arrayTableClass.getMethod("remove", Object.class);
remove.setAccessible(true);
remove.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey);
return v;
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
return null;
}
}
private void restoreSuperText(View v)
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, HTML_PREFIX + myText);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method put = arrayTableClass.getMethod("put", Object.class, Object.class);
put.setAccessible(true);
put.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey, v);
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
}
}
#Override
protected void paintComponent(Graphics g)
{
// Paint the icon
View v = clearSuperText();
super.paintComponent(g);
restoreSuperText(v);
// Paint the HTML
paintHTML(g);
}
public Icon getDefaultIcon()
{
return UIManager.getIcon("RadioButton.icon");
}
private Color getDisableColor()
{
return UIManager.getColor("ComboBox.disabledForeground");
}
private Color getDisableColorBackground()
{
return getDisableColor().brighter().brighter();
}
/**
* paint the radio button
* StackOverflow.com: Copied and modified from Oracle Java API:
*
*/
public synchronized void paintHTML(Graphics g)
{
Font f = getFont();
g.setFont(f);
FontMetrics fm = SwingUtilities2.getFontMetrics(this, g, f);
Insets i = getInsets();
size = getSize(size);
viewRect.x = i.left;
viewRect.y = i.top;
viewRect.width = size.width - (i.right + viewRect.x);
viewRect.height = size.height - (i.bottom + viewRect.y);
iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
textRect.x = textRect.y = textRect.width = textRect.height = 0;
Icon altIcon = getIcon();
String text = SwingUtilities.layoutCompoundLabel(
this, fm, getText(), altIcon != null ? altIcon : getDefaultIcon(),
getVerticalAlignment(), getHorizontalAlignment(),
getVerticalTextPosition(), getHorizontalTextPosition(),
viewRect, iconRect, textRect,
getText() == null ? 0 : getIconTextGap());
// fill background
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, size.width, size.height);
}
// Draw the Text
if (text != null) {
View v = (View) getClientProperty(BasicHTML.propertyKey);
if (v != null) {
if (!isEnabled()) {
// Perpared the grayed out img
BufferedImage img = new BufferedImage(textRect.width + 1, textRect.height + 1, BufferedImage.TYPE_4BYTE_ABGR_PRE);
Graphics2D gg = (Graphics2D) img.createGraphics();
Rectangle imgRect = new Rectangle(0, 0, textRect.width, textRect.height);
gg.setClip(imgRect);
v.paint(gg, imgRect);
Color cBrither = getDisableColorBackground();
Color cDarker = getDisableColor();
int brighter = cBrither.getRGB() & 0x00FFFFFF;
int darker = cDarker.getRGB() & 0x00FFFFFF;
// int brighter = getBackground().brighter().getRGB() & 0x00FFFFFF;
// int darker = getBackground().darker().getRGB() & 0x00FFFFFF;
gg.dispose();
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | brighter;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x + 1, textRect.y + 1, this);
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | darker;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x, textRect.y, this);
} else {
v.paint(g, textRect);
}
} else {
throw new IllegalStateException("The given text isn't HTML!!");
}
}
}
public static void showButtons()
{
String htmlText = "<h1>Laf</h1><p>Ha Ha!<p style='color: green; text-decoration: underline;'>Green?";
JPanel p = new JPanel(new GridLayout(0, 1, 3, 3));
HTMLJRadio button1 = new HTMLJRadio(htmlText);
p.add(button1);
HTMLJRadio button2 = new HTMLJRadio(htmlText);
button2.setEnabled(false);
p.add(button2);
JRadioButton button3 = new JRadioButton("Non html disabled");
button3.setEnabled(false);
p.add(button3);
JOptionPane.showMessageDialog(null, p);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
showButtons();
}
});
}
}
Windows screenshot of 2nd attempt
Approach 3: With determineDisabledColorByWitchCraft():
This one works as well perfect on Ubuntu:
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.View;
import sun.swing.SwingUtilities2;
public class HTMLJRadio extends JRadioButton
{
private static final String HTML_PREFIX = "<html>";
private static Dimension size = new Dimension();
private static Rectangle viewRect = new Rectangle();
private static Rectangle iconRect = new Rectangle();
private static Rectangle textRect = new Rectangle();
private String myText;
public HTMLJRadio(String text)
{
super(HTML_PREFIX + text);
this.myText = text;
}
#Override
public void setEnabled(boolean enabled)
{
super.setEnabled(enabled);
setText(HTML_PREFIX + myText);
}
private View clearSuperText()
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, null);
View v = (View) getClientProperty(BasicHTML.propertyKey);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method remove = arrayTableClass.getMethod("remove", Object.class);
remove.setAccessible(true);
remove.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey);
return v;
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
return null;
}
}
private void restoreSuperText(View v)
{
try {
Field textField = getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("text");
textField.setAccessible(true);
textField.set(this, HTML_PREFIX + myText);
Field clientPropertiesField = getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("clientProperties");
clientPropertiesField.setAccessible(true);
Class arrayTableClass = clientPropertiesField.get(this).getClass();
Method put = arrayTableClass.getMethod("put", Object.class, Object.class);
put.setAccessible(true);
put.invoke(clientPropertiesField.get(this), BasicHTML.propertyKey, v);
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(-1);
}
}
#Override
protected void paintComponent(Graphics g)
{
// Paint the icon
View v = clearSuperText();
super.paintComponent(g);
restoreSuperText(v);
// Paint the HTML
paintHTML(g);
}
public Icon getDefaultIcon()
{
return UIManager.getIcon("RadioButton.icon");
}
public static Color determineDisabledColorByWitchCraft() {
// this is little 'block' character..
JButton b = new JButton(String.valueOf('\u2586'));
b.setSize(b.getPreferredSize());
b.setEnabled(false);
BufferedImage biDisabled = new BufferedImage(
b.getWidth(),
b.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics g2 = biDisabled.getGraphics();
b.paint(g2);
// get the middle pixel..
int x = b.getWidth()/2;
int y = b.getHeight()/2;
return new Color(biDisabled.getRGB(x,y));
}
private Color getDisableColor()
{
return determineDisabledColorByWitchCraft();
}
private Color getDisableColorBackground()
{
return getDisableColor().brighter().brighter();
}
/**
* paint the radio button
* StackOverflow.com: Copied and modified from Oracle Java API:
*
*/
public synchronized void paintHTML(Graphics g)
{
Font f = getFont();
g.setFont(f);
FontMetrics fm = SwingUtilities2.getFontMetrics(this, g, f);
Insets i = getInsets();
size = getSize(size);
viewRect.x = i.left;
viewRect.y = i.top;
viewRect.width = size.width - (i.right + viewRect.x);
viewRect.height = size.height - (i.bottom + viewRect.y);
iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
textRect.x = textRect.y = textRect.width = textRect.height = 0;
Icon altIcon = getIcon();
String text = SwingUtilities.layoutCompoundLabel(
this, fm, getText(), altIcon != null ? altIcon : getDefaultIcon(),
getVerticalAlignment(), getHorizontalAlignment(),
getVerticalTextPosition(), getHorizontalTextPosition(),
viewRect, iconRect, textRect,
getText() == null ? 0 : getIconTextGap());
// fill background
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, size.width, size.height);
}
// Draw the Text
if (text != null) {
View v = (View) getClientProperty(BasicHTML.propertyKey);
if (v != null) {
if (!isEnabled()) {
// Perpared the grayed out img
BufferedImage img = new BufferedImage(textRect.width + 1, textRect.height + 1, BufferedImage.TYPE_4BYTE_ABGR_PRE);
Graphics2D gg = (Graphics2D) img.createGraphics();
Rectangle imgRect = new Rectangle(0, 0, textRect.width, textRect.height);
gg.setClip(imgRect);
v.paint(gg, imgRect);
Color cBrither = getDisableColorBackground();
Color cDarker = getDisableColor();
int brighter = cBrither.getRGB() & 0x00FFFFFF;
int darker = cDarker.getRGB() & 0x00FFFFFF;
// int brighter = getBackground().brighter().getRGB() & 0x00FFFFFF;
// int darker = getBackground().darker().getRGB() & 0x00FFFFFF;
gg.dispose();
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | brighter;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x + 1, textRect.y + 1, this);
for (int y = 0; y < img.getHeight(); ++y) {
for (int x = 0; x < img.getWidth(); ++x) {
int argb = img.getRGB(x, y);
if (argb != 0) {
argb = (argb & 0xFF000000) | darker;
img.setRGB(x, y, argb);
}
}
}
g.drawImage(img, textRect.x, textRect.y, this);
} else {
v.paint(g, textRect);
}
} else {
throw new IllegalStateException("The given text isn't HTML!!");
}
}
}
public static void showButtons()
{
String htmlText = "<h1>Laf</h1><p>Ha Ha!<p style='color: green; text-decoration: underline;'>Green?";
JPanel p = new JPanel(new GridLayout(0, 1, 3, 3));
HTMLJRadio button1 = new HTMLJRadio(htmlText);
p.add(button1);
HTMLJRadio button2 = new HTMLJRadio(htmlText);
button2.setEnabled(false);
p.add(button2);
JRadioButton button3 = new JRadioButton("Non html disabled");
button3.setEnabled(false);
p.add(button3);
JOptionPane.showMessageDialog(null, p);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
showButtons();
}
});
}
}
Enjoy!