¿Cómo puedo cambiar el color de resaltado de un JComboBox enfocado?

Permítanme primero explicar lo que estoy buscando lograr. Estoy creando un formulario de entrada de datos en Swing, compuesto por varios JComboBoxes y JTextFields. Una rutina de validación itera sobre esos componentes y determina si los valores especificados para cada control son ‘válidos’ (los detalles de la validación son irrelevantes para los propósitos de este ejemplo).

Cuando la rutina identifica que un componente contiene un valor no válido, quiero cambiar el color de fondo de ese campo, y también el color de primer plano / texto de ese campo, para aclararle al usuario que hay un problema con ese campo.

Cuando un campo se considera ‘válido’, quiero establecer que el fondo del control sea blanco y que el primer plano / texto sea negro.

Todo bastante sencillo hasta ahora, y todo alcanzable dentro del código de demostración adjunto a continuación.

Cuando un cuadro combinado contiene un valor válido y está enfocado, el fondo del editor dentro del combo está configurado en un color azulado, con lo cual estoy completamente satisfecho.

Sin embargo, lo que bash lograr es cambiar el color que se usa para resaltar un cuadro combinado enfocado cuando ese cuadro combinado contiene un valor no válido. A pesar de haber cambiado el color de fondo del cuadro combinado para que sea de color rosado, si el control está enfocado, aún usa el color azul para indicar que está enfocado.

Ejemplo de campo no válido enfocado: http://postimg.org/image/ne9xgjch3/

Aunque aprecio que este es un comportamiento perfectamente normal, lo que me gustaría hacer es cambiar el color que se utiliza para resaltar uno de los campos “no válidos” a un tono más oscuro del color que tendría un control no enfocado e inválido. – para que el usuario aún pueda ver qué control está enfocado, y sigue siendo rosa en todas partes. Aprecio que esto pueda parecer mezquino, pero mi usuario final insiste en que todo el campo permanece rosado (o más bien, con un tono de rosa diferente) cuando se enfoca. Esto es lo que mi eutopia, un campo enfocado e ‘inválido’, se vería así:

http://postimg.org/image/9793bqcfj/

Intenté extender las clases DefaultListCellRenderer y BasicComboBoxEditor, y establecerlas en el cuadro combinado como renderizador y editor, respectivamente. Tenía la impresión de que el Editor estaría donde necesitaba enfocar mis atenciones, así que dentro del método getEditorComponent de la clase, devolvería una etiqueta con un fondo y un primer plano apropiados; sin embargo, desde ese método, no tengo forma de saber si el control tiene o no foco, por lo que no tengo forma de determinar cómo debo formatear la etiqueta devuelta. Además, tan pronto como comencé a configurar un Editor contra el cuadro combinado, parecía perder la capacidad de enfocar el control en absoluto, aunque podría haber sido mi falta de conocimiento sobre cómo implementar el editor.

He estado leyendo sobre BasicComboBoxUI también, pero nada de lo que he encontrado se ha destacado como la solución.

Por favor, ¿alguien puede indicarme amablemente en la dirección correcta, he pasado días retocando con esto, y realmente está empezando a molestarme? Disculpe el código de demostración generado por netbeans, fue solo para permitirme juntar una demostración rápidamente.

