Java JComponent: The Practical Backbone of Swing UI

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: addNotify is called when the component is added to a displayable container.
  • Layout and sizing: preferred/min/max sizes are queried.
  • Painting: Swing calls paintComponent, paintBorder, then paintChildren.
  • 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, not paint.
  • It calls super.paintComponent to 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 SwingWorker or your own executor.
  • Call repaint() on the EDT. If you’re unsure, wrap it in invokeLater.

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:

Scenario

Best Choice

Why —

— Minor visual tweaks

Subclass existing Swing component

Leverages built‑in focus, accessibility, and behavior Custom visuals + behavior

New JComponent subclass

Full control with less inherited complexity Pure layout container

JPanel

Lighter for layout only Custom Look and Feel

UI delegate

Centralized LAF management

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 paint instead of paintComponent

– 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:

  • getPreferredSize override, or
  • setPreferredSize in 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 visuals
  • paintBorder — the border, if any
  • paintChildren — 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.

Scroll to Top