5

Question:

What does calling setVisible(true) on a JFrame which is already visible do? I was digging through the source code of JFrame, and ultimately, it boils down to this function in Component which does nothing to a frame if it is already visible. Why does it act like revalidate(); repaint();? (See SSCCE below)


Motivation:

I am working on a java app, for which I wrote a class JImagePanel which extends JPanel and allows the user to set an image as the background (see SSCCE). I have found that after editing the background of the panel, I was having issues repainting the background to the correct size. After scouring the internet, I found that the following works:

if(frame.isVisible()) frame.setVisible(true);

Ultimately, I solved the issue using

panel.revalidate();
panel.repaint();

, which I think is the better solution, but it got me to thinking what setVisible(true) actually does on an already visible frame. From my viewpoint, it shouldn't work - but in fact it does.


SSCCE

Here is an example that illustrates my issue. If nothing else, hopefully you find this class extremely useful in the future.

NOTE: Updated source of this file can be found on the project homepage on GitHub of the project this was created for.

Enjoy!

package com.dberm22.utils;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class JImagePanel extends JPanel {

    private static final long serialVersionUID = 6841876236948317038L;
    private Image img = null;
    private Position position = Position.CENTER;

  public enum Position{
      STRETCH,
      CENTER,
      FIT,
      FILL,
      NONE;
  }

  public JImagePanel() {
      }

  public JImagePanel(String img) {
    this(new ImageIcon(img).getImage());
  }

  public JImagePanel(Image img) {

      setBackgroundImage(img);
  }

  @Override
  public void paintComponent(Graphics g)
  {
    super.paintComponent(g);

    Graphics2D g2 = (Graphics2D) g;

    g2.setColor(getBackground());
    g2.fillRect(0, 0, getWidth(), getHeight());

    if (this.position.equals(Position.STRETCH))
    { 
        if(this.img != null) g2.drawImage(img, 0, 0, getWidth(), getHeight(), null); 
    }
    else if (this.position.equals(Position.FILL) || this.position.equals(Position.FIT))
    { 
        if(this.img != null)
        {

             double scaleFactor = getScaleFactor(new Dimension(img.getWidth(null), img.getHeight(null)), getSize());
             int scaleWidth = (int) Math.round(img.getWidth(null) * scaleFactor);
             int scaleHeight = (int) Math.round(img.getHeight(null) * scaleFactor);

             //Image img_scaled = img.getScaledInstance(scaleWidth, scaleHeight, Image.SCALE_SMOOTH);
            g2.drawImage(scaleImage(img, scaleWidth, scaleHeight, getBackground()), (getWidth() - scaleWidth)/2, (getHeight() - scaleHeight)/2, scaleWidth, scaleHeight, null); 
        }
    }
    else if (this.position.equals(Position.CENTER)) { if(this.img != null) g2.drawImage(img, (getWidth() - img.getWidth(null))/2, (getHeight() - img.getHeight(null))/2, null); }
  }

  public void setBackgroundImage(String img) 
  {
      setBackgroundImage(new ImageIcon(img).getImage());
  }

  public void setBackgroundImage(Image img)
  {
        this.img = img;
        Dimension size = new Dimension(img.getWidth(null), img.getHeight(null));
        setPreferredSize(size);
        setMinimumSize(size);
        setMaximumSize(size);
        setSize(size);

        repaint();
  }

  public static double getScaleFactor(int iMasterSize, int iTargetSize) {

        double dScale = 1;
        if (iMasterSize > iTargetSize) {

            dScale = (double) iTargetSize / (double) iMasterSize;

        } else {

            dScale = (double) iTargetSize / (double) iMasterSize;

        }

        return dScale;

    }

    public double getScaleFactor(Dimension original, Dimension targetSize) {

        double dScale = 1d;

        if (original != null && targetSize != null) {

            double dScaleWidth = getScaleFactor(original.width, targetSize.width);
            double dScaleHeight = getScaleFactor(original.height, targetSize.height);

            if (this.position.equals(Position.FIT)) dScale = Math.min(dScaleHeight, dScaleWidth);
            else if(this.position.equals(Position.FILL)) dScale = Math.max(dScaleHeight, dScaleWidth);

        }

        return dScale;

    }

    public BufferedImage scaleImage(Image img, int width, int height, Color background) {

        BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = newImage.createGraphics();
        try {
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                    RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            g.setBackground(background);
            g.clearRect(0, 0, width, height);
            g.drawImage(img, 0, 0, width, height, null);
        } finally {
            g.dispose();
        }
        return newImage;
    }

  public void setBackgroundImagePosition(String pos)
  {
      if("Stretch".equals(pos)) setBackgroundImagePosition(Position.STRETCH);
      else if("Center".equals(pos))  setBackgroundImagePosition(Position.CENTER);
      else if("Fit".equals(pos)) setBackgroundImagePosition(Position.FIT);
      else if("Fill".equals(pos))  setBackgroundImagePosition(Position.FILL);
      else if("None".equals(pos)) setBackgroundImagePosition(Position.NONE);
  }
  public void setBackgroundImagePosition(Position pos)
  {
      this.position = pos;
      repaint();
  }

  public static void main(String[] args)
  {

      JFrame frame = new JFrame("JImagePanel Test");
      frame.setSize( Toolkit.getDefaultToolkit().getScreenSize());
      frame.setPreferredSize( Toolkit.getDefaultToolkit().getScreenSize());
      frame.setExtendedState(JFrame.MAXIMIZED_BOTH); //sets appropriate size for frame

      JImagePanel panel = new JImagePanel();
      frame.add(panel);

      frame.setVisible(true);

      try {Thread.sleep(2000);} catch (InterruptedException e) {}

      panel.setBackgroundImage("C:\\Users\\David\\Pictures\\Wood.jpg");
      panel.setBackgroundImagePosition(JImagePanel.Position.STRETCH);

      panel.revalidate(); // need to revalidate()
      panel.repaint(); //doesnt work by itself

      try {Thread.sleep(2000);} catch (InterruptedException e) {}

      panel.setBackgroundImage("C:\\Users\\David\\Pictures\\Wood.jpg");
      panel.setBackgroundImagePosition(JImagePanel.Position.FIT);

      frame.setVisible(true); //also works --why?

  }

}
dberm22
  • 3,187
  • 1
  • 31
  • 46
  • 2
    Why do you need to repaint the panel? Did you add a new component or there is another reason? Posting a [SSCCE](http://sscce.org/) will help a lot. – Jakub Zaverka Jan 15 '14 at 12:44
  • I'm sure that revalidate() & repaint() from JFrame to refresh all JComponents tree, sure there is one potential issue repaint() is possible to switch off, programatically for JComponents – mKorbel Jan 15 '14 at 12:46
  • 2
    this is exactly prehistoric AWT issue, be sure that to check 3rd side code if isn't based on AWT containers, then you have to repaint this container (e.g. reDraw() must be implemented in good code) – mKorbel Jan 15 '14 at 12:50
  • By your wording it sounds like you're calling `revalidate()` before adding the panel. It's the layout with the panel you want to validate, so you should call `revalidate(); repaint();` after the removing and adding components is done. – kiheru Jan 15 '14 at 12:50
  • @JakubZaverka My extended version of the JImagePanel(linked in the question) contains public variables to change the size of the background image. When I change these variables, I need to refresh the panel to redraw the image to the appropriate size. When I get home (much) later today, I will see what I can hack out of my class in order to get you some better source code to work with. Perhaps I will just make my project open source tonight and post a link to the project. – dberm22 Jan 15 '14 at 12:50
  • probably something wrong in the code you are not showing .. ? – kleopatra Jan 15 '14 at 12:50
  • @kiheru I am calling revalidate(); repaint(); after I add the components. I didn't mean for that list to sound cronological - just listing separate things I've tried. I will edit the wording to make it clearer. – dberm22 Jan 15 '14 at 12:53
  • 3
    _post a link to the project_ not recommended, we are too lazy to wade through whole projects ;-) Instead, distill the behaviour into a SSCCE as already suggested (though nowadays it's called MCVE) – kleopatra Jan 15 '14 at 12:53
  • @mKorbel can you elaborate? I have never heard of this reDraw() method. – dberm22 Jan 15 '14 at 13:02
  • I updated the question with SSCCE (runnable version of my JImagePanel class). I am unable to replicate the issue I am having withing this class, so obviously it is somewhere else. Hopefully you will find the class useful. As I said in the edit, this still doesn't explain why nothing but frame.setVisible(true) works, but it's a point in the right direction. – dberm22 Jan 17 '14 at 01:25
  • This is not a SSCCE. A SSCCE should show the actual problem so that others have a chance to help you. Your example does not replicate the problem. Also if this code does not replicate the problem that should already be a hint: From http://sscce.org: "If you are trimming a very large amount of code for others to see, you might trim out a part of it early on that you think is not related to the problem, yet the problem is fixed. [...] You might look more closely at the part you cut out, and in doing so, spot the problem." – Grodriguez Jan 17 '14 at 08:08
  • @Grodriguez Yes, I'm aware... But I thought others would appreciate the code and hopefully find it useful in the future. I realize it is mostly unrelated to the question. – dberm22 Jan 17 '14 at 10:41
  • @debrm22 Well, your question says it is a SSCCE. You may want to rephrase. Also note the comment about the process of incrementally trimming the original code where the problem occurs. This should either lead you to an actual SSCCE that reproduces the problem, or (luckily) find the problem yourself in the process. – Grodriguez Jan 17 '14 at 10:56
  • @Grodriguez Thanks for nudge I needed. I completely refactored the question (with an actual SSCCE), so hopefully, you are now pleased. Please make sure to completely re-read the queastion since it was such a major update. I think others will also find that this class/example is useful in the future. Maybe calling setVisible() might become the preferred way of doing it - though probably not. – dberm22 Jan 17 '14 at 12:21
  • still don't fully understand what you are after: there is absolutely no need to call frame.setVisible, so the question on face-value is moot, IMO. Changing image/position simply work as expected (that is without you doing anything but changing those properties, no repaint/revalidate in application code) - provided you [don't use setXXSize](http://stackoverflow.com/a/7229519/203657) and the panel repaints/validates itself on setting those properties. So what _is_ your problem? – kleopatra Jan 17 '14 at 13:10
  • @dberm22 Much better :-) See my answer. – Grodriguez Jan 17 '14 at 13:18
  • @kleopatra There is technically no need to call setVisible() as revalidate(); repaint() works, but I was just puzzled as to why it did. Digging through the source code, it seemed to be that the call was ignored if the panel was already visible, but as Grodriguez pointed out, the method is being overridden in a subclass, which makes a call to validate() - essentially doing the same thing as calling validate() directly. This is why it works, though it is definitely still more straightforward calling validate() directly as opposed to relying on setVisible(). Thanks again for your answer, though. – dberm22 Jan 17 '14 at 16:09