package com.test; import java.awt.*; public class TestForm extends javax.swing.JFrame { public TestForm() { initComponents(); } @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { cboOne = new javax.swing.JComboBox(); txtOne = new javax.swing.JTextField(); txtTwo = new javax.swing.JTextField(); btnValidate = new javax.swing.JButton(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); cboOne.setBackground(new java.awt.Color(255, 255, 255)); cboOne.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Valid Value", "Invalid Value", "Another Invalid Value" })); txtOne.setText("123"); txtTwo.setText("123"); btnValidate.setText("Validate"); btnValidate.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { btnValidateActionPerformed(evt); } }); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(cboOne, 0, 376, Short.MAX_VALUE) .addComponent(txtOne) .addComponent(txtTwo) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(0, 0, Short.MAX_VALUE) .addComponent(btnValidate))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(cboOne, javax.swing.GroupLayout.PREFERRED_SIZE, 65, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(txtOne, javax.swing.GroupLayout.PREFERRED_SIZE, 51, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(txtTwo, javax.swing.GroupLayout.PREFERRED_SIZE, 58, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(btnValidate) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); pack(); }// //GEN-END:initComponents private void btnValidateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnValidateActionPerformed //Check if the selection in the ComboBox is valid... if (((String)cboOne.getSelectedItem()).equals("Valid Value")) { //Selected Value is Valid. //We want the combo box to appear with a white background //and black text. cboOne.setBackground(Color.white); cboOne.setForeground(Color.black); } else { //The value specified is invalid. //We want to highlight the field in pink to identify an issue, //and change the color of the text to red too: cboOne.setBackground(Color.pink); cboOne.setForeground(Color.red); } //Check if the value entered into the first Text Field is valid... if (txtOne.getText().equals("123")) { //Selected Value is Valid. //We want the text box to appear with a white background //and black text. txtOne.setBackground(Color.white); txtOne.setForeground(Color.black); } else { //Selected Value is invalid. //We want the text box to appear with a pink background //and red text. txtOne.setBackground(Color.pink); txtOne.setForeground(Color.red); } //Check if the value entered into the second Text Field is valid... if (txtTwo.getText().equals("123")) { //Selected Value is Valid. //We want the text box to appear with a white background //and black text. txtTwo.setBackground(Color.white); txtTwo.setForeground(Color.black); } else { //Selected Value is invalid. //We want the text box to appear with a pink background //and red text. txtTwo.setBackground(Color.pink); txtTwo.setForeground(Color.red); } }//GEN-LAST:event_btnValidateActionPerformed public static void main(String args[]) { /* Create and display the form */ java.awt.EventQueue.invokeLater(new Runnable() { public void run() { new TestForm().setVisible(true); } }); } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton btnValidate; private javax.swing.JComboBox cboOne; private javax.swing.JComboBox jComboBox1; private javax.swing.JComboBox jComboBox2; private javax.swing.JTextField txtOne; private javax.swing.JTextField txtTwo; // End of variables declaration//GEN-END:variables } 

Actualizar

Olvida mencionarlo La razón por la que estás teniendo problemas con el color de tu combobox es porque el color que estás viendo es el color de selección. El color se define dentro de los valores predeterminados de la apariencia, no hay manera de cambiar estos colores para un solo componente sin escribir su propio delegado de aspecto y sensación, que yo, personalmente, no

Este es un ejemplo de proporcionar resaltado a campos no válidos utilizando JXLayer (ahora JLayer , pero no he tenido tiempo de convertirlo), mientras que este ejemplo sí utiliza la API InputVerifer , no hay ninguna razón para hacerlo, solo se usa para parte del ejemplo. Sería bastante fácil hacer resaltado post-validación también, el foco está en la funcionalidad de resaltado, no en el método de validación;).

Esto se basa en la idea presentada por Kirill Grouchnikov en su blog Pushing Pixels, superposiciones de validación usando JXLayer

Este es un prototipo de una idea que hice hace un tiempo, el código todavía necesita algunos ajustes para mejorar el rendimiento, pero por lo demás es bastante funcional … Preferiría mejor el soporte integrado para la validación en tiempo real … pero así soy yo. 😉

Realce

Clase de prueba principal …

 import com.jhlabs.image.GaussianFilter; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import javax.swing.InputVerifier; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.EventListenerList; import org.jdesktop.jxlayer.JXLayer; import org.jdesktop.jxlayer.plaf.AbstractLayerUI; public class FormValidationExample { public static void main(String[] args) { new FormValidationExample(); } public FormValidationExample() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) { } JFrame frame = new JFrame("Testing"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new BorderLayout()); frame.add(new TestPane()); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } public class TestPane extends JPanel { private JXLayer layer; private javax.swing.JComboBox cboOne; private javax.swing.JTextField txtOne; private javax.swing.JTextField txtTwo; private DefaultValidationHighlightModel validationModel; private boolean ignoreValidationRequest; public TestPane() { setLayout(new BorderLayout()); JPanel content = new JPanel(new GridBagLayout()); ValidationUI ui = new ValidationUI(); validationModel = new DefaultValidationHighlightModel(ui); layer = new JXLayer<>(content, ui); add(layer); cboOne = new javax.swing.JComboBox(); cboOne.setInputVerifier(new AbstractValidationInputVerifier(validationModel) { @Override public boolean verify(JComponent input) { boolean valid = false; JComboBox cb = (JComboBox) input; String textOfOne = txtOne.getText(); String textOfTwo = txtTwo.getText(); if (cb.getSelectedIndex() == 2) { valid = true; } else if (cb.getSelectedIndex() == 0 && "123".equals(textOfOne) && "456".equals(textOfTwo)) { valid = true; } else if (cb.getSelectedIndex() == 1 && "456".equals(textOfOne) && "789".equals(textOfTwo)) { valid = true; } return valid; } }); txtOne = new javax.swing.JTextField("123", 10); txtOne.setInputVerifier(new AbstractValidationInputVerifier(validationModel) { @Override public boolean verify(JComponent input) { JTextField field = (JTextField) input; String text = field.getText(); return "123".equals(text) || "456".equals(text); } }); txtTwo = new javax.swing.JTextField("123", 10); txtTwo.setInputVerifier(new AbstractValidationInputVerifier(validationModel) { @Override public boolean verify(JComponent input) { JTextField field = (JTextField) input; String text = field.getText(); return "456".equals(text) || "789".equals(text); } }); cboOne.setModel(new javax.swing.DefaultComboBoxModel(new String[]{"Only works with 123, 456", "Only works with 456, 789", "Works with everybody"})); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.insets = new Insets(4, 4, 4, 4); content.add(cboOne, gbc); content.add(txtOne, gbc); content.add(txtTwo, gbc); validateFields(); } protected void validateFields() { if (!ignoreValidationRequest) { ignoreValidationRequest = true; try { cboOne.getInputVerifier().shouldYieldFocus(cboOne); txtOne.getInputVerifier().shouldYieldFocus(txtOne); txtTwo.getInputVerifier().shouldYieldFocus(txtTwo); } finally { ignoreValidationRequest = false; } } } public abstract class AbstractValidationInputVerifier extends InputVerifier { private IValidationHighlightModel model; public AbstractValidationInputVerifier(IValidationHighlightModel model) { this.model = model; } public IValidationHighlightModel getModel() { return model; } @Override public boolean shouldYieldFocus(JComponent input) { if (verify(input)) { getModel().removeInvalidField(input); } else { getModel().addInvalidField(input); } validateFields(); return true; } } } } 

