Swing圖層的應用——實現tooltip顯示


沒有錯是世紀前的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要輕量級,而且不會使界面失去焦點,阻塞用戶操作。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM