没有错是世纪前的swing。
在使用Swing的时候有个问题一直没有解决,就是Swing自带的tooltip不会跟随鼠标进行移动,而且移动到边界就会遮挡的问题。JCompoent有个createTooltip()方法,但这个方法只能改变tooltip的外观,不能改变行为。事实上tooltip的行为和设置全都是由TooltipManager来进行,所以解决的方法只有自己撸一个类似于ToolTipManager了。
实现方法
从原来TooltipManager的实现原理来看(见Tooltipmanager源码),它是通过三个控制出现、持续和隐藏的线程,和JPopupFactory来实现的。
ToolTipManager() { enterTimer = new Timer(750, new insideTimerAction()); enterTimer.setRepeats(false); exitTimer = new Timer(500, new outsideTimerAction()); exitTimer.setRepeats(false); insideTimer = new Timer(4000, new stillInsideTimerAction()); insideTimer.setRepeats(false); // create accessibility actions postTip = KeyStroke.getKeyStroke(KeyEvent.VK_F1,Event.CTRL_MASK); postTipAction = new Actions(Actions.SHOW); hideTip = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0); hideTipAction = new Actions(Actions.HIDE); moveBeforeEnterListener = new MoveBeforeEnterListener(); }
但我向做一个轻量级别的,即只用一个控制出现的线程,弹出层我不使用JPopupFactory,用的是JComponent,使用JlayeredPane来控制图层层叠次序。
实现之前
实现之前建议先了解JRootPane、JLayeredPane的相关知识。以及Java API文档对这两个的解释。
Oracle官方有文档:How to Use Root Panes、How to Use Layered Panes
概括来说,就是:一个Window(JDialog、JFrame等),窗口显示区域是JRootPane区域,如图,menu区域(可无)+ContentPane区域=JRootPane区域。
如果一个窗口没有设置menu,那么ContentPane区域=JRootPane区域。
如图,JRootPane里面有两个儿子:GlassPane,JLayeredPane两个图层。GlassPane的图层次序是0,代表顶层,JLayeredPane的次序是-1,代表底层。GlassPane默认是不显示的。JLayeredPane也有两个儿子:conentPane和JMenubar,其中contentPane是我们常用的图层。而我们要做的就是再给JlayeredPane加一个儿子,把ToolTip加进去,当然你也可以加在GlassPane里面。
(-1:底层 0:顶层)
JRootPane.java
public void setLayeredPane(JLayeredPane layered) { ...... this.add(layeredPane, -1); } public void setGlassPane(Component glass) { ...... this.add(glassPane, 0); ...... }
JRootPane 的Layeredpane 和 GlassPane的初始化方法。
实现组件
1、对于默认JToolTip来说其实它就是一个JLabel,对于多行文本的显示比较差,往往需要使用HTML标记来使用。
所以我自己实现的组件使用了JTextPane,支持html、以及多行文本形式。
2、ToolTip需要给鼠标划过的组件添加侦听,当鼠标移除后要及时移除侦听,我还采用了一个变量记录上一个侦听,防止侦听溢出。
3、动态计算ToolTip位置,当移动到右、下边界时会分别平移到鼠标左、上,防止遮挡。
4、使用一个线程计时,实现弹出时间,和tooltipmanager不同的是,鼠标不移开组件,Tooltip就不会消失。
代码实现
PopTimingTip.java
1 package; 2 3 import java.awt.BorderLayout; 4 import java.awt.Color; 5 import java.awt.Component; 6 import java.awt.Dimension; 7 import java.awt.Point; 8 import java.awt.event.ActionEvent; 9 import java.awt.event.ActionListener; 10 import java.awt.event.MouseAdapter; 11 import java.awt.event.MouseEvent; 12 13 import javax.swing.JComponent; 14 import javax.swing.JLayeredPane; 15 import javax.swing.JPanel; 16 import javax.swing.JRootPane; 17 import javax.swing.JTextPane; 18 import javax.swing.SwingUtilities; 19 import javax.swing.Timer; 20 import javax.swing.border.LineBorder; 21 22 public class PopTimingTip extends JComponent { 23 24 25 private JPanel mainPanel; 26 private JTextPane textComponent; 27 private TipListener tipListener; 28 private Component tipParent; 29 private int initTime = 0; 30 private int lastTime = 0; 31 private int vanishTime = 0; 32 private JLayeredPane windowLayer;//窗口的遮罩层,不能随便修改,只作父对象应用 33 private Point constPoint = new Point(12, 5);//距离鼠标常量,防止鼠标遮挡 34 private Point constPoint2 = new Point(2,2);//常量2 防止离鼠标太近,触发mouseexit事件 35 private Timer tipTimer; 36 private TipTimerListener tipTimerListener; 37 private static PopTimingTip popTimingTip; 38 private JRootPane rootPane;//当前根面板 39 private Dimension curTipSize; 40 private int curConType; 41 42 /** 43 * 单例外部不允许初始化 44 */ 45 private PopTimingTip() { 46 super(); 47 initTip(); 48 } 49 50 public static PopTimingTip getInstance() { 51 if(popTimingTip == null) { 52 popTimingTip = new PopTimingTip(); 53 } 54 return popTimingTip; 55 } 56 57 private void initTip() { 58 this.setLayout(new BorderLayout()); 59 this.setOpaque(false); 60 //this.setBorder(null); 61 this.setVisible(false); 62 textComponent = new JTextPane(); 63 textComponent.setContentType("text/html"); 64 textComponent.setBorder(new LineBorder(Color.BLACK)); 65 textComponent.setBackground(new Color(245, 245, 245)); 66 mainPanel = new JPanel(new BorderLayout()); 67 mainPanel.add(textComponent, BorderLayout.CENTER); 68 this.add(mainPanel, BorderLayout.CENTER); 69 70 tipTimerListener = new TipTimerListener(); 71 tipTimerListener.state = 0; 72 73 tipListener = new TipListener(); 74 tipTimer = new Timer(0, tipTimerListener); 75 tipTimer.setRepeats(false); 76 77 curTipSize = new Dimension(0,0); 78 } 79 public void showTip() { 80 this.setVisible(true); 81 } 82 /** 83 * 为某个组件设置tip 84 * @param parent 显示tooltip的对象 85 * @param text 86 */ 87 public void showTipText(JComponent parent, String text) { 88 if(parent == null) { 89 return; 90 } 91 //如果进入了新的组件,先从旧组件中移除侦听防止泄漏 92 if(tipParent != null && tipParent != parent) { 93 tipParent.removeMouseListener(tipListener); 94 tipParent.removeMouseMotionListener(tipListener); 95 } 96 tipParent = parent; 97 98 rootPane = parent.getRootPane(); 99 //防止异常获取不了根面板的情况 100 if(rootPane == null) { 101 return; 102 } 103 104 JLayeredPane layerPane = rootPane.getLayeredPane(); 105 //先从旧面板中移除tip 106 if(windowLayer != null && windowLayer != layerPane) { 107 windowLayer.remove(this); 108 } 109 windowLayer = layerPane; 110 //防止还有没有移除侦听的组件 111 tipParent.removeMouseListener(tipListener); 112 tipParent.removeMouseMotionListener(tipListener); 113 layerPane.remove(this); 114 //放置tip在遮罩窗口顶层 115 layerPane.add(this, JLayeredPane.POPUP_LAYER); 116 //窗口遮罩层添加侦听 117 tipParent.addMouseMotionListener(tipListener); 118 tipParent.addMouseListener(tipListener); 119 //测试侦听器数量 120 //System.out.println(tipParent.getMouseListeners().length + " " + tipParent.getMouseMotionListeners().length); 121 //设置tiptext 122 textComponent.setText(text); 123 mainPanel.doLayout(); 124 //this.setPreferredSize(textComponent.getPreferredSize()); 125 curTipSize = textComponent.getPreferredSize(); 126 this.setSize(textComponent.getPreferredSize().width, textComponent.getPreferredSize().height); 127 } 128 129 /** 130 * 初始化toolTip 131 * @param contentType 0:html 1:文本类型 132 * @param initTime 鼠标进入后等待时间 133 * @param lastTime 持续时间(未完成) 134 * @param vanishTime 鼠标移走后消失时间(未完成) 135 */ 136 public void setConfigure(int contentType, int initTime) { 137 if(contentType == 0 && curConType != contentType) { 138 textComponent.setContentType("text/html"); 139 } else if(contentType ==1 && curConType != contentType) { 140 textComponent.setContentType("text/plain"); 141 } 142 curConType = contentType; 143 this.initTime = initTime; 144 //this.vanishTime = vanishTime; 145 //this.lastTime = lastTime; 146 } 147 /** 148 * 坐标转换,标签跟随鼠标移动 149 */ 150 private void followWithMouse(MouseEvent e) { 151 if(windowLayer == null) { 152 return; 153 } 154 155 Point screenPoint = e.getLocationOnScreen(); 156 157 SwingUtilities.convertPointFromScreen(screenPoint, windowLayer); 158 159 int newLocationX = screenPoint.x + constPoint.x; 160 int newLocationY = screenPoint.y + constPoint.y; 161 162 Dimension tipSize = textComponent.getPreferredSize(); 163 if(newLocationX + tipSize.width > rootPane.getWidth()) { 164 newLocationX = screenPoint.x - tipSize.width - constPoint2.x; 165 } 166 if(newLocationY + tipSize.height > rootPane.getHeight()) { 167 newLocationY = screenPoint.y - tipSize.height - constPoint2.y; 168 } 169 this.setLocation(newLocationX, newLocationY); 170 //textComponent.getPreferredSize()在html初始化计算的时候有问题,重算一次 171 if(!curTipSize.equals(textComponent.getPreferredSize())) { 172 this.setSize(textComponent.getPreferredSize().width, textComponent.getPreferredSize().height); 173 } 174 } 175 176 private void setTipState(int state) { 177 tipTimer.stop();//停止上一次的任务 178 if(state == 0) {//进入组件,延迟显示 179 tipTimerListener.state = 0; 180 tipTimer.setInitialDelay(initTime); 181 tipTimer.start(); 182 } else if(state == 1) {//鼠标移出,组件消失 183 tipTimerListener.state = 1; 184 PopTimingTip.this.setVisible(false); 185 } 186 } 187 188 private class TipTimerListener implements ActionListener { 189 int state; 190 public void actionPerformed(ActionEvent e) { 191 if(state == 0) { 192 PopTimingTip.this.setVisible(true); 193 } 194 } 195 } 196 197 /** 198 * 鼠标移除后及时清除侦听防止侦听器溢出 199 */ 200 private void removeTipAndListener() { 201 if(tipParent == null) { 202 return; 203 } 204 tipParent.removeMouseListener(tipListener); 205 tipParent.removeMouseMotionListener(tipListener); 206 if(windowLayer != null) { 207 windowLayer.remove(this); 208 } 209 } 210 211 private class TipListener extends MouseAdapter { 212 public void mouseEntered(MouseEvent e) { 213 setTipState(0); 214 followWithMouse(e); 215 } 216 217 /** 218 * 鼠标移出对象时,移除对象的侦听和ToolTip 219 */ 220 public void mouseExited(MouseEvent e) { 221 setTipState(1); 222 followWithMouse(e); 223 removeTipAndListener(); 224 } 225 226 //在组件上移动时触发 227 public void mouseMoved(MouseEvent e){ 228 setTipState(0); 229 followWithMouse(e); 230 } 231 232 public void mouseClicked(MouseEvent e) { 233 if((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {//右键点击,tip消失 234 setTipState(1); 235 followWithMouse(e); 236 removeTipAndListener(); 237 } 238 } 239 } 240 241 }
ToolTipTest.java
package swingpac; import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.border.EmptyBorder; public class ToolTipTest extends JFrame { private JPanel contentPane; /** * Launch the application. */ public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { try { ToolTipTest frame = new ToolTipTest(); frame.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } /** * Create the frame. */ public ToolTipTest() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 450, 300); contentPane = new JPanel(); contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); contentPane.setLayout(new BorderLayout(0, 0)); setContentPane(contentPane); contentPane.setLayout(new FlowLayout()); initTip(); } private void initTip() { JButton btn = null; MListener m = new MListener(); for(int i=0; i<10; i++) { btn = new JButton(); btn.setName("第"+i+"个按钮!"); btn.setText("按钮"+i); btn.addMouseListener(m); contentPane.add(btn); } } class MListener extends MouseAdapter { public void mouseEntered(MouseEvent e) { JButton btn = (JButton)e.getSource(); StringBuilder sb = new StringBuilder(); sb.append("当前进入的按钮是: ") .append(btn.getName()).append(" ") .append("正在进行演示自定义Tooltip! ") .append("请自行查看源码"); PopTimingTip.getInstance().setConfigure(1, 300); PopTimingTip.getInstance().showTipText(btn, sb.toString()); } } }
效果演示(红色为鼠标位置)
遇到边界平移
总结
利用JLayeredPane还可以做出其它组件,比如弹出层录入等,个人感觉比Popup要轻量级,而且不会使界面失去焦点,阻塞用户操作。