JXLayer con JXLayer , resalte las capas de UI …

 public class ValidationUI extends HighlightComponentUI { public ValidationUI() { super(Color.RED); } } public class HighlightComponentUI extends AbstractLayerUI { private List> lstHighlights; private Color highlightColor; public HighlightComponentUI(Color highlight) { highlightColor = highlight; lstHighlights = new ArrayList>(25); } protected void cleanReferences() { if (lstHighlights.size() > 0) { List> removed = new ArrayList>(lstHighlights.size()); for (WeakReference wr : lstHighlights) { Component weak = wr.get(); if (weak == null) { removed.add(wr); } } lstHighlights.removeAll(removed); setDirty(true); } } protected boolean contains(Component comp) { boolean contains = false; cleanReferences(); for (WeakReference wr : lstHighlights) { Component weak = wr.get(); if (weak.equals(comp)) { contains = true; break; } } return contains; } protected void clearHighlights() { lstHighlights.clear(); setDirty(true); } protected void addHighlight(Component comp) { if (comp != null) { if (!contains(comp)) { lstHighlights.add(new WeakReference(comp)); setDirty(true); } } } public Component[] getHighlightedComponents() { List comps = new ArrayList<>(lstHighlights.size()); for (WeakReference wr : lstHighlights) { Component comp = wr.get(); if (comp != null) { comps.add(comp); } } return comps.toArray(new Component[comps.size()]); } protected void removeHighlight(Component comp) { cleanReferences(); WeakReference toRemove = null; for (WeakReference wr : lstHighlights) { Component weak = wr.get(); if (weak.equals(comp)) { toRemove = wr; break; } } if (toRemove != null) { lstHighlights.remove(toRemove); setDirty(true); } } public Color getHighlight() { return highlightColor; } /** * Does a recursive search of all the child components of the supplied * parent looking for the supplied child * * @param parent * @param child * @return true if the child resides within the parent's hierarchy, * otherwise false */ public boolean contains(Container parent, Component child) { boolean contains = false; if (child.getParent() != null) { if (child.getParent().equals(parent)) { contains = true; } else { for (Component comp : parent.getComponents()) { if (comp instanceof Container) { if (contains((Container) comp, child)) { contains = true; break; } } } } } return contains; } @Override protected void paintLayer(Graphics2D g2, JXLayer l) { super.paintLayer(g2, l); Graphics2D c = (Graphics2D) g2.create(); JComponent view = l.getView(); while (view instanceof JXLayer) { view = (JComponent) ((JXLayer) view).getView(); } for (WeakReference wr : lstHighlights) { Component comp = wr.get(); if (comp != null && contains(view, comp)) { // A cache here would be VERY useful, would need to be mainatined // against the component instance as well as the component // size properties... BufferedImage img = new BufferedImage(comp.getWidth(), comp.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = img.createGraphics(); g2d.setComposite(AlphaComposite.Clear); g2d.fillRect(0, 0, img.getWidth(), img.getHeight()); g2d.setComposite(AlphaComposite.SrcOver); comp.printAll(g2d); g2d.dispose(); BufferedImage glow = GlowEffectFactory.generateGlow(img, 8, getHighlight(), 0.75f); Point point = comp.getLocation(); point = SwingUtilities.convertPoint(comp.getParent(), point, view); int x = point.x - ((glow.getWidth() - comp.getWidth()) / 2); int y = point.y - ((glow.getHeight() - comp.getHeight()) / 2); c.drawImage(glow, x, y, l); } } c.dispose(); } } 

Clase relacionada con el modelo de validación (me gusta usar interfaces e implementaciones abstract para proporcionar flexibilidad a la API y reducir el acoplamiento donde puedo). Mi prototipo original tenía la capa y el modelo de UI separados y actualizados a través del soporte de ChangeListener , pero los combiné aquí para simplificar …

 public class DefaultValidationHighlightModel extends AbstractValidationHighlightModel { private HighlightComponentUI ui; public DefaultValidationHighlightModel(HighlightComponentUI ui) { this.ui = ui; } @Override public void addInvalidField(Component comp) { if (!ui.contains(comp)) { ui.addHighlight(comp); fireStateChanged(); } } @Override public void removeInvalidField(Component comp) { if (ui.contains(comp)) { ui.removeHighlight(comp); fireStateChanged(); } } @Override public Component[] getInvalidFields() { return ui.getHighlightedComponents(); } } public abstract class AbstractValidationHighlightModel implements IValidationHighlightModel { private EventListenerList listenerList; public EventListenerList getListenerList() { if (listenerList == null) { listenerList = new EventListenerList(); } return listenerList; } @Override public void addChangeListener(ChangeListener listener) { getListenerList().add(ChangeListener.class, listener); } @Override public void removeChangeListener(ChangeListener listener) { getListenerList().remove(ChangeListener.class, listener); } protected ChangeListener[] getChangeListeners() { return getListenerList().getListeners(ChangeListener.class); } protected void fireStateChanged() { ChangeListener[] listeners = getChangeListeners(); if (listeners != null && listeners.length > 0) { ChangeEvent evt = new ChangeEvent(this); for (ChangeListener listener : listeners) { listener.stateChanged(evt); } } } } public interface IValidationHighlightModel { public void addInvalidField(Component comp); public void removeInvalidField(Component comp); public Component[] getInvalidFields(); public void addChangeListener(ChangeListener listener); public void removeChangeListener(ChangeListener listener); } public static class GlowEffectFactory { public static BufferedImage createCompatibleImage(int width, int height) { return createCompatibleImage(width, height, Transparency.TRANSLUCENT); } public static BufferedImage createCompatibleImage(Dimension size) { return createCompatibleImage(size.width, size.height); } public static BufferedImage createCompatibleImage(int width, int height, int transparency) { GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); BufferedImage image = gc.createCompatibleImage(width, height, transparency); image.coerceData(true); return image; } public static BufferedImage applyMask(BufferedImage sourceImage, BufferedImage maskImage, int method) { BufferedImage maskedImage = null; if (sourceImage != null) { int width = maskImage.getWidth(null); int height = maskImage.getHeight(null); maskedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D mg = maskedImage.createGraphics(); int x = (width - sourceImage.getWidth(null)) / 2; int y = (height - sourceImage.getHeight(null)) / 2; mg.drawImage(sourceImage, x, y, null); mg.setComposite(AlphaComposite.getInstance(method)); mg.drawImage(maskImage, 0, 0, null); mg.dispose(); } return maskedImage; } public static BufferedImage generateBlur(BufferedImage imgSource, int size, Color color, float alpha) { GaussianFilter filter = new GaussianFilter(size); int imgWidth = imgSource.getWidth(); int imgHeight = imgSource.getHeight(); BufferedImage imgBlur = createCompatibleImage(imgWidth, imgHeight); Graphics2D g2 = imgBlur.createGraphics(); g2.drawImage(imgSource, 0, 0, null); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, alpha)); g2.setColor(color); g2.fillRect(0, 0, imgSource.getWidth(), imgSource.getHeight()); g2.dispose(); imgBlur = filter.filter(imgBlur, null); return imgBlur; } public static BufferedImage generateBlur(BufferedImage imgSource, int size) { GaussianFilter filter = new GaussianFilter(size); int imgWidth = imgSource.getWidth(); int imgHeight = imgSource.getHeight(); BufferedImage imgBlur = createCompatibleImage(imgWidth, imgHeight); Graphics2D g2 = imgBlur.createGraphics(); g2.drawImage(imgSource, 0, 0, null); g2.dispose(); imgBlur = filter.filter(imgBlur, null); return imgBlur; } public static BufferedImage generateGlow(BufferedImage imgSource, int size, Color color, float alpha) { int imgWidth = imgSource.getWidth() + (size * 2); int imgHeight = imgSource.getHeight() + (size * 2); BufferedImage imgMask = createCompatibleImage(imgWidth, imgHeight); Graphics2D g2 = imgMask.createGraphics(); int x = Math.round((imgWidth - imgSource.getWidth()) / 2f); int y = Math.round((imgHeight - imgSource.getHeight()) / 2f); g2.drawImage(imgSource, x, y, null); g2.dispose(); // ---- Blur here --- BufferedImage imgGlow = generateBlur(imgMask, size, color, alpha); // ---- Blur here ---- imgGlow = applyMask(imgGlow, imgMask, AlphaComposite.DST_OUT); return imgGlow; } } 

Advertencias

Esto requiere JXLayer (estaba usando la versión 3) (que ya no parece estar disponible en la red …) y SwingX (estaba usando la versión 1.6.4)

Puse todo el código fuente de JXLayer (versión 3) y los ejemplos de Piet en un solo archivo comprimido y sugiero que, si está interesado, tome una copia y la guarde en un lugar seguro.

También necesitarás filtros JHLabs