The first time I had to make a Swing UI feel “native” to a specialized workflow, I hit a wall: none of the stock widgets behaved quite right. The missing piece wasn’t another library or a custom theme—it was understanding JComponent as the real backbone of Swing. Once I learned how JComponent works, I could shape behaviors, paint cycles, focus handling, and event routing in a way that made the UI match the problem domain instead of forcing the domain into generic widgets. That’s what I want to give you here.
You’ll learn how JComponent sits in the Swing hierarchy, how it differs from AWT, which fields and methods actually matter in day‑to‑day work, and how to build clean, modern custom components. I’ll also cover practical gotchas (like thread rules, performance traps, and painting mistakes), when to use JComponent vs. ready‑made widgets, and how to test and ship custom Swing components in a 2026‑style workflow.
Why JComponent Matters More Than the Widget You Pick
When you create a JButton or a JTextField, you’re not really dealing with a “button class” or a “text class.” You’re working with a subclass of JComponent. That means the real power in Swing doesn’t come from memorizing every widget API. It comes from understanding the component lifecycle, the painting pipeline, and how Swing handles events. JComponent is the base class for all Swing components except top‑level containers like JFrame and JDialog. It’s the contract for lightweight components—rendered by the Java runtime instead of delegated to the native OS widgets.
I like to think of JComponent as a workshop table. Every Swing widget puts its tools on that same table: event handling, painting hooks, focus handling, and layout hints. Once you know where everything is on the table, building new widgets feels like crafting furniture from familiar parts.
Lightweight vs. heavyweight in plain terms
AWT components (like java.awt.Button) are heavyweight. The OS draws them and owns their painting. Swing components are lightweight. The JVM draws them. The big advantage is consistency: the same rendering pipeline across platforms and the ability to deeply customize behavior and visuals. The trade‑off is you must respect Swing’s threading and painting rules because you own more responsibility.
The JComponent Class in Context
JComponent sits between java.awt.Container and your custom Swing classes. It implements Serializable, so you can persist component state. In practice, this means you can snapshot a component tree (with care) and restore it, which helps in some UI builders or stateful desktop apps.
Syntax you should know:
public abstract class JComponent extends Container implements Serializable
Key implications:
- It’s abstract, so you never instantiate JComponent directly.
- It’s a Container, so it can hold child components.
- It’s Serializable, so you can store UI state if you build the right serialization logic.
Nested class: AccessibleJComponent
JComponent includes an accessibility inner class that ties into the Java Accessibility API. If your app is used in enterprise or educational environments, you should care about this. You don’t need to touch it often, but you should know it exists and that Swing has a baseline accessibility layer.
Fields You Actually Use (and Why They Matter)
Most devs never touch JComponent fields directly, and that’s usually correct. But a few matter conceptually because they explain behavior you’ll see in practice:
listenerList: this keeps event listeners registered on the component. When you add listeners, they land here.ui: the ComponentUI delegate. This is where Look and Feel (LAF) rendering happens.TOOLTIPTEXT_KEY: the key Swing uses for tooltip lookups.WHENFOCUSED,WHENINFOCUSEDWINDOW,WHENANCESTOROFFOCUSEDCOMPONENT: constants for keyboard action registration.
I rarely access these directly, but I often rely on them by calling methods that feed into them. For example, when you call registerKeyboardAction, those constants decide when your action fires.
The Core Lifecycle: How a JComponent Lives
A JComponent follows a predictable lifecycle. Understanding it prevents the two classic Swing problems: blank components and flickering paint.
- Instantiation: your constructor sets defaults.
- UI installation: LAF applies its UI delegate.
- Hierarchy attachment:
addNotifyis called when the component is added to a displayable container. - Layout and sizing: preferred/min/max sizes are queried.
- Painting: Swing calls
paintComponent,paintBorder, thenpaintChildren. - Event handling: mouse, key, focus, and ancestor events are dispatched.
The most important rule: never do heavy work in paint. Painting is frequent and on the Event Dispatch Thread (EDT). Treat it like a heartbeat, not a long‑running task.
Building Your First Custom Component (Runnable)
Here’s a real example: a “Progress Thermometer” component for a shipping dashboard. It shows a vertical fill and a target line. This component is easy to understand and demonstrates how to override painting and expose a clean API.
import javax.swing.*;
import java.awt.*;
public class ProgressThermometer extends JComponent {
private int value = 0; // 0..100
private int target = 80; // 0..100
public ProgressThermometer() {
setPreferredSize(new Dimension(60, 200));
setToolTipText("Delivery readiness");
}
public void setValue(int value) {
this.value = clamp(value);
repaint();
}
public void setTarget(int target) {
this.target = clamp(target);
repaint();
}
private int clamp(int v) {
return Math.max(0, Math.min(100, v));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
int padding = 8;
// Background
g2.setColor(new Color(240, 240, 240));
g2.fillRoundRect(padding, padding, w - 2 padding, h - 2 padding, 12, 12);
// Fill based on value
int fillHeight = (int) ((h - 2 padding) (value / 100.0));
int y = h - padding - fillHeight;
g2.setColor(new Color(66, 135, 245));
g2.fillRoundRect(padding, y, w - 2 * padding, fillHeight, 12, 12);
// Target line
int targetY = h - padding - (int) ((h - 2 padding) (target / 100.0));
g2.setColor(new Color(220, 80, 80));
g2.drawLine(padding, targetY, w - padding, targetY);
} finally {
g2.dispose();
}
}
// Simple demo
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Progress Thermometer");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
ProgressThermometer thermometer = new ProgressThermometer();
thermometer.setValue(55);
thermometer.setTarget(75);
frame.add(thermometer);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Why this example matters:
- It uses
paintComponent, notpaint. - It calls
super.paintComponentto clear the background safely. - It keeps state private and triggers repaint on changes.
Events and Input: Listening Without Noise
JComponent provides event handling through listener registration. For custom components, I prefer to expose custom events or use standard listeners where possible.
Example: turning a component into a click‑toggle indicator.
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class ToggleDot extends JComponent {
private boolean on = false;
public ToggleDot() {
setPreferredSize(new Dimension(40, 40));
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
on = !on;
repaint();
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIAS_ON);
g2.setColor(on ? new Color(70, 200, 120) : new Color(200, 90, 90));
int size = Math.min(getWidth(), getHeight()) - 8;
g2.fillOval(4, 4, size, size);
} finally {
g2.dispose();
}
}
}
I keep the event logic minimal and local. If you need external control, expose methods or fire a custom event (PropertyChangeSupport is a great option for that).
Painting Strategy: The One Rule That Saves You
A rule I enforce in every Swing codebase: Paint only state, never compute state. If you calculate expensive geometry during painting, you’ll see jank. Instead, compute geometry on state change and store it, or cache it lazily with invalidation.
A practical approach:
- Change state in setters.
- Recompute geometry in setters or a private
rebuildLayout()method. - Call
repaint()after state changes.
When I build data‑dense dashboards, I often precompute shapes or gradients once per data update. Typical gains are in the 10–30ms range for medium‑complex components, which is the difference between smooth and stutter.
Swing Threading Rules (EDT Reality)
Swing is single‑threaded. All UI updates must run on the Event Dispatch Thread. For custom components, you should:
- Create and show UI in
SwingUtilities.invokeLater. - Do background work in
SwingWorkeror your own executor. - Call
repaint()on the EDT. If you’re unsure, wrap it ininvokeLater.
Here’s a safe pattern for background updates:
SwingWorker worker = new SwingWorker() {
@Override
protected Integer doInBackground() {
// Heavy computation
return computeMetric();
}
@Override
protected void done() {
try {
int value = get();
thermometer.setValue(value);
} catch (Exception ignored) {
}
}
};
worker.execute();
If you break this rule, you’ll see random glitches that look like bugs but are actually race conditions.
When to Use JComponent vs. Existing Swing Widgets
You should not build a custom component unless the existing widget falls short in one of these areas:
- You need custom painting beyond UIManager properties.
- You need non‑standard interaction (gesture, composite hits, inline charts).
- You need a performance‑tuned rendering path.
If you just need a custom look, consider subclassing a standard component and overriding paintComponent. If you need a truly unique widget, start from JComponent.
Here’s a quick decision table I use:
Best Choice
—
Subclass existing Swing component
New JComponent subclass
JPanel
UI delegate
My practical recommendation: start with existing components unless you need full control. You’ll save time on keyboard handling, accessibility, and focus management.
Common Mistakes I Still See (and How to Avoid Them)
- Overriding
paintinstead ofpaintComponent
– You skip double buffering and child painting if you do this. Use paintComponent.
- Not calling
super.paintComponent
– Background artifacts and ghosting show up when your component is reused or resized.
- Heavy computation in paint
– Frame drops. Move it to setters or background tasks.
- Incorrect preferred size
– Layout managers rely on this. Set it in the constructor if you can.
- Forgetting
revalidate()when size changes
– If your preferred size changes after state updates, call revalidate() to refresh layout.
- Thread violations
– Anything that touches UI must be on the EDT.
Custom Keyboard Actions the Right Way
JComponent lets you register keyboard actions without hacking KeyListeners. It’s cleaner and respects focus rules. I use this for component‑level hotkeys.
import javax.swing.*;
import java.awt.event.ActionEvent;
public class HotkeyPanel extends JComponent {
public HotkeyPanel() {
getInputMap(WHENINFOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl S"), "save");
getActionMap().put("save", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Save triggered");
}
});
}
}
The constants like WHENINFOCUSED_WINDOW define how wide the focus net is. It’s an effective way to add shortcut behavior without building a global key dispatcher.
Layout, Size, and the “Why Is My Component Invisible” Problem
If your custom component doesn’t show up, 80% of the time it’s a layout issue. You need to provide at least one of these:
getPreferredSizeoverride, orsetPreferredSizein the constructor
You can also use setMinimumSize and setMaximumSize for layout managers like BoxLayout. For grid‑based layouts (GridBagLayout), preferred size still matters because it’s used as the basis for constraints.
I recommend setting a reasonable preferred size on any custom component unless it’s purely decorative and you’re explicitly controlling layout constraints.
Accessibility and Tooltips
JComponent has built‑in support for tooltips via setToolTipText. This uses TOOLTIPTEXT_KEY and integrates with ToolTipManager. If you ignore it, you’re missing a simple usability win.
Example:
setToolTipText("Shows the current batch quality score");
For accessibility, you can also set accessible name and description:
getAccessibleContext().setAccessibleName("Quality Meter");
getAccessibleContext().setAccessibleDescription("Shows batch quality from 0 to 100");
This is especially important in regulated environments, and it costs almost nothing to add.
Performance Considerations in Real Projects
Swing performance is generally fine on modern hardware, but you can still trip over inefficiencies. Here are the ones I see most:
- Large gradients or alpha blending: These can be expensive at high frame rates. I limit them or cache them.
- Excessive repaints: Calling
repaint()every 10ms might look smooth but drains CPU. A 30–60ms cadence often looks just as good.
JComponent and the Swing “MVC‑ish” Pattern
Swing isn’t strict MVC, but it’s definitely model‑view‑controller flavored. JComponent is the view and part of the controller. Your model can live outside the component, or inside if it’s simple. I prefer a small internal model when the component is self‑contained, and a separate model when the component is part of a larger system.
Here’s how I think about it:
- Model: data and state (values, thresholds, data points)
- View: painting in
paintComponent - Controller: input handling (mouse, keys, focus) and state changes
If you build a custom component that will be reused in multiple screens, separating the model saves you from repainting the world when the data updates.
A tiny model example with property changes
PropertyChangeSupport gives you a light‑weight event system without inventing a custom listener interface. It’s also a standard Java pattern, so it plays nicely with external code.
import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class Meter extends JComponent {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
int old = this.value;
this.value = Math.max(0, Math.min(100, value));
pcs.firePropertyChange("value", old, this.value);
repaint();
}
public void addPropertyChangeListener(PropertyChangeListener l) {
pcs.addPropertyChangeListener(l);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setColor(new Color(50, 150, 200));
int w = (int) (getWidth() * (value / 100.0));
g2.fillRect(0, 0, w, getHeight());
} finally {
g2.dispose();
}
}
}
This pattern makes the component easy to observe, test, and integrate with other UI pieces.
Painting Internals: What Actually Happens During Repaint
It helps to know the order Swing uses for painting:
paintComponent— your custom visualspaintBorder— the border, if anypaintChildren— child components
If you override paint, you are responsible for all three steps. Most of the time, you don’t want that. When you override paintComponent, Swing still handles the rest. That’s the safe default.
Double buffering and why flicker disappears
JComponent is double‑buffered by default. That means painting happens off‑screen, then the buffer is swapped onto the screen. If you bypass this (for example, by messing with setDoubleBuffered(false) or by drawing directly to a heavyweight component), flicker can return. I only disable double buffering when I’m solving a very specific performance problem, and even then I benchmark before and after.
Clipping and dirty regions
When you call repaint(), Swing doesn’t repaint everything. It calculates a “dirty region” and repaints only the damaged area. If you draw outside your bounds, it won’t show. If you’re animating, call repaint(x, y, w, h) to limit the dirty region and keep performance stable.
Repaint vs. Revalidate: Two Signals, Two Purposes
I treat these as two different tools:
- repaint(): the pixels changed
- revalidate(): the layout changed
If your preferred size changes because of new content, call both:
public void setLabel(String label) {
this.label = label;
revalidate();
repaint();
}
Forget revalidate() and your layout manager won’t run again; your component may be clipped or invisible. Forget repaint() and it will have the new size but the old visuals.
Borders, Insets, and the UI Delegate
Borders are an underused part of JComponent. They let you add padding, outlines, and drop shadows without baking those into your painting logic. This keeps your component reusable because the visuals aren’t hardcoded.
setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
Insets from borders should be respected in your painting logic. A common pattern:
Insets insets = getInsets();
int x = insets.left;
int y = insets.top;
int w = getWidth() - insets.left - insets.right;
int h = getHeight() - insets.top - insets.bottom;
The UI delegate (ComponentUI) is where Look and Feel code lives. For custom components, you often skip a UI delegate entirely. But if you want your component to adapt to different LAFs, a UI delegate is the scalable approach. I only create a custom UI delegate when the component will be part of a larger design system.
Focus Handling That Doesn’t Surprise Users
Focus is one of those Swing details that can make or break usability. JComponent gives you control but doesn’t force best practices. I follow three rules:
- Make focus visible: draw a focus ring or highlight.
- Only focus if it makes sense: call
setFocusable(true)only for interactive components. - Respect focus traversal: don’t consume Tab or Shift+Tab unless you have a very good reason.
Here’s a minimal focus ring example:
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
if (isFocusOwner()) {
g2.setColor(new Color(80, 140, 255));
g2.drawRoundRect(2, 2, getWidth() - 5, getHeight() - 5, 8, 8);
}
// your normal painting
} finally {
g2.dispose();
}
}
And don’t forget to enable focus:
setFocusable(true);
Input Maps: Cleaner than KeyListeners
KeyListeners can be brittle. Input and Action Maps let you attach actions to keystrokes in a way that respects Swing’s focus system. I use them to keep keyboard logic next to the component, without wiring up global listeners.
A slightly more structured example:
public class Dial extends JComponent {
private int value;
public Dial() {
setFocusable(true);
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke("LEFT"), "dec");
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke("RIGHT"), "inc");
getActionMap().put("dec", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValue(value - 1);
}
});
getActionMap().put("inc", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValue(value + 1);
}
});
}
public void setValue(int value) {
this.value = Math.max(0, Math.min(100, value));
repaint();
}
}
It’s readable, testable, and doesn’t interfere with other parts of the UI.
Drag and Drop (DnD) with Custom Components
Swing DnD is old but still works. The easiest modern path is to use TransferHandler. It can be attached to any JComponent.
public class DraggableTag extends JComponent {
public DraggableTag() {
setTransferHandler(new TransferHandler("text"));
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
JComponent c = (JComponent) e.getSource();
TransferHandler handler = c.getTransferHandler();
handler.exportAsDrag(c, e, TransferHandler.COPY);
}
});
}
@Override
public String getToolTipText() {
return "Drag me";
}
}
This is simple but powerful for workflow‑driven apps like planners, scheduling boards, or visual editors.
High‑DPI, Scaling, and 2026 Reality
Modern monitors are dense. If you hardcode pixel sizes, your component will look tiny or fuzzy. I handle this in three ways:
- Avoid fixed fonts: use
getFont()and let LAF scale it. - Scale based on font metrics: compute sizes relative to text height.
- Use vector shapes: draw shapes that scale with
Graphics2D.
Example of font‑driven sizing:
FontMetrics fm = getFontMetrics(getFont());
int padding = fm.getAscent();
int h = fm.getHeight() * 3;
This adapts to accessibility font sizes and OS scaling settings without extra work.
Animation Without Burning the CPU
Swing isn’t a game engine, but it can animate well for subtle effects. The trick is to use a Swing Timer (which runs on the EDT) and keep the frame rate modest.
Timer timer = new Timer(33, e -> {
// update animation state
repaint();
});
timer.start();
I rarely animate faster than 30 FPS in Swing. It’s enough for UI transitions and keeps the CPU calm.
Advanced Example: A Resizable Sparkline Component
This example ties together sizing, painting, and performance. It’s a tiny chart component for dashboards.
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class Sparkline extends JComponent {
private List points = List.of();
public Sparkline() {
setPreferredSize(new Dimension(120, 40));
}
public void setPoints(List points) {
this.points = points;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (points == null || points.isEmpty()) return;
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
int max = points.stream().mapToInt(Integer::intValue).max().orElse(1);
int min = points.stream().mapToInt(Integer::intValue).min().orElse(0);
int range = Math.max(1, max - min);
int n = points.size();
int prevX = 0;
int prevY = h - (points.get(0) - min) * h / range;
g2.setColor(new Color(40, 120, 200));
for (int i = 1; i < n; i++) {
int x = i * (w - 1) / (n - 1);
int y = h - (points.get(i) - min) * h / range;
g2.drawLine(prevX, prevY, x, y);
prevX = x;
prevY = y;
}
} finally {
g2.dispose();
}
}
}
It doesn’t overcomplicate things, yet it adapts to size changes and handles variable data sets.
When NOT to Use JComponent
Custom components are not always the right choice. I avoid JComponent when:
- A standard component already handles keyboard, accessibility, and UI state well.
- The UI is form‑based and mostly standard (labels, fields, buttons).
- I’m fighting the LAF instead of working with it.
In those cases, I subclass a standard component or use UIManager customization. JComponent is powerful, but it’s not free. You pay in maintenance and testing.
Debugging and Testing Custom Components
Swing UI testing can be tricky, but I still test logic and rendering boundaries. My approach:
- Unit test the model/state: If you separate model logic, test it like any other class.
- Smoke test painting: Render to a BufferedImage and verify it doesn’t throw errors.
- Manual interaction tests: Small demo frame for each component.
A quick paint smoke test:
BufferedImage img = new BufferedImage(200, 60, BufferedImage.TYPEINTARGB);
Graphics2D g2 = img.createGraphics();
try {
Sparkline s = new Sparkline();
s.setSize(200, 60);
s.paint(g2);
} finally {
g2.dispose();
}
It won’t tell you if the pixels are perfect, but it will catch exceptions and layout mistakes early.
Serialization: Useful but Easy to Misuse
JComponent is Serializable, but don’t assume it’s safe to serialize a full UI tree in production. It can be fragile, version‑dependent, and can drag in more state than you want. I only serialize small component states, not entire windows.
If you do serialize, mark anything sensitive or transient appropriately:
private transient SomeCache cache;
And consider a manual state export format instead of raw Java serialization.
Practical Workflow in 2026: How I Ship Custom Swing Components
Swing isn’t dead—it’s just mature. A modern workflow for custom components looks like this:
- Component in isolation: I develop with a tiny demo frame, not inside the full app.
- Design tokens: Colors, spacing, and fonts from a small constants class.
- CI tests: At least unit tests for model logic and paint smoke tests.
- Accessibility pass: Tooltips, accessible names, keyboard navigation.
- Performance sanity check: Avoid repaint loops and large alpha blending.
This keeps legacy desktop apps reliable while still letting you ship modern, polished experiences.
A Quick Checklist I Use Before Shipping
I keep a short pre‑ship list on my desk:
- Does the component paint correctly with different sizes?
- Does it behave when used in a JScrollPane?
- Does it respond correctly to focus and keyboard?
- Does it look okay in both light and dark LAFs?
- Does it repaint too often when idle?
- Do I have a small demo frame for future debugging?
If the answer is “yes” across the board, I ship it.
Expansion Strategy
Add new sections or deepen existing ones with:
- Deeper code examples: More complete, real-world implementations
- Edge cases: What breaks and how to handle it
- Practical scenarios: When to use vs when NOT to use
- Performance considerations: Before/after comparisons (use ranges, not exact numbers)
- Common pitfalls: Mistakes developers make and how to avoid them
- Alternative approaches: Different ways to solve the same problem
If Relevant to Topic
- Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
- Comparison tables for Traditional vs Modern approaches
- Production considerations: deployment, monitoring, scaling
A Practical Edge Case: Component in a Scroll Pane
Custom components often end up in a scroll pane. If your preferred size changes based on data, you need to keep the scroll bars accurate. The fix is simple: call revalidate() when size changes, and make sure getPreferredSize() returns the correct value. If you don’t, scrolling will feel broken or the component may not appear at all.
Another Edge Case: Rendering with Transparency
If you use transparency (alpha values), be aware that it can stack up in complex UIs. A single translucent overlay is fine, but multiple layered alpha composites can slow down repaint. I keep translucent effects small and localized, and I avoid animating them unless necessary.
A Final Example: A Labeled Range Bar
This component demonstrates text alignment, preferred size, and both repaint and revalidate on state changes.
import javax.swing.*;
import java.awt.*;
public class RangeBar extends JComponent {
private int value = 50;
private String label = "Range";
public RangeBar() {
setPreferredSize(new Dimension(240, 36));
}
public void setValue(int value) {
this.value = Math.max(0, Math.min(100, value));
repaint();
}
public void setLabel(String label) {
this.label = label == null ? "" : label;
revalidate();
repaint();
}
@Override
public Dimension getPreferredSize() {
FontMetrics fm = getFontMetrics(getFont());
int textW = fm.stringWidth(label) + 16;
int h = Math.max(24, fm.getHeight() + 8);
return new Dimension(200 + textW, h);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
int barW = (int) (w * (value / 100.0));
g2.setColor(new Color(230, 230, 230));
g2.fillRoundRect(2, 2, w - 4, h - 4, 8, 8);
g2.setColor(new Color(90, 170, 240));
g2.fillRoundRect(2, 2, Math.max(6, barW), h - 4, 8, 8);
g2.setColor(new Color(30, 30, 30));
FontMetrics fm = g2.getFontMetrics();
int textY = (h + fm.getAscent() - fm.getDescent()) / 2;
g2.drawString(label, 8, textY);
} finally {
g2.dispose();
}
}
}
It’s not fancy, but it’s production‑ready and demonstrates the core discipline: keep state separate, paint only what’s needed, and respect layout.
Closing: Why JComponent Is Still Worth Learning
Swing might not be the newest UI toolkit, but JComponent is still one of the best ways to understand how component systems work. When you master it, you can build interfaces that are precise, reliable, and tuned to your domain. I’ve used JComponent to build scheduling boards, industrial dashboards, internal workflow tools, and data‑dense editors. The patterns are the same: clear state, disciplined painting, and respect for the EDT.
If you take one thing away, let it be this: JComponent is less about drawing and more about owning your UI’s behavior. Once you understand that, Swing stops being a set of widgets and becomes a toolkit you can actually shape.


