Search code examples
javaswinguser-interfaceevent-handlingobserver-pattern

Create and integrate custom events in a Java Swing App


I've been creating a Java Swing app which tracks the position of dummy cars and plots them on a map. I'm afraid that the app is getting bigger and I need a sort of split between UI and app logic.

Is it possible to implement an event/listener architecture with different types of events to update the UI when necessary? How?

At present, I manage to update car position in this way:

class car {
    setPosition(int x, int y) {
        _x = x; _y = y;
        carImage.setPosition(x,y);
    }
    changeImage(enum typeOfImage) {
        carImage.changeImage(typeOfImage);
    }
}

As you can see I need a UI object inside my logic app, and this breaks the split between UI and logic. In addition, I would like to change the logo of the car whenever the car's state changes and hide or show the image at my convenience. So I think that an observer pattern should be suitable in this scenario.

Thank you


Solution

  • One way to add "events" to any code is to use a listener framework, and a clean one to use is the java.beans Property Change Listener framework. You would add to the model classes a support object and methods that allow addition or removal of property change listener objects. This will allow outside code to be notified of what changes occur and when. For example:

    import java.beans.PropertyChangeListener;
    import javax.swing.event.SwingPropertyChangeSupport;
    
    public class Car {
        public static final String POSITION = "position";
        public static final String IMAGE = "image";
        private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
        private CarPosition carPosition;
        private TypeOfImage carImage;
    
        public void setPosition(int x, int y) {
            CarPosition oldPosition = carPosition;
            this.carPosition = new CarPosition(x, y);
            pcSupport.firePropertyChange(POSITION, oldPosition, carPosition);
        }
    
        public void setImage(TypeOfImage typeOfImage) {
            TypeOfImage oldImage = typeOfImage;
            this.carImage = typeOfImage;
            pcSupport.firePropertyChange(IMAGE, oldImage, typeOfImage);
        }
    
        public int getX() {
            return carPosition.x();
        }
    
        public int getY() {
            return carPosition.y();
        }
    
        public CarPosition getCarPosition() {
            return carPosition;
        }
    
        public TypeOfImage getImage() {
            return carImage;
        }
    
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            pcSupport.addPropertyChangeListener(listener);
        }
    
        public void removePropertyChangeListener(PropertyChangeListener listener) {
            pcSupport.removePropertyChangeListener(listener);
        }
    }
    

    Assuming a simple enum

    enum TypeOfImage {
        RED_CAR, BLUE_CAR, GREEN_CAR
    }
    

    Best to have the listener return a single object, and so if changing position in one method, have a record to allow this to be returned

    record CarPosition(int x, int y) {
    }
    

    Then this can be used in the view as follows

    class CarView extends JPanel {
        private Car car;
    
        public CarView(Car car) {
            this.car = car;
            car.addPropertyChangeListener(e -> {
                if (e.getPropertyName().equals(Car.POSITION)) {
                    repaint();
                }
            });
            car.addPropertyChangeListener(e -> {
                if (e.getPropertyName().equals(Car.IMAGE)) {
                    repaint();
                }
            });
        }
    
        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(car.getImage().getSprite(), car.getX(), car.getY(), null);
        }
    }