I've created this custom JToolTip for my application. When the tooltip is entirely diplayed inside a JFrame, no background is visible (expected), but when the tooltip is displayed outside the JFrame, the background will be visible. How can I have it removed either way?
I've tried setBackground(new Color(255, 255, 255, 0)); with the '0' alpha value to make sure the background is transparent, but that didn't do the trick.
The tooltip inside the frame, as expected:
The tooltip exceeding the JFrame, with the unwanted background:
The custom JTooltip:
public class DefaultToolTip extends JToolTip {
public DefaultToolTip() {
setOpaque(false);
setPreferredSize(new Dimension(275, 30));
setBackground(new Color(255, 255, 255, 0));
}
#Override
public void addNotify() {
super.addNotify();
setOpaque(false);
Component parent = this.getParent();
if (parent != null) {
if (parent instanceof JComponent) {
JComponent jparent = (JComponent) parent;
jparent.setOpaque(false);
}
}
}
#Override
public void paint(Graphics g) {
String text = getComponent().getToolTipText();
addNotify();
Graphics2D g2d = drawComponent(g);
drawText(text, g2d);
g2d.dispose();
}
private void drawText(String text, Graphics2D g2d) {
//Draw the text
int cHeight = getComponent().getHeight();
FontMetrics fm = g2d.getFontMetrics();
g2d.setColor(Color.WHITE);
if (cHeight > getHeight())
g2d.drawString(text, (getWidth() - fm.stringWidth(text)) / 2, (getHeight() + fm.getAscent()) / 2 + 2);
else
g2d.drawString(text, (getWidth() - fm.stringWidth(text)) / 2, (cHeight + fm.getAscent()) / 2 + 2);
}
private Graphics2D drawComponent(Graphics g) {
//Create a round rectangle
Shape round = new RoundRectangle2D.Float(0, 8, getWidth(), getHeight(), 8, 8);
//Draw the background
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.GRAY);
g2d.fill(round);
//Draw the left triangle
Point p1 = new Point(getWidth() / 2, getHeight() - 30);
Point p2 = new Point(getWidth() / 2 + 8, getHeight() - 20);
Point p3 = new Point(getWidth() / 2 - 8, getHeight() - 20);
int[] xs = {p1.x, p2.x, p3.x};
int[] ys = {p1.y, p2.y, p3.y};
Polygon triangle = new Polygon(xs, ys, xs.length);
g2d.fillPolygon(triangle);
return g2d;
}
}
Solution
A few things have changed to have the tooltip behave as expected. the paint method has been replaced by the paintComponent method, the addNotify call was removed, the method updated to fetch the window of the component and to give it a transparent background. setBorder(BorderFactory.createEmptyBorder()); was also needed to remove the components default border.
public class DefaultToolTip extends JToolTip {
public DefaultToolTip() {
setOpaque(false);
setPreferredSize(new Dimension(275, 30));
setBackground(new Color(255, 255, 255, 0));
setBorder(BorderFactory.createEmptyBorder());
}
#Override
public void addNotify() {
super.addNotify();
setOpaque(false);
Component parent = this.getParent();
if (parent != null) {
if (parent instanceof JComponent) {
JComponent jparent = (JComponent) parent;
jparent.setOpaque(false);
}
}
Window window = SwingUtilities.windowForComponent(this);
try {
window.setBackground(new Color(255, 255, 255, 0));
} catch (IllegalComponentStateException e) {
//Do nothing
}
}
#Override
public void paintComponent(Graphics g) {
//super.paintComponent(g);
String text = getComponent().getToolTipText();
Graphics2D g2d = drawComponent(g);
drawText(text, g2d);
g2d.dispose();
}
private void drawText(String text, Graphics2D g2d) {
//Draw the text
int cHeight = getComponent().getHeight();
FontMetrics fm = g2d.getFontMetrics();
g2d.setColor(Color.WHITE);
if (cHeight > getHeight())
g2d.drawString(text, (getWidth() - fm.stringWidth(text)) / 2, (getHeight() + fm.getAscent()) / 2 + 2);
else
g2d.drawString(text, (getWidth() - fm.stringWidth(text)) / 2, (cHeight + fm.getAscent()) / 2 + 2);
}
private Graphics2D drawComponent(Graphics g) {
//Create a round rectangle
Shape round = new RoundRectangle2D.Float(0, 8, getWidth(), getHeight(), 8, 8);
//Draw the background
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.GRAY);
g2d.fill(round);
//Draw the left triangle
Point p1 = new Point(getWidth() / 2, getHeight() - 30);
Point p2 = new Point(getWidth() / 2 + 8, getHeight() - 20);
Point p3 = new Point(getWidth() / 2 - 8, getHeight() - 20);
int[] xs = {p1.x, p2.x, p3.x};
int[] ys = {p1.y, p2.y, p3.y};
Polygon triangle = new Polygon(xs, ys, xs.length);
g2d.fillPolygon(triangle);
return g2d;
}
}
A note however, super.paintComponent(g) was commented out, since it would draw text another time.
Don't know if any of these will help but:
Don't override paint(...). Custom painting is done by overriding paintComponent(...).
Invoke super.paintComponent(...) as the first statement
Don't invoke addNotify() in a painting method. A painting method is for painting only.
with the '0' alpha value to make sure the background is transparent,
Swing components don't know how to handle transparent backgrounds. Just make the component non-opaque.
When the tooltip overlaps the component. The tooltip is actually added to a JWindow before it is displayed. So in your addNotify() logic, you can search for the window and make it transparent.
Check out:
Window window = SwingUtilities.windowForComponent(...);
Related
I need to draw an arc around a volume knob, the arc length represents the volume.
I tried with Graphics.drawArc() and Arc2D.Double, but the result is not good: the path of the arc is not exactly the same for different extent values.
public class ArcComponent extends JComponent
{
private ImageIcon knobImage = new ImageIcon(ArcComponent.class.getResource("Knob-w37.png"));
int arcLength = 220; // Degrees
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Paint knob image
knobImage.paintIcon(this, g2, 20, 20);
// Add arc
var arc = new Arc2D.Double(22, 22, 32, 32, 220 - arcLength, arcLength, Arc2D.OPEN);
g2.setColor(Color.CYAN);
g2.setStroke(new BasicStroke(2));
g2.draw(arc);
g2.dispose();
}
void shorter()
{
arcLength -= 5;
if (arcLength < 0)
{
arcLength = 260;
}
repaint();
}
void longer()
{
arcLength += 5;
if (arcLength > 260)
{
arcLength = 0;
}
repaint();
}
}
How could I ensure that the path of the arc is always the same?
EDIT: added knob image painting
A dirty trick: draw the complete circle and ovewrite the part that you don't need with a shape, same color of background.
Something like this:
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Shape circle=new Ellipse2D.Double(20, 20, 30, 30);
g2.setColor(Color.CYAN);
g2.setStroke(new BasicStroke(2f));
g2.draw(circle);
Shape arc=new Arc2D.Double(10,10,50,50,230,360-arcLength,Arc2D.PIE);
g2.setColor(getBackground());
g2.fill(arc);
g2.dispose();
}
Unfortunately this requires the arc to be drawn before any other thing in the same area, but you can probably deal with this.
EDIT: For the additional constraint, the inconsistent rendering is actually due to the stroke, so try creating a thick shape and fill it, example:
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
double thickness=8.0; //This replaces your stroke width
GeneralPath shape=new GeneralPath();
shape.append(new Arc2D.Double(20-thickness/2,20-thickness/2, 30+thickness, 30+thickness, 230, -arcLength, Arc2D.OPEN),false);
shape.append(new Arc2D.Double(20+thickness/2,20+thickness/2, 30-thickness, 30-thickness, 230-arcLength, arcLength, Arc2D.OPEN),true);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.CYAN);
g2.fill(shape);
g2.dispose();
}
Simply draw a full circle and clip on the arc shape (with some padding to ensure everything gets drawn).
Graphics2D g2 = (Graphics2D) g.create();
Ellipse2D circle = new Ellipse2D.Double(20, 20, 30, 30);
int d = 2;
Arc2D arc = new Arc2D.Double(20 - d, 20 - d, 30 + 2 * d, 30 + 2 * d, 230, -arcLength, Arc2D.PIE);
g2.setColor(Color.CYAN);
g2.setClip(arc);
g2.setStroke(new BasicStroke(2f));
g2.draw(circle);
knobImage.paintIcon(this, g, 20, 20);
So I am having trouble drawing Concentric Circles(think of a Bull's eye Target). My issue is that each circle I draw is not centered, instead shifts position from the original circle. Here is my code:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;
public class TargetPanel extends JPanel {
public TargetPanel() {
this.setPreferredSize(new Dimension(800,800));
}//end constructor
public void paintComponent(Graphics g) {
Color blue = new Color(0, 100, 0);
Color yellow = new Color(100, 0, 0);
super.paintComponent(g);
int dimension = 800;
int partition = 75;
drawCirlce(g, Color.WHITE, Color.BLACK, dimension);
}//end draw circle
private void drawCirlce(Graphics g, Color blue, Color yellow, int dimension) {
g.setColor(Color.WHITE);
g.fillOval((getHeight()- (dimension))/2, (getWidth()-(dimension))/2, dimension, dimension);
g.setColor(Color.BLACK);
g.drawOval((getHeight() - (dimension))/2, (getWidth()-(dimension))/2, dimension, dimension);
g.drawOval((getHeight()- (dimension))/2, (getWidth()- (dimension))/2, dimension-30, dimension-30);
}//end drawCircle
}//end main
I think I know what the issue is: the -30 shifts it, that being the case how do I form smaller cirlces with the origin of the circles being centered?
I think you need to subtract 30 from the dimension and then update the dimension value, like so:
private void drawCirlce(Graphics g, Color blue, Color yellow, int dimension) {
g.setColor(Color.WHITE);
g.fillOval((getHeight() - (dimension)) / 2, (getWidth() - (dimension)) / 2, dimension, dimension);
g.setColor(Color.BLACK);
g.drawOval((getHeight() - (dimension)) / 2, (getWidth() - (dimension)) / 2, dimension, dimension);
// Updated code here:
dimension -= 30;
g.drawOval((getHeight() - (dimension)) / 2, (getWidth() - (dimension)) / 2, dimension, dimension);
}//end drawCircle
Like this you should get the different circles at a regular distance.
The code below can be run and displays two circle lines at the same distance:
import javax.swing.*;
import java.awt.*;
public class Circles extends JFrame {
public Circles() throws HeadlessException {
this.setLayout(new BorderLayout());
final Dimension dimension = new Dimension(600, 600);
this.setSize(new Dimension(dimension.width + 50, dimension.height + 50));
this.add(new TargetPanel(dimension), BorderLayout.CENTER);
}
public static void main(String[] args) {
Circles circles = new Circles();
circles.setLocationRelativeTo(null);
circles.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
circles.setVisible(true);
}
}
class TargetPanel extends JPanel {
public TargetPanel(Dimension dimension) {
this.setPreferredSize(dimension);
}//end constructor
public void paintComponent(Graphics g) {
Color blue = new Color(0, 100, 0);
Color yellow = new Color(100, 0, 0);
super.paintComponent(g);
int dimension = this.getPreferredSize().width;
int partition = 75;
drawCircle(g, Color.WHITE, Color.BLACK, dimension);
}//end draw circle
private void drawCircle(Graphics g, Color blue, Color yellow, int dimension) {
g.setColor(Color.WHITE);
final int yCenter = (getHeight() - (dimension)) / 2;
final int xCenter = (getWidth() - (dimension)) / 2;
g.fillOval(xCenter, yCenter, dimension, dimension);
g.setColor(Color.BLACK);
g.drawOval(xCenter, yCenter, dimension, dimension);
dimension -= 30;
g.drawOval((getWidth() - (dimension)) / 2, (getHeight() - (dimension)) / 2, dimension, dimension);
}//end drawCircle
}//end main
I use this code: to outline a font:
public class MyText extends JPanel {
String text1 = null;
public MyText (String text) {
text1 = text;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
setBackground(Color.white);
int w = getSize().width;
int h = getSize().height;
Graphics2D g2d = (Graphics2D) g;
FontRenderContext fontRendContext = g2d.getFontRenderContext();
Font font = new Font("Verdana", 1, 72);
String st = new String(text1);
TextLayout text = new TextLayout(st, font, fontRendContext);
Shape shape = text.getOutline(null);
Rectangle rect = shape.getBounds();
AffineTransform affineTransform = new AffineTransform();
affineTransform = g2d.getTransform();
affineTransform.translate(w / 2 - (rect.width / 2), h / 2
+ (rect.height / 2));
g2d.transform(affineTransform);
g2d.setColor(Color.black);
g2d.draw(shape);
g2d.setClip(shape);
}
The problem is I have no idea how to adjust the thickness of the outline.
I tried displaying another bigger string over the first string, but the result is quite bad (wrong pixels...).
Hav you got any ideas?
Thanks in advance.
You can use setStroke. For example
g2d.setStroke(new BasicStroke(4));
Start by taking a look at Stroking and Filling Grapcs Primitives and Fun with Java2D - Strokes
I am using a paintComponent Class in a project of mine, and I am currently wondering how I can decrease the size of the rectangle from the top making it's way downwards.
This is the part of the code:
public Battery(){
super();
firstTime = true;
f = new Font("Helvetica", Font.BOLD, 14);
m = Toolkit.getDefaultToolkit().getFontMetrics(f);
}
public void paintComponent(Graphics g){
if(firstTime){
firstTime = false;
batteryLevel = 1 + this.getHeight();
decr = batteryLevel / 20;
}else{
g.setColor(Color.RED);
g.fillRect(1, 0, this.getWidth(), this.getHeight());
g.setColor(Color.GREEN);
g.fillRect(1, 0, this.getWidth(), batteryLevel);
g.setColor(Color.BLACK);
g.setFont(f);
g.drawString("TEST", (getWidth() - m.stringWidth("TEST")) / 2 , this.getHeight() / 2);
}
}
public void decreaseBatteryLevel(){
batteryLevel -= decr;
this.repaint();
}
PS. Sorry if I did something wrong, I'm new to this forum.
As you want the visible battery level to descend you will want to increase your Y co-ordinate in relation to the value of batteryLevel. You could use:
g.fillRect(1, getHeight() - batteryLevel, getWidth(), batteryLevel);
Instead
g.fillRect(1, 0, this.getWidth(), batteryLevel);
Do
g.fillRect(1, batteryLevel, this.getWidth(), getHeight() - batteryLevel);
Also maybe repaint(50L) instead of repaint().
If your question meant: how to animate a change in the battery level.
Use a javax.swing.Timer:
int toPaintBatteryLevel = batteryLevel;
// In the paintComponent paint upto toPaintBatteryLevel.
Timer timer = new Timer(100, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (toPaintBatteryLevl == batteryLevel) {
return;
}
if (toPaintBatteryLevl > batteryLevel) {
--toPaintBatteryLevel; // Animate slowly
} else {
toPaintBatteryLevel = batteryLevel; // Change immediately
}
repaint(50L);
};
});
timer.start();
For ease of coding, there is a permanent timer. And externally one changes the batteryLevel,
and the time determines the toPaintBatteryLevel, which paintComponent uses to paint.
I have an animation, and would like to make it disappear progressively when it reaches the left side of the display area.
AffineTransform at = new AffineTransform();
at.scale(-1, 1);
at.translate((-clip.x + (clip.x - xPositionToPixel(imgX))), clip.y - 1);
if ((clip.x - clip.width) < (at.getTranslateX() - bufImg.getWidth())) {
g2d.drawImage(bufImg, at, null);
} else {
at = new AffineTransform();
at.scale(-1, 1);
at.translate((-clip.x + (clip.x - xPositionToPixel(imgX))), clip.y - 1);
g2d.drawImage(bufImg, (int)at.getTranslateX(), clip.y - 1, (int)(bufImg.getWidth() - (xPositionToPixel(imgX) + bufImg.getWidth())), clip.height, null);
}
I'm drawing the animation from right to left, that is the reason why i scale and translate each the coordinate. clip.x is the start of the display area, and imgX is the new x coordinate.
Thanks for your help.
I tried several way of achieving what i want and the closest is this :
AffineTransform at = new AffineTransform();
at.scale(-1, 1);
at.translate((-clip.x + (clip.x - xPositionToPixel(imgX))), clip.y - 1);
if ((clip.x - clip.width) < (at.getTranslateX() - bufImg.getWidth())) {
g2d.drawImage(bufImg, at, null);
} else {
at = new AffineTransform();
at.scale(-1, 1);
at.translate((-clip.x + (clip.x - xPositionToPixel(imgX))), clip.y - 1);
g2d.drawImage(bufImg, (int)at.getTranslateX(), clip.y - 1, (int)(bufImg.getWidth() - (xPositionToPixel(imgX) + bufImg.getWidth())), clip.height, null);
}
But still not a good soluation has i'm just shrinking the width of my image, but still draw it entirely.
Still don't know what exactly the problem is (animation or transform?), here's a simple snippet to play with:
final JButton button = new JButton("Swinging!");
button.setSize(button.getPreferredSize());
button.doLayout();
final BufferedImage image = new BufferedImage(
button.getWidth(), button.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
button.paint(g);
g.dispose();
final Point location = new Point(500, 100);
final JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D gd = (Graphics2D) g.create();
gd.translate(location.x, location.y);
gd.scale(-1, 1);
gd.drawImage(image, 0, 0, this);
gd.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(button.getWidth()* 10, button.getHeight() * 20);
}
};
ActionListener l = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
location.x -= 5;
panel.repaint();
if (location.x < - button.getWidth()) {
((Timer) e.getSource()).stop();
}
}
};
new Timer(100, l).start();