A arquitetura de uma interface gráfica de usuário (GUI) robusta frequentemente se inspira em mecanismos do mundo real. Considere uma tecla de teclado físico: ela possui um mecanismo interno que define sua função (o ato de ser pressionada) e uma superfície externa que define sua representação visual (a letra ou símbolo impresso). Essa separação de responsabilidades permite a reutilização do mesmo mecanismo interno para diferentes teclas, alterando apenas a aparência externa.
No desenvolvimento de software, esse conceito é formalizado pelo padrão de projeto Model-View-Controller (MVC). Embora o MVC seja tradicionalmente aplicado ao design de interfaces completas, a Java Foundation Classes (JFC), especificamente o toolkit Swing, adapta esse padrão para a construção de componentes individuais, como JTable, JTree e JButton. Essa abordagem garante que o modelo, a visualização e o controlador de um componente possam ser modificados de forma indepandente, conferindo extrema flexibilidade ao toolkit.
O Padrão MVC no Contexto do Swing
No Swing, o padrão MVC é dividido da seguinte forma:
- Model: Gerencia o estado interno e o comportamento de baixo nível do componente. Ele não tem conhecimento direto sobre a interface visual. Quando seu estado é alterado, notifica os observadores registrados.
- View e Controller: No Swing, a View (representação visual) e o Controller (gerenciamento de interações do usuário) são frequentemente combinados em uma única entidade conhecida como UI Delgeate (Delegado de Interface). Essa fusão simplifica a arquitetura, resultando no padrão Model-Delegate.
Análise Profunda do Componente Button
Para ilustrar essa arquitetura, analisaremos a implementação de um componente de botão. O componente em si atua como uma "cola" que conecta o Model ao UI Delegate, permitindo que desenvolvedores substituam qualquer uma dessas partes em tempo de execução.
A Classe do Componente (A "Cola")
O componente visual encapsula a integração entre o modelo e a interface. O código abaixo demonstra como o modelo e o delegado de UI são acoplados e desacoplados dinamicamente:
public void attachModel(ButtonModel newModel) {
if (currentModel != null) {
currentModel.removeChangeListener(stateListener);
currentModel.removeActionListener(actionListener);
stateListener = null;
actionListener = null;
}
currentModel = newModel;
if (currentModel != null) {
stateListener = new StateHandler();
actionListener = new ActionHandler();
currentModel.addChangeListener(stateListener);
currentModel.addActionListener(actionListener);
}
refreshComponent();
}
public void attachUI(ButtonUI newUI) {
if (currentUI != null) {
currentUI.uninstall(this);
}
currentUI = newUI;
if (currentUI != null) {
currentUI.install(this);
}
refreshComponent();
}
private void refreshComponent() {
revalidate();
repaint();
}
O Model do Botão
O ButtonModel é responsável por manter os estados lógicos do componente, como pressed (pressionado), armed (armado/pronto para disparar) e selected (selecionado). Ele também gerencia a notificação de eventos para os listeners interessados.
private boolean isPressedState = false;
public boolean checkPressed() {
return isPressedState;
}
public void updatePressed(boolean state) {
if (this.isPressedState != state) {
this.isPressedState = state;
notifyStateChange(new ChangeEvent(this));
}
}
private final List<ChangeListener> stateListeners = new CopyOnWriteArrayList<>();
public void registerChangeListener(ChangeListener listener) {
stateListeners.add(listener);
}
public void unregisterChangeListener(ChangeListener listener) {
stateListeners.remove(listener);
}
protected void notifyStateChange(ChangeEvent event) {
for (ChangeListener listener : stateListeners) {
listener.stateChanged(event);
}
}
O UI Delegate (View e Controller)
O ButtonUI lida com a renderização gráfica e a tradução de eventos de baixo nível (como cliques do mouse) para eventos semânticos do modelo. Por padrão, ele pode apenas desenhar um retângulo, mas subclasses específicas (como Metal, Windows, ou Motif) sobrescrevem esses métodos para fornecer aparências nativas.
public void render(CustomButton btn, Graphics g) {
Dimension size = btn.getBounds().getSize();
g.setColor(btn.getBackground());
g.fillRect(0, 0, size.width, size.height);
}
private final UIEventHandler eventHandler = new UIEventHandler();
public void install(CustomButton btn) {
btn.addMouseListener(eventHandler);
btn.addMouseMotionListener(eventHandler);
btn.addChangeListener(eventHandler);
}
public void uninstall(CustomButton btn) {
btn.removeMouseListener(eventHandler);
btn.removeMouseMotionListener(eventHandler);
btn.removeChangeListener(eventHandler);
}
O Manipulador de Eventos do UI
Para manter o UI Delegate stateless (sem estado), os listeners de eventos são implementados de forma que múltiplas instâncias do botão possam compartilhar o mesmo manipulador. Este componente intercepta as interações do mouse e atualiza o modelo correspondentemente.
public void mouseDragged(MouseEvent e) {
CustomButton btn = (CustomButton) e.getSource();
ButtonModel model = btn.getModel();
if (model.checkPressed()) {
boolean isInside = btn.getUI().contains(btn, e.getPoint());
model.updateArmed(isInside);
}
}
public void mousePressed(MouseEvent e) {
CustomButton btn = (CustomButton) e.getSource();
ButtonModel model = btn.getModel();
model.updatePressed(true);
model.updateArmed(true);
}
public void mouseReleased(MouseEvent e) {
CustomButton btn = (CustomButton) e.getSource();
ButtonModel model = btn.getModel();
model.updatePressed(false);
model.updateArmed(false);
}
public void stateChanged(ChangeEvent e) {
CustomButton btn = (CustomButton) e.getSource();
btn.repaint();
}