views:

1494

answers:

4

I'm working on a simple little swing component, and I'm tearing out my hair trying to figure out why my paint method isn't working.

The idea behind this component is that it's a small JPanel with a label. The background (behind the label) is supposed to be white, with a colored rectangle on the left-hand side indicating the ratio of two measurements: "actual" and "expected".

If you had a bunch of these components aligned vertically, they'd form a bar chart, composed of horizontal bars.

This kind of thing should be super-simple.

Anyhow, here's the code:

package com.mycompany.view;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;

import javax.swing.JLabel;
import javax.swing.JPanel;

public class BarGraphPanel extends JPanel {

   private static final Color BACKGROUND = Color.WHITE;
   private static final Color FOREGROUND = Color.BLACK;

   private static final Color BORDER_COLOR = new Color(229, 172, 0);
   private static final Color BAR_GRAPH_COLOR = new Color(255, 255, 165);

   private int actual = 0;
   private int expected = 1;

   private JLabel label;

   public BarGraphPanel() {
      super();
      label = new JLabel();
      label.setOpaque(false);
      label.setForeground(FOREGROUND);
      super.add(label);
      super.setOpaque(true);
   }

   public void setActualAndExpected(int actual, int expected) {
      this.actual = actual;
      this.expected = expected;
   }

   @Override
   public void paint(Graphics g) {

      double proportion = (expected == 0) ? 0 : ((double) actual) / expected;
      Rectangle bounds = super.getBounds();

      g.setColor(BACKGROUND);
      g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);

      g.setColor(BAR_GRAPH_COLOR);
      g.fillRect(bounds.x, bounds.y, (int) (bounds.width * proportion), bounds.height);

      g.setColor(BORDER_COLOR);
      g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);

      label.setText(String.format("%s of %s (%.1f%%)", actual, expected, proportion * 100));
      super.paint(g);
      g.dispose();
   }

}

And here's the simple test harness:

package com.mycompany.view;

import java.awt.Dimension;
import java.awt.GridLayout;

import javax.swing.JFrame;
import javax.swing.UIManager;

public class MyFrame extends JFrame {

   public MyFrame() {
      super();
      super.setLayout(new GridLayout(3, 1));
      super.setPreferredSize(new Dimension(300, 200));

      BarGraphPanel a = new BarGraphPanel();
      BarGraphPanel b = new BarGraphPanel();
      BarGraphPanel c = new BarGraphPanel();

      a.setActualAndExpected(75, 100);
      b.setActualAndExpected(85, 200);
      c.setActualAndExpected(20, 300);

      super.add(a);
      super.add(b);
      super.add(c);
   }

   public static void main(String[] args) {
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGUI();
         }
      });
   }

   public static void createAndShowGUI() {

      try {
         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      } catch (Throwable t) { }

      MyFrame frame = new MyFrame();
      frame.pack();
      frame.setVisible(true);
   }

}

The test harness creates a simple frame and then adds three of these controls.

The labels are all rendered correctly, which indicates to me that the paint() method is actually being called, but the rectangles aren't being drawn to the Graphics object.

What am I doing wrong?

And why does Swing programming suck so much?


Here's my final code. Thanks, everyone, for your help!

public void paintComponent(Graphics g) {

   double proportion = (expected == 0) ? 0 : ((double) actual) / expected;

   Rectangle bounds = super.getBounds();

   g.setColor(BACKGROUND);
   g.fillRect(0, 0, bounds.width, bounds.height);

   g.setColor(BAR_GRAPH_COLOR);
   g.fillRect(0, 0, (int) (bounds.width * proportion), bounds.height);

   g.setColor(BORDER_COLOR);
   g.drawRect(0, 0, bounds.width - 1, bounds.height - 1);

   FontMetrics metrics = g.getFontMetrics();
   String label = String.format("%s of %s (%.1f%%)", actual, expected, proportion * 100);
   Rectangle2D textBounds = metrics.getStringBounds(label, g);

   g.setColor(FOREGROUND);
   g.drawString(label, 5, (int) ((bounds.height + textBounds.getHeight()) / 2));
}
A: 

Not sure if this is the source of your problem, but in Swing you're supposed to override paintComponent(Graphics2D) instead...

David Zaslavsky
Yeah, in various versions of this code, I've been using paint() and paintComponent(). It doesn't make any difference.
benjismith
If you override paintComponent(), you don't have to worry about painting children.
Michael Myers
In addition, you don't have to worry about painting the border. paint delegates to paintComponent, paintBorder and paintChildren.
Dan Dyer
+2  A: 

I think you've almost answered your own question in the comments, along with David's answer. Change paint(Graphics g) to paintComponent(Graphics g) and remove the last two lines of the method, and you should be fine.

EDIT: Oddly, this only works for the first bar of the three. More testing in progress...

By the way, you have an off-by-one error in the border-painting code. It should be:

g.setColor(BORDER_COLOR);
g.drawRect(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1);

EDIT2: OK, got it. Your full paintComponent method should be as follows:

@Override
public void paintComponent(Graphics g) {
    double proportion = (expected == 0) ? 0 : ((double) actual) / expected;
    Rectangle bounds = super.getBounds();
    g.setColor(BACKGROUND);
    g.fillRect(0, 0, bounds.width, bounds.height);
    g.setColor(BAR_GRAPH_COLOR);
    g.fillRect(0, 0, (int) (bounds.width * proportion), bounds.height);
    g.setColor(BORDER_COLOR);
    g.drawRect(0, 0, bounds.width-1, bounds.height-1);
    label.setText(String.format("%s of %s (%.1f%%)", actual, expected, proportion * 100));
}

Note that the coordinates given to g.fillRect() and g.drawRect() are relative to the component, so they must start at (0,0).

And no, I can't help you with your last question, though I feel your pain. :)

Michael Myers
+1  A: 

In your JPanel, you called super.setOpaque(true). The JPanel is going to completely fill in the background when you call super.paint() and overwrite your retangles.

basszero
A: 

If anything, I think you should be calling super.paint(g); at the TOP of your method, not at the very bottom. It's possible the superclass is drawing on top of your stuff.

Outlaw Programmer
I've tried calling it at the top and at the bottom and it didn't make any difference. But that could have been because of one of the other errors.
benjismith