Implementing a JButton with Rounded Corners

Navigation


Saturday, January 19, 2008

Introduction

I put all the code up front so you can meet your deadline. If you'd like to see how I came up with the final implementation, you can read on to explore some JDK source with me. As a result, we'll hopefully gain a keener understanding of how Swing performs paints, which we can use to our advantage to develop more effecient GUIs.

The Code

The following code differs very little from the final class I implemented for my current project, which you can launch from your browser since it is a Web Start application located at http://tranche.proteomecommons.org. If you'd like to see the result, you can launch the tool and click on "Download by Hash String". The rounded buttons appear at the bottom of every wizard screen.

This implementation takes very little work, and all of the essential work takes place in the overriden paint method.

public class GenericRoundedButton extends JButton { public GenericRoundedButton(String text) { super(text); } public GenericRoundedButton() { super(); } public void paint(Graphics g) { // Set background same as parent. setBackground(getParent().getBackground()); setBorder(Styles.BORDER_NONE); // I don't need this -- calls to above methods will // invoke repaint as needed. // super.paint(g); // Take advantage of Graphics2D to position string Graphics2D g2d = (Graphics2D)g; // Make it grey #DDDDDD, and make it round with // 1px black border. // Use an HTML color guide to find a desired color. // Last color is alpha, with max 0xFF to make // completely opaque. g2d.setColor(new Color(0xDD, 0xDD, 0xDD, 0xFF)); // Draw rectangle with rounded corners on top of // button g2d.fillRoundRect(0,0,getWidth(),getHeight(),18,18); // I'm just drawing a border g2d.setColor(Color.BLACK); g2d.drawRoundRect(0,0,getWidth()-1, getHeight()-1,18,18); // Finding size of text so can position in center. FontRenderContext frc = new FontRenderContext(null, false, false); Rectangle2D r = getFont().getStringBounds(getText(), frc); float xMargin = (float)(getWidth()-r.getWidth())/2; float yMargin = (float)(getHeight()-getFont().getSize())/2; // Draw the text in the center g2d.drawString(getText(), xMargin, (float)getFont().getSize() + yMargin); } }

Disclaimer

My numbers below were gathered using grep and wc, meaning they may be off by a bit. I like accuracy, but I'm going for the forest. Feel free to leave a comment if you feel that this makes me a jerk.

Also I accept no responsibility for the consequences of your use of my code and you are a developer so lalala: use at your own risk.

Explanation

I am provided the Graphics object, which I am going to manipulate to create my custom button with rounded corners. After making the few changes to the Graphics object (Graphics and Graphics2D are just the interface to the implementing class), the rounded button is rendered on top the button image rendered by any parent classes.

This is a useful because it allows our GUIs to get outside of the standard box and implement our own graphics while using the underlying event listeners for the existing widget classes! If you wanted to develop your own widget from scratch, you'd have a lot of functionality to implement that you could otherwise easily inherit from existing classes.

So by overriding paint, you get your cake and get to eat it, too.

Note that setBorder is static, so it might as well be invoked in a constructor. (I generally make a method called init and invoke it from my constructors.) For the sake of simplicity, I put all my work together in paint for illustrative purposes.

However, setBackground plays a dynamic role: it ensures that whenever my button is painted (initially and when repainted), it will have the same background color as it's parent container. I'm hoping the classes that implement getBackground and setBackground are efficient! (They are, as we'll see.)

In fact, if we stripped out all of the graphics changes in the method following after our changes to the border and background, we'd get a free transparent button.

(This is nice if you want to create HTML-like links. You can set a border Border on the bottom for a familiar underlined link, which is a particularly useful visual cue when you want to provide a button that launches a web resource in a browser from your GUI. IMHO.)

Theory is great, but in the name of science let's demonstrate something! Hypothesis: if super.paint(g) is invoked at the end of my paint method, all of my custom graphics will be for naught! I tested this by placing a call to super.paint(g) at the very end of the paint method, recompiled and ran my tool. The outcome: I lost my rectangle with rounded corners, and the only this that appears is the text label!

Since I set the background and border in my paint method, their new values are used in super.paint, meaning that my paint method still does something. Just because super.paint is invoked last doesn't mean it completely negates my custom paint method! However, I lose my Graphics tweaks since JComponent.paint, which is inherited by JButton, tweaks the Graphics object as well, undoing my work.

Digging thru the JDK source

My first thought is that I should call super.paint(g) within the method. The reason is that I set the background and border, and I want to make sure that the changes are repainted to the screen.

Before I do that, I'd like to see what's going on. Since I'm making this a widget intended for re-use in my project, so I'm interested in making this efficient. This is also a great opportunity to learn a little more about Swing.

Because I'm curious, I'm going to forfeit a tiny bit of my evening to take a peek at the source code. Since I develop using Java 5, I went to http://www.sun.com/software/communitysource/j2se/java2/download.xml to download a zip archive of the JDK 5 source.

The logical place to start is with JButton. Perchance you want to follow along, the class source is located at:

jdk-1_5_0-src-scsl/j2se/src/share/classes/javax/swing/JButton.java

Using some simple heuristics*, I estimate that the class is an anorexic 308 lines, and 190 lines are documentation, leaving 118 lines of object-oriented code. It inherits a lot of functionality from its parent class.

At the moment, I'm looking for some interesting Graphics-related code to help me understand the process of repainting. Hopefully, I'll gain some insight that will serve me with the design of my rounded button as well as with my general Swing development.

A quick search for Color or Graphic in the JButton class comes up empty, so I'm going to pull up its parent abstract class, AbstractButton. Like any other Java developer, I'm all too familiar with JavaDocs, but I feel I have a pragmatic interest in implementation details, particularly with repaint invocations.

AbstractButton.java, which is in the same package (hence directory), weighs in at 2902 lines, on which 1298 are documentation, leaving 1604 lines of logic, which is more than an entire order of magnitude larger than JButton.

Note this class builds the button widget for not only JButton, but also for JMenuItem and JToggleButton, according to JavaDoc. AbstractButton depends on an inherited repaint method to actually redraw changes to an implementing widget. For example, search for setText. Note that if the icon is changed, the method calls revalidate and repaint.

Moving on, I note that AbstractButton extends the abstract class JComponent, also in the same package, so I'm going to scope that class out next.

My infatuation with numbers continues! JComponent, the parent of 32 classes in Java 5 (two of which are inner classes), is 5288 lines young (nearly twice as heavy as AbstractButton), with 2386 lines of logic. Familiar methods like setFont, setBackground and setBorder are implemented in this class! The method setBackground is pretty simple: it sets the background color in the parent, and it then invokes repaint if the background color was changed. Efficient.

I'm going to shift my attention to repaint (since it invoked whenever widgets are updated). There are two repaint methods implemented in JComponent, one which takes a rectangle and the other takes the x and y coordinates along with the dimensions.

The most interesting artifact in our excursion is RepaintManager.addDirtyRegion, which manages changes to widgets. It maintains a queue of changes to regions, called dirty regions, and it double buffers the changes, meaning that changes are written to an offscreen buffer which is eventually written to screen. (This is done to minimize visual annoyances during frequent redraws.) Like any good queue, then request is processed when the resources are available. All changes will occur on the event dispatch thread.

Note that repaint() without any parameters is inherited from Component, which is located in AWT and invoked fairly frequently if a lot of changes are committed to your widget. It allows places repaint requeusts in a queue of requests to process on the event dispatch thread.

Take-home points

If you're at work and simply need to generate a button with rounded corners NOW, you probably didn't get this far. However, our exploration of very little source afforded us a great deal of implementation details that demystify Swing events greatly.

The biggest take-home point is that your paint method will be invoked on the precious event dispatch thread, where all GUI changes and events are queued. This is a coveted resource that should not be squandered. Keep computationally-thick or IO-bound actions off this thread and out of your paint method. If you need to update your GUI based on some calculation, file or network access, do it elsewhere and queue the output as a thread to be invoked by the event dispatch thread. Here's how you can quickly and efficiently respect your user's runtime environment:

{ // ... lot's of work. Results will appear in a GUI. Thread t = new Thread("I'm going to be placed on dispatch thread!") { public void run() { // GUI updates. Do the work before this thread. } }; // Queue up the requeust with the other requests for the dispatch thread SwingUtilities.invokeLater(t); }

Another important point is that paint is eventually invoked by a repaint, and that the Swing and AWT classes will handle this fairly intelligently. If you use certain setters on Swing or AWT widgets, the classes will invoke repaint when needed.

If you are creating custom widgets and override certain setters (e.g., setBackground), you will need to either call repaint or invoke super.setBackground. When reasonable, I'd go with the latter since the implementers of the standard library widgets are all so smart that their code intelligently invokes repaint when needed.

Lastly, note that getters such as getWidth, getBackground, etc., are called very very frequently from the dispatch thread, most often without your consent. Keep your getters simple so that your GUIs remain responsive!

* For all line counts, I used the following two commands:

cat ClassName.java | wc -l cat ClassName.java | grep "*" | wc -l

The former of which I used to get the full line count and the latter of which I used to determine the line count for documentation. I simply subtracted the documentation to get the logic line count.

Of course, the asterisk can be used for the multiplication operator or appear within a string. Since I estimated that there aren't that many of either potential uses in the classes I examined, I went with the number the shell produced.


Thursday, January 24, 2008

I had previously written an entry called Implementing a JButton with Rounded Corners. I got some email feedback, as well as discovered issues on non-Macs.

The updated class code

public class GenericRoundedButton extends JButton { public GenericRoundedButton(String title) { super(title); init(); } public GenericRoundedButton() { super(); init(); } private void init() { setBackground(Styles.COLOR_BACKGROUND_LIGHT); setBorder(Styles.BORDER_NONE); setFocusable(false); } public void paint(Graphics g) { // Don't need to set these to get transparent button -- // we'll simply not draw it! // setBackground(getParent().getBackground()); // setBorder(Styles.BORDER_NONE); // Don't draw the button or border this.setContentAreaFilled(false); this.setBorderPainted(false); Graphics2D g2d = (Graphics2D)g; // Anti-aliased lines and text g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // This is needed on non-Mac so text // is repainted correctly! super.paint(g); // Make it grey #DDDDDD, and make it round with 1px // black border g2d.setColor(new Color(0xDD,0xDD,0xDD,0xFF)); g2d.fillRoundRect(0,0,getWidth(),getHeight(),18,18); g2d.setColor(Color.BLACK); g2d.drawRoundRect(0,0,getWidth()-1,getHeight()-1,18,18); // Determine the label size so can center it FontRenderContext frc = new FontRenderContext(null, false, false); Rectangle2D r = getFont().getStringBounds(getText(), frc); float xMargin = (float)(getWidth()-r.getWidth())/2; float yMargin = (float)(getHeight()-getFont().getSize())/2; // Draw the text in the center g2d.drawString(getText(),xMargin, (float)getFont().getSize()+yMargin); } }

There are more functionality in my class that I am omitting to simplify the logic, but not much! I added some quick code to pad the text with spaces so that I can sync multiple buttons to get the same width.

This code is more reliable in different runtime and OS environments, though this is anything but thoroughly tested across major JREs.

Text Issues on Windows, Linux

During a weekly developer meeting, after committing my code to the developer's repository, the rounded buttons' text appeared too small and misaligned during a related demo by a co-developer using Windows XP. I later built and ran the GUI on Ubuntu and replicated the issue.

The problem was solved by uncommenting the super.paint(g) method call I was too eager to remove. On both the Windows and Ubuntu boxes, the text was initially rendered correctly; however, when any element of the GUI changed so that repaint() was invoked on my instance of GenericRoundedButton, the text shrunk but kept it's position, meaning that it lost it's center alignment!

I'm not sure why this happens on the Win/Linux HotSpot JVMs (1.5) and not on the Mac JVM, and any feedback would be great! (I don't have the exact JRE versions/builds on hand.)

Where's a screenshot?

A comment I received stated that it was great to put the code up front, but where's the screenshot? That's a very good point, especially for a post invoking Swing GUI development.

This is an image with a single button for the Tranche download tool. On the future screens, two buttons appear so that a user can navigate forwards and backwards in the download process, much like a standard wizard guiding a user through any process. Our upload tool uses the same generic wizard architecture.

Anti-aliasing

The feedback also suggested that I include the code for anti-aliasing, which is included in the class code block at the top. (Here's a brief article about anti-aliasing, including illustrations.)

Off topic exploratory text

Another point I received in the feedback is that the text starts out strong with the code, but goes off topic too quickly and is difficult to follow.

Looking back at it, I would probably not bother with that in someone else's blog, so I'll avoid it in the future!