JavaSE JFC技术 (AWT + Swing + Graphics2D):完全不改变原生Swing代码,换肤。
源码打包:Swing_lnfImpl.zip (23.5Kb)
结构:
源码:
package com.han.lnf; import javax.swing.*; import javax.swing.plaf.LayerUI; import javax.swing.plaf.synth.SynthLookAndFeel; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.text.ParseException; /** * Class note: Created by Gaowen on 14-1-9. */ public class TestCase extends JPanel { private static JLayer<JPanel> jLayer; private static WaitLayerUI waitLayerUI; private static Timer stopper; private static JButton orderButton; public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { createAndShowGUI(); } }); } private static void createAndShowGUI() { initLookAndFeel();// specify the L&F final LayerUI<JPanel> spotlightLayerUI = new SpotlightLayerUI(); waitLayerUI = new WaitLayerUI(); stopper = new Timer(4000, new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { waitLayerUI.stop(); jLayer.setUI(spotlightLayerUI); } }); stopper.setRepeats(false); JPanel contentPane = new TestCase(); jLayer = new JLayer<>(contentPane, spotlightLayerUI); final JFrame f = new JFrame("Custom L&F"); f.add(jLayer); f.getRootPane().setDefaultButton(orderButton); f.setSize(290, 180); f.setResizable(false); f.setLocationRelativeTo(null); f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); f.setVisible(true); } private static void initLookAndFeel() { SynthLookAndFeel lookAndFeel = new SynthLookAndFeel(); try { lookAndFeel.load( TestCase.class.getResourceAsStream("lnfimpl/synth.xml"), TestCase.class); } catch (ParseException e) { System.err.println("There is an error in parsing xml of Synth"); e.printStackTrace(); } try { UIManager.setLookAndFeel(lookAndFeel); } catch (UnsupportedLookAndFeelException e) { System.err.println("Synth is not a supported look and feel"); e.printStackTrace(); } } private static class SpotlightLayerUI extends LayerUI<JPanel> { private boolean mActive; private int mX, mY; @Override public void installUI(JComponent c) { super.installUI(c); @SuppressWarnings("unchecked") JLayer<JPanel> jLayer = (JLayer<JPanel>) c; jLayer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } @Override public void uninstallUI(JComponent c) { super.uninstallUI(c); @SuppressWarnings("unchecked") JLayer<JPanel> jLayer = (JLayer<JPanel>) c; jLayer.setLayerEventMask(0); } @Override public void paint(Graphics g, JComponent c) { super.paint(g, c);// paint the view Graphics2D g2 = (Graphics2D) g.create(); if (mActive) { /* Create a radial gradient, transparent in the middle */ Point2D center = new Point2D.Float(mX, mY); float radius = 72; float[] dist = {0.0f, 1.0f}; Color[] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK}; RadialGradientPaint p = new RadialGradientPaint(center, radius, dist, colors); g2.setPaint(p); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .6f)); g2.fillRect(0, 0, c.getWidth(), c.getHeight()); } g2.dispose(); } @Override protected void processMouseEvent(MouseEvent e, JLayer<? extends JPanel> l) { if (e.getID() == MouseEvent.MOUSE_ENTERED) { mActive = true; } else if (e.getID() == MouseEvent.MOUSE_EXITED) { mActive = false; } l.repaint(); } @Override protected void processMouseMotionEvent(MouseEvent e, JLayer<? extends JPanel> l) { Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l); mX = p.x; mY = p.y; l.repaint(); } } private static class WaitLayerUI extends LayerUI<JPanel> implements ActionListener { private boolean mIsRunning; private boolean mIsFadingOut; private Timer mTimer; private int mAngle; private int mFadeCount; private final int mFadeLimit = 15;// immutable, so declared as final @Override public void paint(Graphics g, JComponent c) { super.paint(g, c);// paint the view if (!mIsRunning) return; float fade = mFadeCount / mFadeLimit; Graphics2D g2 = (Graphics2D) g.create(); /* Gray it out, in fact it uses the FOREGROUND of L&F */ Composite urComposite = g2.getComposite(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f * fade)); int w = c.getWidth(); int h = c.getHeight(); g2.fillRect(0, 0, w, h); g2.setComposite(urComposite); /* Paint the wait indicator */ int s = Math.min(w, h) / 5; int cx = w / 2; int cy = h / 2; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setStroke(new BasicStroke(s / 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2.setPaint(Color.white); g2.rotate(Math.PI * mAngle / 180, cx, cy); for (int i = 0; i < 12; i++) { float scale = (11.0f - i) / 11.0f; g2.drawLine(cx + s, cy, cx + s * 2, cy); g2.rotate(-Math.PI / 6, cx, cy); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, scale * fade)); } g2.dispose(); } @Override public void actionPerformed(ActionEvent e) { if (mIsRunning) { firePropertyChange("tick", 0, 1); mAngle += 3; if (mAngle >= 360) { mAngle = 0; } if (mIsFadingOut) { if (--mFadeCount == 0) { mIsRunning = false; mTimer.stop(); } } else if (mFadeCount < mFadeLimit) { mFadeCount++; } } } private void start() { if (mIsRunning) return; /* Run a thread for animation */ mIsRunning = true; mIsFadingOut = false; mFadeCount = 0; int fps = 24; int tick = 1000 / fps; mTimer = new Timer(tick, this); mTimer.start(); } private void stop() { mIsFadingOut = true; } @Override public void applyPropertyChange(PropertyChangeEvent pce, JLayer<? extends JPanel> l) { if ("tick".equals(pce.getPropertyName())) { l.repaint(); } } } TestCase() { ButtonGroup entreeGroup = new ButtonGroup(); JRadioButton radioButton = new JRadioButton("Beef", true); JRadioButton radioButton2 = new JRadioButton("Chicken"); JRadioButton radioButton3 = new JRadioButton("Vegetable"); entreeGroup.add(radioButton); entreeGroup.add(radioButton2); entreeGroup.add(radioButton3); JCheckBox jCheckBox = new JCheckBox("Ketchup"); JCheckBox jCheckBox2 = new JCheckBox("Mustard"); JCheckBox jCheckBox3 = new JCheckBox("Pickles"); JLabel label = new JLabel("Special requests:"); JTextField tf = new JTextField(15); final JLabel info = new JLabel(); info.setName("customLabel"); orderButton = new JButton("Place Order"); orderButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { info.setText("default button clicked"); if (stopper.isRunning()) return; stopper.start(); waitLayerUI.start(); jLayer.setUI(waitLayerUI); } }); JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { info.setText("cancel button clicked"); } }); add(radioButton); add(radioButton2); add(radioButton3); add(jCheckBox); add(jCheckBox2); add(jCheckBox3); add(label); add(tf); add(orderButton); add(cancelButton); add(info); } }
皮肤:
<?xml version="1.0" encoding="UTF-8"?> <synth> <!-- A backing style, it is good practice to do this --> <style id="backingStyle"> <opaque value="true"/> <font name="Dialog" size="12"/> <state> <color type="BACKGROUND" value="#D8D987"/> <color type="FOREGROUND" value="#9400d3"/> <color type="TEXT_FOREGROUND" value="#3C3C3C"/> </state> </style> <bind style="backingStyle" type="region" key=".*"/> <style id="button"> <font name="Monotype Corsiva" size="18"/> <property key="Button.defaultButtonFollowsFocus" type="boolean" value="false"/> <!-- Shift the text one pixel when pressed --> <property key="Button.textShiftOffset" type="integer" value="1"/> <!-- set size of buttons --> <insets top="0" left="10" bottom="0" right="10"/> <!-- The state behavior is like an cascading filter --> <state> <imagePainter id="defaultStatePainter" method="buttonBackground" path="lnfimpl/button/graphics/btn_general_normal.png" sourceInsets="3 3 3 3"/> </state> <state value="DISABLED"> <imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_disabled.png" sourceInsets="3 2 4 3"/> </state> <state value="DEFAULT"> <imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_special.png" sourceInsets="4 4 4 4"/> </state> <state value="PRESSED"> <imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_general_pressed.png" sourceInsets="4 4 4 4"/> </state> <state value="MOUSE_OVER"> <imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_general_hover.png" sourceInsets="4 4 4 4"/> </state> <state value="FOCUSED"> <painter idref="defaultStatePainter" method="buttonBackground"/> <object id="buttonFocusedPainter" class="com.han.lnf.lnfimpl.button.ButtonFocusedPainter"/> <object id="bgColor" class="javax.swing.plaf.ColorUIResource"> <int>218</int> <int>116</int> <int>62</int> </object> <defaultsProperty key="Button.background" type="idref" value="bgColor"/> <painter idref="buttonFocusedPainter" method="buttonBorder"/> </state> </style> <bind style="button" type="region" key="Button"/> <style id="textfield"> <!-- font here must support Chinese characters --> <font name="Microsoft YaHei UI" size="11"/> <color id="caretColor" value="MAGENTA"/> <property key="TextField.caretForeground" type="idref" value="caretColor"/> <insets top="3" left="5" bottom="4" right="5"/> <state> <color type="TEXT_FOREGROUND" value="#008b8b"/> <color type="TEXT_BACKGROUND" value="#ffa349"/> <imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_normal.png" sourceInsets="5 5 5 5" paintCenter="false"/> </state> <state value="DISABLED"> <imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_disabled.png" sourceInsets="5 5 5 5" paintCenter="true"/> </state> <state value="FOCUSED"> <imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_pressed.png" sourceInsets="5 5 5 5" paintCenter="false"/> </state> </style> <bind style="textfield" type="region" key="TextField"/> <style id="label"> <font name="Impact" size="12"/> </style> <bind style="label" type="region" key="Label"/> <style id="customLabel" clone="label"> <font name="Comic Sans MS" size="12"/> </style> <bind style="customLabel" type="name" key="custom.*"/> <style id="checkbox"> <font name="Segoe Script" size="12"/> <insets top="4" left="2" bottom="4" right="4"/> <imageIcon id="check_off_normal" path="lnfimpl/checkbox/graphics/cb_un_normal.png"/> <imageIcon id="check_off_disable" path="lnfimpl/checkbox/graphics/cb_un_disable.png"/> <imageIcon id="check_off_pressed" path="lnfimpl/checkbox/graphics/cb_un_pressed.png"/> <imageIcon id="check_on_normal" path="lnfimpl/checkbox/graphics/cb_normal.png"/> <imageIcon id="check_on_disable" path="lnfimpl/checkbox/graphics/cb_disable.png"/> <imageIcon id="check_on_pressed" path="lnfimpl/checkbox/graphics/cb_pressed.png"/> <state> <property key="CheckBox.icon" value="check_off_normal"/> </state> <state value="SELECTED and DISABLED"> <property key="CheckBox.icon" value="check_on_disable"/> </state> <state value="SELECTED and PRESSED"> <property key="CheckBox.icon" value="check_on_pressed"/> </state> <state value="SELECTED and FOCUSED"> <property key="CheckBox.icon" value="check_on_normal"/> <object id="checkboxFocusedPainter" class="com.han.lnf.lnfimpl.checkbox.CheckBoxFocusedPainter"/> <object id="bgColor" class="javax.swing.plaf.ColorUIResource"> <int>29</int> <int>88</int> <int>32</int> </object> <defaultsProperty key="CheckBox.background" type="idref" value="bgColor"/> <painter idref="checkboxFocusedPainter" method="checkBoxBackground"/> </state> <state value="SELECTED and ENABLED"> <property key="CheckBox.icon" value="check_on_normal"/> </state> <state value="DISABLED"> <property key="CheckBox.icon" value="check_off_disable"/> </state> <state value="PRESSED"> <property key="CheckBox.icon" value="check_off_pressed"/> </state> <state value="FOCUSED"> <painter idref="checkboxFocusedPainter" method="checkBoxBackground"/> </state> </style> <bind style="checkbox" type="region" key="CheckBox"/> <style id="radiobutton"> <font name="Segoe Print" size="12"/> <insets top="4" left="2" bottom="4" right="4"/> <imageIcon id="radio_off_normal" path="lnfimpl/radiobutton/graphics/rb_un_normal.png"/> <imageIcon id="radio_off_disable" path="lnfimpl/radiobutton/graphics/rb_un_disable.png"/> <imageIcon id="radio_off_pressed" path="lnfimpl/radiobutton/graphics/rb_un_pressed.png"/> <imageIcon id="radio_on_normal" path="lnfimpl/radiobutton/graphics/rb_normal.png"/> <imageIcon id="radio_on_disable" path="lnfimpl/radiobutton/graphics/rb_disable.png"/> <imageIcon id="radio_on_pressed" path="lnfimpl/radiobutton/graphics/rb_pressed.png"/> <state> <property key="RadioButton.icon" value="radio_off_normal"/> </state> <state value="SELECTED and DISABLED"> <property key="RadioButton.icon" value="radio_on_disable"/> </state> <state value="SELECTED and PRESSED"> <property key="RadioButton.icon" value="radio_on_pressed"/> </state> <state value="SELECTED and FOCUSED"> <property key="RadioButton.icon" value="radio_on_normal"/> <object id="radiobuttonFocusedPainter" class="com.han.lnf.lnfimpl.radiobutton.RadioButtonFocusedPainter"/> <painter idref="radiobuttonFocusedPainter" method="radioButtonBackground"/> </state> <state value="SELECTED and ENABLED"> <property key="RadioButton.icon" value="radio_on_normal"/> </state> <state value="DISABLED"> <property key="RadioButton.icon" value="radio_off_disable"/> </state> <state value="PRESSED"> <property key="RadioButton.icon" value="radio_off_pressed"/> </state> <state value="FOCUSED"> <painter idref="radiobuttonFocusedPainter" method="radioButtonBackground"/> </state> </style> <bind style="radiobutton" type="region" key="RadioButton"/> <style id="panel"> <object id="panelPainter" class="com.han.lnf.lnfimpl.panel.PanelPainter"/> <object id="backgroundColor" class="javax.swing.plaf.ColorUIResource"> <int>255</int> <int>255</int> <int>255</int> </object> <defaultsProperty key="Panel.background" type="idref" value="backgroundColor"/> <object id="foregroundColor" class="javax.swing.plaf.ColorUIResource"> <int>200</int> <int>200</int> <int>200</int> </object> <defaultsProperty key="Panel.foreground" type="idref" value="foregroundColor"/> <painter idref="panelPainter" method="panelBackground"/> </style> <bind style="panel" type="region" key="Panel"/> </synth>
package com.han.lnf.lnfimpl.button; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.UIManager; import javax.swing.plaf.synth.SynthContext; import javax.swing.plaf.synth.SynthPainter; public class ButtonFocusedPainter extends SynthPainter { @Override public void paintButtonBorder(SynthContext context, Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); float[] arr = {8, 2, 2, 6}; Color background = UIManager.getColor("Button.background"); g2.setColor(background); g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0)); g2.drawRoundRect(x + 4, y + 4, w - 8, h - 8, 10, 10); g2.dispose(); } }
package com.han.lnf.lnfimpl.checkbox; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.UIManager; import javax.swing.plaf.synth.SynthContext; import javax.swing.plaf.synth.SynthPainter; public class CheckBoxFocusedPainter extends SynthPainter { @Override public void paintCheckBoxBackground(SynthContext context, Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); float[] arr = {8, 2, 2, 6}; Color background = UIManager.getColor("CheckBox.background"); g2.setColor(background); g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0)); g2.drawRoundRect(x + 28, y + 7, w - 30, h - 13, 10, 10); g2.dispose(); } }
package com.han.lnf.lnfimpl.panel; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.geom.CubicCurve2D; import javax.swing.UIManager; import javax.swing.plaf.synth.SynthContext; import javax.swing.plaf.synth.SynthPainter; public class PanelPainter extends SynthPainter { @Override public void paintPanelBackground(SynthContext context, Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); Color background = UIManager.getColor("Panel.background"); g2.setColor(background); g2.fillRect(x, y, w, h); Color foreground = UIManager.getColor("Panel.foreground"); g2.setColor(foreground); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); CubicCurve2D.Double arc2d = new CubicCurve2D.Double(0, h / 4d, w / 3d, h / 10d, .67 * w, 1.5 * h, w, h / 8d); g2.draw(arc2d); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g2.dispose(); } }
package com.han.lnf.lnfimpl.radiobutton; import java.awt.BasicStroke; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.plaf.synth.SynthContext; import javax.swing.plaf.synth.SynthPainter; public class RadioButtonFocusedPainter extends SynthPainter { @Override public void paintRadioButtonBackground(SynthContext context, Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); float[] arr = {8, 2, 2, 6}; g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0)); g2.drawRoundRect(x + 28, y + 8, w - 30, h - 14, 10, 10); g2.dispose(); } }
由于本人不是专门做美工的,所以界面有定制的功能,但是定制的不算漂亮~望喜欢
这个和Android的xml皮肤比较像,不过我觉得Android的更强大,因为它xml里面可以使用NinePatch Tech。不过幸运的是,安卓的NinePatch技术可以由源码过度到JavaSE Swing里,但是必须是Java paint代码。。所以Synth可以使用NinePatch技术,但不如Android直接在XML中描述方便。
利用 Synth 可以创建出完全专业的外观,Java 1.4 中发布的 GTK+ 和 Windows XP 外观就完全是用 Synth 创建的。(那时它不是一个已公布的 API。)
由于我上面的例子实践中用了20张png图片,加载速度以及内存占用比Metal的L&F可能差一点,不过如果是复杂界面或者创建数量多的Components时,Synth和Metal还是差不多的。比如,我这个例子中(XP + JDK7u45 + Eclipse Kepler)如果注销掉
initLookAndFeel();// specify the L&F
这行代码,得到的Metal经典外观时占用的内存为25Kb,如果不注销,使用的定制的外观时内存为29Kb,这些内存和启动速度我觉得都比JavaFX应用要好。
如果使用100 个组件的界面来测试:
当时设计时遇到一个问题:
当使用自定义感官时,比如我上面的L&F或者使用jgoodies的UIManager.setLookAndFeel(new com.jgoodies.looks.windows.WindowsLookAndFeel());
则如果定义文本框等,像JTextField,JTextPane时,在用智能拼音输入时,输入窗口会出现方框形式的乱码。取消这种观感的设置既可解决问题(比如注销掉initLookAndFeel();// specify the L&F)。原因是这种观感不支持中文。怎么让自定义的感官支持呢?
解决了,是因为字体的原因,Arial不支持中文的显示。改为雅黑就行了,可以使用Component的getFont()以及Font的canDisplay(int codePoint)或者canDisplayUpTo(String)来判断是否支持特定的字符显示。