2 Answers2

2

Calling setVisible(true) on a JFrame which is already visible works for you because this ends up calling validate() internally, which in turn revalidates all subcomponents in the frame.

To see why, refer to the implementation of Component.setVisible(boolean b):

public void setVisible(boolean b) {
    show(b);
}

public void show(boolean b) {
    if (b) {
        show();
    } else {
        hide();
    }
}

But the show() method is overriden in Window (of which JFrame is a subclass). So this ends up calling Window.show():

public void show() {
    if (peer == null) {
        addNotify();
    }
    validate();
    [...]

Hope this explains the behaviour you are seeing.

Grodriguez
  • 21,501
  • 10
  • 63
  • 107
2

Assuming you are after a half-way clean implementation of your imagePanel:

  • let the panel do its own revalidation/-paint as need (vs application code)
  • do not call setXXSize, ever, not even internally in the panel

That's change your setters and override the getXXSize. Note that basing the sizing hints on the image size alone is not what you would want to do in real world code, you probably want to take super's hint into account as well (f.i. if the panel has children)

@Override
public Dimension getPreferredSize() {
    if (img != null) {
        return new Dimension(img.getWidth(this), img.getHeight(this));
    }
    return super.getPreferredSize();
}

public void setBackgroundImage(Image img) {
    this.img = img;
    // need to revalidate as our sizing hints might have changed
    revalidate();
    repaint();
}

public void setBackgroundImagePosition(Position pos) {
    this.position = pos;
    repaint();
}

Application code that uses it now simply calls the setters, nothing else:

    JFrame frame = new JFrame("JImagePanel Test");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    // frame.setLayout(new FlowLayout()); // just to see the effect of a pref-respecting layout
    final JImagePanel panel = new JImagePanel();
    frame.add(panel);

    final Image[] images = new Image[]{
            XTestUtils.loadDefaultImage(), XTestUtils.loadDefaultImage("500by500.png"), null};
    Action toggleImage = new AbstractAction("toggle image") {
        int index = 0;
        @Override
        public void actionPerformed(ActionEvent e) {
            panel.setBackgroundImage(images[index]);
            index = (index +1) % images.length;
        }
    };
    Action togglePosition = new AbstractAction("toggle position") {
        int index = 0;
        @Override
        public void actionPerformed(ActionEvent e) {
            panel.setBackgroundImagePosition(Position.values()[index]);
            index = (index +1) % Position.values().length;
        }
    };
    frame.add(new JButton(toggleImage), BorderLayout.NORTH);
    frame.add(new JButton(togglePosition), BorderLayout.SOUTH);
    // size for frame
    //frame.setSize(800, 800);
    frame.setExtendedState(JFrame.MAXIMIZED_BOTH); //sets appropriate
    frame.setVisible(true);
Community
  • 1
  • 1
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • This doesn't answer the question (See Grodriguez), but calling revalidate() internally is definitely an improvement to my class I should (and will) make. And +1 for the extended test case. Thanks for the input. – dberm22 Jan 17 '14 at 16:01