沒有錯是世紀前的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("當前進入的按鈕是:\n") .append(btn.getName()).append("\n") .append("正在進行演示自定義Tooltip!\n") .append("請自行查看源碼"); PopTimingTip.getInstance().setConfigure(1, 300); PopTimingTip.getInstance().showTipText(btn, sb.toString()); } } }
效果演示(紅色為鼠標位置)
遇到邊界平移
總結
利用JLayeredPane還可以做出其它組件,比如彈出層錄入等,個人感覺比Popup要輕量級,而且不會使界面失去焦點,阻塞用戶操作。