I've been working up an answer for another question and came across a bizare issue that I've not seen before...
Basically, the program uses a AffineTransform to provide translation, scaling and rotating of a Graphics element, simple enough stuff, done a thousand times before
The problem is, when the screen first appears, the output is not where it should be, but once I touch one of the controls (adjust one of the slides) it jumps to the right spot.
Based on the screen shots, the Graphics content seems to be misplaced by the amount of the other controls.
If I remove the controls from the GUI, it appears in the right spot (in the center). If I resize the window, it doesn't fix the issue, it only fixes when one of the sliders triggers repaint on the DrawPane...
I've added diagnostics to the output and the all the values are the same - that is, they print the same values when the program first starts and when I adjust all slider values to their initial values.
If I remove the setRotation and setScale calls from the AffineTransform, it doesn't fix it. If I remove the setTranslation, the square isn't initially painted until the panel is updated (it's painted off screen)
If I use Graphics2D g2d = (Graphics)g; instead of g.create(), same result (and yes, I reset the transform before the paintComponent method exited ;))
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Parker {
public static void main(String[] args) {
new Parker();
}
public Parker() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ControlPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ControlPane extends JPanel {
private JSlider slider; //declare slider
private DrawPane myPanel;
public ControlPane() {
setLayout(new BorderLayout());
myPanel = new DrawPane();
myPanel.setBackground(Color.cyan); //change background color
slider = new JSlider(SwingConstants.VERTICAL, 0, 400, 100);// restrains the slider from scaling square to 0-300 pixels
slider.setMajorTickSpacing(20); //will set tick marks every 10 pixels
slider.setPaintTicks(true); //this actually paints the ticks on the screen
slider.addChangeListener(
new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
myPanel.setScale(slider.getValue()); //Wherever you set the slider, it will pass that value and that will paint on the screen
}
}
);
JSlider rotate = new JSlider(SwingConstants.VERTICAL, 0, 720, 0);
rotate.setMajorTickSpacing(20); //will set tick marks every 10 pixels
rotate.setPaintTicks(true); //this actually paints the ticks on the screen
rotate.addChangeListener(
new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
JSlider slider = (JSlider) e.getSource();
myPanel.setAngle(slider.getValue());
}
}
);
add(slider, BorderLayout.WEST);
add(rotate, BorderLayout.EAST);
add(myPanel);
slider.setValue(0);
slider.setValue(100);
rotate.setValue(0);
}
}
public class DrawPane extends JPanel {
private double scale = 1;
private double angle = 0;
private final int rectWidth = 20;
private final int rectHeight = 20;
#Override
protected void paintComponent(Graphics g)//paints obj on the screen
{
super.paintComponent(g); //prepares graphic object for drawing
int originX = getWidth() / 2;
int originY = getHeight() / 2;
int xOffset = -(rectWidth / 2);
int yOffset = -(rectHeight / 2);
g.setColor(Color.BLACK);
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = new AffineTransform();
at.translate(originX, originY);
g2d.setTransform(at);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(angle), 0, 0);
g2d.fillRect(xOffset, yOffset, rectWidth, rectHeight);
g2d.dispose();
g.setColor(Color.RED);
g.drawRect(originX + xOffset, originY + yOffset, rectWidth, rectWidth);
}
public void setAngle(double angle) {
this.angle = angle;
repaint();
}
public void setScale(int scale) {
// Scaling is normalized so that 1 = 100%
this.scale = (scale / 100d);
repaint();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
}
Basically, it appears that the Graphics context, for some reason is not been translated properly when it is first painted and I have no idea why...
ps- Test on Java 6 and Java 7 under Windows 7
pps- I've also tried setting the initial scale and rotation to other values before the screen was visible, same result
System Properties
awt.toolkit=sun.awt.windows.WToolkit
file.encoding=UTF-8
file.encoding.pkg=sun.io
file.separator=\
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.awt.printerjob=sun.awt.windows.WPrinterJob
java.class.path=C:\DevWork\personal\java\projects\wip\SystemProperties\build\classes
java.class.version=51.0
java.endorsed.dirs=C:\Program Files\Java\jdk1.7.0_51\jre\lib\endorsed
java.ext.dirs=C:\Program Files\Java\jdk1.7.0_51\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
java.home=C:\Program Files\Java\jdk1.7.0_51\jre
java.io.tmpdir=C:\Users\shane\AppData\Local\Temp\
java.runtime.name=Java(TM) SE Runtime Environment
java.runtime.version=1.7.0_51-b13
java.specification.name=Java Platform API Specification
java.specification.vendor=Oracle Corporation
java.specification.version=1.7
java.vendor=Oracle Corporation
java.vendor.url=http://java.oracle.com/
java.vendor.url.bug=http://bugreport.sun.com/bugreport/
java.version=1.7.0_51
java.vm.info=mixed mode
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vm.specification.name=Java Virtual Machine Specification
java.vm.specification.vendor=Oracle Corporation
java.vm.specification.version=1.7
java.vm.vendor=Oracle Corporation
java.vm.version=24.51-b03
os.arch=amd64
os.name=Windows 7
os.version=6.1
path.separator=;
sun.arch.data.model=64
sun.cpu.endian=little
sun.cpu.isalist=amd64
sun.desktop=windows
sun.io.unicode.encoding=UnicodeLittle
sun.java.command=systemproperties.SystemProperties
sun.java.launcher=SUN_STANDARD
sun.jnu.encoding=Cp1252
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
sun.os.patch.level=Service Pack 1
user.country=AU
user.dir=C:\DevWork\personal\java\projects\wip\SystemProperties
user.home=C:\Users\shane
user.language=en
user.name=shane
user.script=
user.timezone=
user.variant=
Updated
If I use...
g2d.translate(originX, originY);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(angle), 0, 0);
Instead of the AffineTransform, it works fine. I've noted that it doesn't matter how I use the AffineTransform (translation only, rotation only, scale only) I seem to get the same results
Updated with example image
Example showing resizing frame...
nb The last position shift of the rectangle is a result of the MouseListener mentioned below
However, if I add a MosueListener to the DrawPane (either direclty within the class or externally via the myPanel instance) and call repaint on mouseClicked, it re-aligns correctly :P
Updated
If I translate a Shape, using the following
AffineTransform at = new AffineTransform();
at.translate(originX, originY);
at.scale(scale, scale);
at.rotate(Math.toRadians(angle), 0, 0);
g2d.setTransform(at);
Rectangle2D rect = new Rectangle2D.Double(xOffset, yOffset, rectWidth, rectHeight);
Shape shape = at.createTransformedShape(rect);
System.out.println(rect.getBounds());
System.out.println(shape.getBounds());
The resulting output is in align with expectations, but the (graphics) output is still wrong...
The AffineTransform associated with the graphics context passed to paintComponent() is not always the identity transform. For reasons that aren't clear, the m12 entry has the value 38.0 initially and after resizing the enclosing frame. Trivially, one can modify the copy supplied by g.create().
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = g2d.getTransform();
at.translate(originX, originY);
g2d.setTransform(at);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(angle), 0, 0);
g2d.fillRect(xOffset, yOffset, rectWidth, rectHeight);
g2d.dispose();
Addendum: As #MadProgrammer observes, "If I remove the controls from the GUI, it appears … in the center." Indeed, the observed horizontal offset is precisely the preferred width of the slider in BorderLayout.WEST. I suspect that the origin is adjusted after a resize to meet the Component#paintAll() contract more easily: "The origin of the graphics context, its (0, 0) coordinate point, is the top-left corner of this component."
Related
In the following reproducer, I am reproducing what I'm seeing in the graphical interface.
I'd like to repaint a dirty region of a canvas (a JPanel), because a full repaint introduces more latencies.
However when the repaint (dirty rectangle) function is invoked, it causes some issues with nested Graphics2D objects. In particular
This code may mis-use clip bounds, but I wasn't expecting this "artifact" upon the creation of the sub (nested) graphic context.
What would be the proper way to deal with clips, nested graphic contexts, and the dirty region (or rectangle) repaint in this situation.
My intuition tells me the code should compute its shape without using the clipping area.
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
public class Reproducer {
public static void main(String[] args) {
var comp = new MyJPanel();
var timer = new Timer(2_000, e -> {
SwingUtilities.invokeLater(comp::bug);
});
timer.start();
timer.setRepeats(true);
SwingUtilities.invokeLater(() -> {
var frame = new JFrame("Reproducer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(new Dimension(300, 300));
frame.setVisible(true);
frame.getContentPane().add(comp);
});
}
private static class MyJPanel extends JPanel {
public MyJPanel() {
setSize(600, 600);
setPreferredSize(getSize());
}
#Override
protected void paintComponent(Graphics g) {
var g2 = (Graphics2D) g;
super.paintComponent(g2);
g2.setColor(Color.cyan);
g2.fill(g2.getClip());
var graphicsBox = (Graphics2D) g2.create(10, 10, 290, 290);
var clipBounds = graphicsBox.getClipBounds();
graphicsBox.setColor(Color.LIGHT_GRAY);
graphicsBox.setStroke(new BasicStroke(2));
graphicsBox.drawRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
graphicsBox.setColor(Color.red);
graphicsBox.fill(new Rectangle2D.Double(clipBounds.x + 2,
clipBounds.y + 2,
clipBounds.width - 2,
clipBounds.height - 2));
graphicsBox.dispose();
var graphicsOverlay = (Graphics2D) g2.create(100, 100, 100, 100);
graphicsOverlay.setColor(Color.YELLOW);
graphicsOverlay.fillOval(10, 10, 60, 60);
graphicsOverlay.dispose();
}
public void bug() {
getVisibleRect();
repaint(100, 100, 100, 100);
}
}
}
EDIT by moving the fill above drawRect
So, there are a couple of "mistakes"
I found this...
graphicsBox.drawRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
graphicsBox.setColor(Color.red);
graphicsBox.fill(new Rectangle2D.Double(clipBounds.x + 2,
clipBounds.y + 2,
clipBounds.width - 2,
clipBounds.height - 2));
to be a little erroneous, which meant when a clip was applied, the background area was not been filled properly.
The call to super.paintComponent was filling the clip area with the background color of the component, but graphicsBox.fill wasn't filling the entire area, allow part of the background color to be seen.
I also found the result of Graphics2D#create(int, int, int, int) to be a little surprising
The new Graphics object has its origin translated to the specified point (x, y). Its clip area is determined by the intersection of the original clip area with the specified rectangle.
(emphasis added by me)
I mean, honestly, I shouldn't be surprised, but I've just never used these features before so it took me a little by surprise.
So, I corrected the graphicsBox rendering code so that the fill will fill the entire clipping area first and THEN the rectangle is drawn (within the bounding rectangle)
var graphicsBox = (Graphics2D) g2.create(10, 10, 290, 290);
var clipBounds = graphicsBox.getClipBounds();
graphicsBox.setColor(Color.red);
graphicsBox.fill(new Rectangle2D.Double(clipBounds.x,
clipBounds.y,
clipBounds.width,
clipBounds.height));
Collections.shuffle(colors);
graphicsBox.setColor(colors.get(0));
graphicsBox.setStroke(new BasicStroke(2));
graphicsBox.drawRect(clipBounds.x + 1, clipBounds.y + 1, clipBounds.width - 2, clipBounds.height - 2);
graphicsBox.dispose();
nb: The reference to colors is just a list of Colors which I randomise during the paint process, so I can "see" what's going on
Another trick might be to change the background color of the component to make it easier to "see" where things are been updated incorrectly.
When you have issues like this, it's really important to reduce the amount of clutter to see if you can narrow down the problem, I took out most of the paint code to figure out all these "little" things which were combining to give you issues.
Runnable example...
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
var comp = new MyJPanel();
var timer = new Timer(2_000, e -> {
SwingUtilities.invokeLater(comp::bug);
});
timer.start();
timer.setRepeats(true);
SwingUtilities.invokeLater(() -> {
var frame = new JFrame("Reproducer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(new Dimension(300, 300));
frame.setVisible(true);
frame.getContentPane().add(comp);
});
}
private static class MyJPanel extends JPanel {
private List<Color> colors;
public MyJPanel() {
setSize(600, 600);
setPreferredSize(getSize());
colors = Arrays.asList(new Color[] {
Color.BLACK,
Color.BLUE,
Color.CYAN,
Color.DARK_GRAY,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.ORANGE,
Color.PINK,
Color.RED,
Color.WHITE,
Color.YELLOW
});
}
#Override
protected void paintComponent(Graphics g) {
var g2 = (Graphics2D) g;
super.paintComponent(g2);
g2.setColor(Color.cyan);
g2.fill(g2.getClip());
var graphicsBox = (Graphics2D) g2.create(10, 10, 290, 290);
var clipBounds = graphicsBox.getClipBounds();
graphicsBox.setColor(Color.red);
graphicsBox.fill(new Rectangle2D.Double(clipBounds.x,
clipBounds.y,
clipBounds.width,
clipBounds.height));
Collections.shuffle(colors);
graphicsBox.setColor(colors.get(0));
graphicsBox.setStroke(new BasicStroke(2));
graphicsBox.drawRect(clipBounds.x + 1, clipBounds.y + 1, clipBounds.width - 2, clipBounds.height - 2);
graphicsBox.dispose();
var graphicsOverlay = (Graphics2D) g2.create(100, 100, 100, 100);
graphicsOverlay.setColor(Color.YELLOW);
graphicsOverlay.fillOval(10, 10, 60, 60);
graphicsOverlay.dispose();
}
public void bug() {
getVisibleRect();
repaint(100, 100, 100, 100);
}
}
}
If you're having "performance" issues, you might consider using a BufferedImage as the primary canvas and update it, then simply paint the image.
Alternatively, it might be time to move over to using BufferStrategy, this is going to get you as close to the "metal" as Java/Swing allows
I am trying to create an volume level bar where different level's of volume are represented with an different color here is my approach
I have 2 arrays
Color[] scales gives different color representation for each volume level if the length of this array is lets say 4 then there are 4 volume level's and so on
float[] weights denotes how much percentage/space each color should occupy in the bar
For example
private final Color scales[]={Color.GREEN,Color.YELLOW,Color.RED};
private final float weights[]={0.3f,0.2f,0.5f};
means there are 3 levels of volume where
if the current volume is <=30% of max then 30% of the volume bar is covered in GREEN
if the current volume is >30% and <= (30+20)=50% the next 20% of the volume bar is covered in YELLOW
if the current volume is >50% and <= (50+50)=100% the final 50% of the volume bar is covered in RED
Now the user interacts with the volume bar by clicking & dragging the mouse hence let' say if the volume bar has dimensions (x=120,y=50) and lets say I click or drag till Xposition=25 then
30% of 120=36
XPosition=25
25<36 hence must draw an green color rect of dimensions x=0,y=0,width=36-25=12,height=50
and we continue likewise for the remaining positions calculating where the user clicks and drawing different color rectangles till that point.
Now I think I butchered the explanation but I am not asking for the code I already have it implemented here
final class VolumeBar extends JPanel
{
VolumeBar()
{
super(new BorderLayout());
add(Box.createRigidArea(new Dimension(500,100)),BorderLayout.NORTH);
add(Box.createRigidArea(new Dimension(500,100)),BorderLayout.SOUTH);
JPanel container=new JPanel(new BorderLayout());
container.add(Box.createRigidArea(new Dimension(120,50)),BorderLayout.WEST);
container.add(Box.createRigidArea(new Dimension(120,50)),BorderLayout.EAST);
container.add(new JVolume(),BorderLayout.CENTER);
add(container,BorderLayout.CENTER);
}
private final class JVolume extends JLabel
{
private final Color scales[]={Color.GREEN,Color.YELLOW,Color.RED};
private final float weights[]={0.6f,0.2f,0.2f};
private int endingX;
private JVolume()
{
addMouseMotionListener(new Drag());
addMouseListener(new Click());
setPreferredSize(new Dimension(260,50));
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2d=(Graphics2D)g;
Dimension size=getSize();
float endPoints[]=new float[scales.length+1];
endPoints[0]=0;
for(int i=1;i<endPoints.length;i++){endPoints[i]=endPoints[i-1]+(size.width*weights[i-1]);}
for(int i=1;i<endPoints.length;i++)
{
float
prev=endPoints[i-1],
current=endPoints[i];
if(endingX>prev)
{
g2d.setColor(scales[i-1]);
g2d.fill(new Rectangle2D.Float(prev,0,endingX>current?current-prev:endingX-prev,size.height));
}
else{break;}
}
g2d.setColor(getBackground());
Polygon clear=new Polygon();
clear.addPoint(0,0);
clear.addPoint(size.width,0);
clear.addPoint(0,size.height);
clear.addPoint(0,0);
g2d.fill(clear); //clear the upper left triangle with background to make it look like an increasing triangle
g2d.setColor(Color.BLACK); draw the lower right triangle to give the bar some border
Polygon polygon=new Polygon();
polygon.addPoint(1,size.height-1);
polygon.addPoint(size.width-1,1);
polygon.addPoint(size.width-1,size.height-1);
polygon.addPoint(1,size.height-1);
g2d.drawPolygon(polygon);
}
private void compute(MouseEvent m)
{
endingX=m.getX();
repaint();
}
private final class Drag extends MouseMotionAdapter
{
#Override
public void mouseDragged(MouseEvent m){compute(m);}
}
private final class Click extends MouseAdapter
{
#Override
public void mouseClicked(MouseEvent m){compute(m);}
}
}
}
And here is how it look's like just click or drag anywhere on the bar
Look's great for the most part but what I am aiming for is the volume bar in VLC media player
Forgive the blurriness, I had to scale the image up but if you look closely you can notice how the color's blend at the boundary for example there is an gradient from green->yellow making some white in between at the boundary and there is an gradient from yellow->red making some orange in between
I want to achieve this gradient.
Ideas anyone?
As recommended by #AndrewThompson in the comments above you can use MultipleGradientPaint using a LinearGradientPaint.
Currently I'm confused by which of the 2 you need, above you said:
if the current volume is <=30% of max then 30% of the volume bar is covered in GREEN
if the current volume is >30% and <= (30+20)=50% the next 20% of the volume bar is covered in YELLOW
if the current volume is >50% and <= (50+50)=100% the final 50% of the volume bar is covered in RED
And then you said:
I need 60% of my bar in green then 20% in yellow not the other way around which is what LinearGradient requires.you see why it's a problem now?
But those are just numbers.
What you can do is create your LinearGradientPaint this way:
Rectangle2D rect = new Rectangle2D.Double(10, 10, 250, 150);
Point2D startPoint = new Point2D.Double(rect.getMinX(), rect.getCenterY());
Point2D endPoint = new Point2D.Double(rect.getMaxX(), rect.getCenterY());
float[] percentages = new float[] {0.0f, 0.6f, 0.8f};
Color[] colors = new Color[] {Color.GREEN, Color.YELLOW, Color.RED};
LinearGradientPaint gradient = new LinearGradientPaint(startPoint, endPoint, percentages, colors, CycleMethod.REPEAT);
You were confused at where you need to start the gradient because I guess your percentages were: 0.6f, 0.2f, 0.2f but instead you need to define the start point and then add the next percentage from there, 0.0f, 0.6f, 0.8f (This will start at 0% then go up to 60% to green, and then up to 80% to yellow, and then the rest to 100% on red.
And you get this output.
MRE for you to test changes:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.RenderingHints;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class LinearGradientExample {
private JFrame frame;
private JPanel pane;
public static void main(String[] args) {
SwingUtilities.invokeLater(new LinearGradientExample()::createAndShowGUI);
}
#SuppressWarnings("serial")
private void createAndShowGUI() {
frame = new JFrame(getClass().getSimpleName());
Rectangle2D rect = new Rectangle2D.Double(10, 10, 250, 150);
Point2D startPoint = new Point2D.Double(rect.getMinX(), rect.getCenterY());
Point2D endPoint = new Point2D.Double(rect.getMaxX(), rect.getCenterY());
float[] percentages = new float[] {0.0f, 0.6f, 0.8f};
Color[] colors = new Color[] {Color.GREEN, Color.YELLOW, Color.RED};
LinearGradientPaint gradient = new LinearGradientPaint(startPoint, endPoint, percentages, colors, CycleMethod.REPEAT);
pane = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(gradient);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.fill(rect);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(300, 200);
}
};
frame.add(pane);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
I'm making a simple 2-D game for which I'd like to move the camera with the mouse. There are loads of better ways to do this, I'm sure, but I decided to try out the Graphics2D method setTransform().
AffineTransform at = new AffineTransform();
at.translate(mousex, 0);
Graphics2D g2d = (Graphics2D)g.create();
g2d.setTransform(at);
However, the graphics don't translate linearly with the mouse, as you can see from the images below. For the first few pixels it seems to move correctly, but it kind of slows down later?
By the way, the mouse is indicated by the blue circle.
When the mouse is near the edge of the frame, the graphics moves almost linearly with it to the right.When the mouse is dragged further to the right, the graphics moves with it, but with a kind of lag (it shouldn't)
The white border around the blocks represents the outline of the graphics that should be moving.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
public class Bruh extends JPanel implements MouseMotionListener
{
int mousex = 0;
public static void main(String[] args) {
JFrame f = new JFrame();
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setUndecorated(true);
f.add(new Bruh());
f.setVisible(true);
}
Bruh()
{
setBackground(Color.ORANGE);
addMouseMotionListener(this);
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
AffineTransform at = new AffineTransform();
at.translate(mousex, 0);
Graphics2D g2d = (Graphics2D)g.create();
g2d.setTransform(at);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 100, 100, 100);
g2d.dispose();
g.setColor(Color.BLUE);
g.fillOval(mousex-5, 200-5, 10, 10);
repaint();
}
#Override
public void mouseDragged(MouseEvent e) {
mousex = e.getX();
}
#Override
public void mouseMoved(MouseEvent e) {
mousex = e.getX();
}
}
TL;DR
The setTransform(AffineTransform at) function of Graphics2D isn't working as intended. Any help is appreciated :)
Okay so I got the answer. The problem isn't in my code, or with my touchpad scrolling.
On my laptop, I had set the scaling of the display to 125%, causing everything to work normally EXCEPT apps that use default scaling - Java being one of them.
The problem was that my mouse moved correctly (because that's what mice do) but the in-built graphics of java were responding to the default scaling of the display i.e. 125%. So everything was moving 1.25 times slower than expected.
I'd like to make a Java panel that creates objects where the user clicks. Since my actual application uses a MVC approach I'd like also for these objects to be able to repaint themselves when a model is changed, and provide menus to change their properties.
I think that the best way to control their x and y locations would be to take a canvas based approach whereby the JPanel calls a draw method on these objects from the paintComponent method. This however will only draw the shape on the canvas and does not add the object itself loosing all abilities to control object properties. I'd be very grateful if someone could tell me the best approach for what I want to do.
I've created some sample code which can be seen below. When clicked I'd like the circle to change colour, which is implemented using a MouseListener (it basically represents changing the models properties in this small example). Also I'd just like to make sure that zooming in/out still works with any sample code/advice can provide so I've added buttons to zoom the objects in and out as a quick test.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import java.awt.geom.Ellipse2D;
public class Main {
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ExamplePanel panel = new ExamplePanel();
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
});
}
//I could not get this to with when it extended JLayeredPane
private static class ExamplePanel extends JPanel {
private static final int maxX = 500;
private static final int maxY = 500;
private static double zoom = 1;
private static final Circle circle = new Circle(100, 100);
public ExamplePanel() {
this.setPreferredSize(new Dimension(maxX, maxY));
this.setFocusable(true);
Button zoomIn = new Button("Zoom In");
zoomIn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoom += 0.1;
repaint();
}
});
add(zoomIn);
Button zoomOut = new Button("Zoom Out");
zoomOut.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoom -= 0.1;
repaint();
}
});
add(zoomOut);
// add(circle); // Comment back in if using JLayeredPane
}
#Override
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.scale(zoom, zoom);
super.paintComponent(g);
circle.paint(g); // Comment out if using JLayeredPane
}
}
static class Circle extends JPanel {
private Color color = Color.RED;
private final int x;
private final int y;
private static final int DIMENSION = 100;
public Circle(int x, int y) {
// setBounds(x, y, DIMENSION, DIMENSION);
this.x = x;
this.y = y;
addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
color = Color.BLUE;
}
#Override
public void mouseReleased(MouseEvent e) {
}
});
}
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setPaint(color);
g2.fillOval(x, y, DIMENSION, DIMENSION);
}
// I had some trouble getting this to work with JLayeredPane even when setting the bounds
// In the constructor
// #Override
// public void paintComponent(Graphics g) {
// Graphics2D g2 = (Graphics2D) g;
// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// g2.setPaint(color);
// g2.fillOval(x, y, DIMENSION, DIMENSION);
// }
#Override
public Dimension getPreferredSize(){
return new Dimension(DIMENSION, DIMENSION);
}
}
}
As an aside I did try using a JLayeredPane(useful because I'd also like to layer my objects) but could not get my objects to even render. I know it has no default layout manager so tried calling setBounds in the circle in the constructor, but sadly it did not work. I know it's better to use a layout manager but can't seem to find one suitable for my needs!
Thanks in advance.
Don't override paint components, use paintComponent and don't forget to call super.paintComponent
A component already has a concept of "location", so when painting, the top left position of your component is actually 0x0
What you are doing is actually painting beyond the boundaries of you component
For example, if you place your Circle at 100x100 and then did...
g2.fillOval(x, y, DIMENSION, DIMENSION);
You would actually start painting at 200x200 (100 for the actual location of the component and 100 for you additional positioning).
Instead use
g2.fillOval(x, y, DIMENSION, DIMENSION);
And go back and try using JLayeredPane.
You could actually write your own layout manager that takes the location of the component and it's preferred size and updates the components bounds and then apply this to a JLayeredPane. This gives you the "benefits" of an absolute layout, but keeps you within how Swing works to update its components when things change.
You should also be careful with doing anything like...
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
The Graphics context is a shared resource. That means, anything you apply to, will still be in effect when the next component is painted. This may produce some strange results.
Instead try using...
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//...
g2.dispose();
Updated
For zooming I would take a closer look at JXLayer (or JLayer in Java 7)
The JXLayer (and excellent PBar extensions) have gone quite on the net, so you can grab a copy from here
(I tried finding a better example, but this is the best I could do with the limited time I have available)
Updated with working zooming example
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.RenderingHints;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jdesktop.jxlayer.JXLayer;
import org.pbjar.jxlayer.demo.TransformUtils;
import org.pbjar.jxlayer.plaf.ext.transform.DefaultTransformModel;
public class TestJLayerZoom {
public static void main(String[] args) {
new TestJLayerZoom();
}
public TestJLayerZoom() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JXLayer<JComponent> layer;
private DefaultTransformModel transformModel;
private JPanel content;
public TestPane() {
content = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridy = 0;
JLabel label = new JLabel("Hello");
JTextField field = new JTextField("World", 20);
content.add(label, gbc);
content.add(field, gbc);
gbc.gridy++;
gbc.gridwidth = GridBagConstraints.REMAINDER;
final JSlider slider = new JSlider(50, 200);
slider.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
int value = slider.getValue();
double scale = value / 100d;
transformModel.setScale(scale);
}
});
content.add(slider, gbc);
transformModel = new DefaultTransformModel();
transformModel.setScaleToPreferredSize(true);
Map<RenderingHints.Key, Object> hints = new HashMap<>();
//hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//hints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
//hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
layer = TransformUtils.createTransformJXLayer(content, transformModel, hints);
setLayout(new BorderLayout());
add(layer);
}
}
}
I've left the rendering hints in to demonstrate their use, but I found that they screwed with the positing of the cursor within the text field, but you might like to have a play
I'd just like to add that I fixed the zooming issue not in the way suggested by the answer, but just by keeping the line that applied a scaled transform call in the ExamplePanel paintComponent method:
g2.scale(zoom, zoom);
I thought that this was the nicest implementation since none of the components require any knowledge about zooming and it seemed far simpler than JLayer since I only required basic zooming functionalities.
How could I make non-rectangular windows with soft borders in Java?
Soft borders (also known as soft clipping) are borders without aliasing artifacts.
I searched the web a lot and found several posts about translucent and/or
non-rectangular windows.
The topic "soft border" is confusing. It seems that the information I found deals
with applying soft borders to component which are inside another Java components.
But, can I, or can I not apply soft borders to custom shaped JWindow which is
placed just on the desktop?
I am primely referring to following post:
http://today.java.net/pub/a/today/2008/03/18/translucent-and-shaped-swing-windows.html
When it comes to soft clipping, the article forwards to
http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html
But here, soft clipping on an existing Graphics2D object is described.
Here's my take on a soft-clipped, shaped, top-level window. Note: shaped windows use a proprietary API (com.sun.awt.AWTUtilities) and is not guaranteed to work on non-Sun JVMs. However, in JDK 7 it becomes part of the Window class.
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class MySoftClippedWindow extends JPanel {
public MySoftClippedWindow() {
super();
setLayout(new GridBagLayout());
JButton button = new JButton(new AbstractAction("Close") {
#Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
button.setOpaque(false);
add(button);
}
#Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
int width = getWidth();
int height = getHeight();
// Create a soft clipped image for the background
BufferedImage img = java_2d_tricker(g2d, width, height);
g2d.drawImage(img, 0, 0, null);
g2d.dispose();
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
final JWindow w = new JWindow();
Container cp = w.getContentPane();
cp.setLayout(new BorderLayout());
cp.add(new MySoftClippedWindow());
w.setAlwaysOnTop(true);
com.sun.awt.AWTUtilities.setWindowOpaque (w, false);
w.setSize(200, 200);
w.setVisible(true);
}
});
}
/*
* This code is taken from
* http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html
*/
private BufferedImage java_2d_tricker(Graphics2D g2d, int width, int height) {
GraphicsConfiguration gc = g2d.getDeviceConfiguration();
BufferedImage img = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
Graphics2D g2 = img.createGraphics();
g2.setComposite(AlphaComposite.Clear);
g2.fillRect(0, 0, width, height);
g2.setComposite(AlphaComposite.Src);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.WHITE);
g2.fillOval(width / 4, height / 4, width / 2, height / 2);
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW));
g2.fillRect(0, 0, width, height);
g2.dispose();
return img;
}
}
Have you read this article:
http://www.pushing-pixels.org/?p=272
It mentions soft clipping and the previous articles you mentioned, but also includes some source code to implement a soft clipped window, the direct link is here:
http://www.pushing-pixels.org/wp-content/uploads/2008/03/softclippedwindow.java
That should provide you with a possible solution for what you want to do.
import java.awt.*;
public class First extends Applet
{
public void paint(Graphics g)
{
g.drawRect(100,50,500,800);
}
}
/*<Applet code="First.class"height=500 width=500>
</Applet>
*/