<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Swing


    天行健 君子以自強不息

    posts - 69, comments - 215, trackbacks - 0, articles - 16
       :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

    Swing框架之Component

    Posted on 2007-10-12 11:39 zht 閱讀(3183) 評論(3)  編輯  收藏 所屬分類: Swing
     昨天晚上寫完Swing的模型和渲染器之后,覺得對Swing的體系結構還是沒有說清楚。Swing的基礎體系結構中的四大基本對象Component、 Model、UI Delegate以及Renderer都值得詳細解釋。Swing的樹狀組件結構(雖然這是用戶界面工具通有的特征)也值得詳細解釋,因為這是完成某些復雜Swing組件,尤其像JTable、JTree、JList和JComboBox這種復雜組件中編輯功能得關鍵。此外,Swing / AWT的事件模型如Event Dispatching和Propagation和事件處理線程模型也需要詳細解釋,理解這部份是編寫高效率Swing界面的關鍵。

             今天從Swing的四大基本對象Component說起。

    ====================================

             Component在Swing的MVC模型中擔任Controller的角色,同時它也是Swing API中代表具體組件的對象。Component在Swing中對外負責提供API接口,對內負責協調控制Model和UI Delegate(有時可能還包括Renderer)的操作,可以說是整個Swing結構的中心角色。為了方便你回憶Swing的MVC模型,特地將上一篇文章中的Swing模型示意圖引了過來:

             Component代表Swing對應用程序提供了如下幾類編程接口:

    1. 用戶界面的組件樹的創建和修改的方法。這包括組件的添加和刪除等操作。
    2. 組件屬性訪問的方法,比如組件位置、組件前后背景色、組件字體等等。
    3. 組件狀態及生命周期的管理的方法,比如隱藏和顯示、創建和銷毀等等。
    4. 組件位置、大小的管理,包括通過布局管理器的方法。
    5. 組件事件處理接口的管理,包括添加、刪除等操作。

             從開發者的角度來看,Component是組件樹上的節點、是控制外觀和行為的入口、是組件事件的發源地。從Swing組件實現者的角度來看, Component是協調Model和UI Delegate的操作的地方,是低層次事件處理的地方,是高層事件發生的地方,是同父組件和子組件交互的地方。掌握的這些角度,Swing程序員完全可以實現自己的自定義簡單組件,當然如需要實現類似于JTable和JTree等復雜的矢量組件,還需要進一步了解Swing的Model和UI Delegate以及Renderer模型。

             對于復合型(Composite)組件很簡單,這兒要講述的是如何實現自定義的組件,比如表盤,比如溫度計這些沒有標準組件可以使用的組件,那么如何自己實現這種自定義組件呢?

             不考慮Model分隔和UI Delegate皮膚分離問題,能夠簡化自定義Component的模型。總的來說自定義組件需要完成兩樣基本任務:第一偵聽并處理低層事件,根據具體情況改變組件狀態,如需要還要發出高級事件;第二,根據當前組件的狀態畫出當前組件的外觀。

             偵聽底層的事件是指偵聽類似于mouse、keyboard、focus等事件,然后處理此事件,如果發現此事件帶有特定語義,表達某種組件行為,則改變當前的組件狀態以記錄,并觸發某種事件通知應用程序進行處理。舉例說明,想象你準備實現一個簡單的按鈕,你可以通過繼承JComponent來完成。你可以在按鈕初始化時,注冊此按鈕的鼠標事件偵聽器,以偵聽發生自己組件上的鼠標事件。當按鈕捕獲到鼠標按下時,檢查鼠標按下的點是否在按鈕有效區域內。如果是,則認為當前是一個按鈕按下動作,那么改變按鈕的狀態為按下去,調用repaint方法通知按鈕重畫成按下去的狀態,然后發出 ActionPerformed的事件,通知注冊在此按鈕上的應用程序的ActionListener處理這個動作。下面是一個簡單示意代碼:

    public class MyButton extends Jcomponent implements MouseListener{
        private String text;
        private boolean pressed=false;
        private ArrayList<ActionListener> listeners=new ArrayList<ActionListener>();
        public MyButton(){
            addMouseListener(this);//將自己注冊為自己的鼠標事件偵聽器,監聽鼠標事件
        }
        ....
       public void mousePressed(MouseEvent evt){
            Point p=evt.getPoint();
            if(getBounds().contains(p)){//判斷鼠標落點是否在有效區域內。
                pressed=true; //鼠標點擊的含義是按鈕被按下!改表按鈕狀態。
                repaint();    //通知按鈕重畫,由原來的抬起狀態改變成按下狀態。
                fireActionPerformed(new ActionEvent(this)); //這是一個按鈕動作事件,觸發它。
            }
       }
       public void addActionListener(ActionListener listener){
            listeners.add(listener);
       }
       public void removeActionListener(ActionListener listener){
            listeners.remove(listener);
       }
       protected fireActionPerformed(ActionEvent evt){
            for(ActionListener listener:listeners){
                listener.actionPerformed(evt);
            }
       }
       ...
       //這兒你要覆蓋paint方法,實現按鈕狀態的重畫
       public void paint(Graphics g){
           if(pressed){
             //畫出按下的樣子
           }else{
             //畫出抬起的樣子
           }
       }
    }

             上面要注意的是你要自己管理自定義組件的事件監聽器,包括addListener和removeListener方法,以及如何觸發。這個過程很簡單,基本上就是上面的模板來實現添加刪除和觸發。

             除了要負責事件的處理和新事件的觸發,自定義組件第二個要完成的任務就是要根據組件當前的狀態改變重畫組件的外觀。重畫組件的外觀只需要覆蓋public void paint(Graphics g)方法即可以,在這個方法里,你只需要根據當前的組件狀態分別畫出當前的組件即可。

             當然除了上面兩個基本準則外,不要忘了添加訪問你的組件屬性的方法,比如,如果上面的按鈕是個二元按鈕(相當于 JCheckbox/JToggleButton的那種按鈕),你可能需要提供isPressed或者setPressed來獲取和設置當前按鈕的狀態。注意,在設置狀態按鈕變化的訪問方法中,比如setPressed,你需要使用repaint方法通知按鈕重新渲染(復雜的實現可能包括觸發propertyChange事件,這兒從簡):

    public void setPressed(boolean p){
        pressed=p;
        repaint();
    }

             到此為止,你已經能根據上面的兩條準則簡單的實現你想要的組件了。但是你發現沒有,你的按鈕狀態和外觀行為都被堆到了Component (MyButton)中實現了,而且,對于各個平臺都是一個樣子,不能換皮膚。這對于比較簡單、不想要皮膚的組件,可能沒有什么,但是對于復雜的組件,比如JTable或者甚至Excel類似的電子表格的那種組件,你把數據(組件狀態)和外觀堆在這兒實現就嚴重違反了MVC原則。

             如何簡化這種組件的實現呢?使你實現的此種組件容易維護、擴展以及重用,皮膚容易換呢?這就需要Swing結構中的另外三個元素:Model、UI Delegate和Renderer,后面的幾個文章將講述Model、UI Delegate和Renderer幫你逐步實現一個復雜、靈活、可擴展、高效的矢量組件。

    =====================================================

             今天這樣講,不知道講明白沒有。當初剛開始學習Swing的時候,還不了解Swing的這種MVC結構,因此當時自己做的自定義組件都是這樣寫的,沒有單獨的Model和可以定制的UI Delegate,更不用說Renderer了。但是我覺得自己的這個學習過程,恰恰是人們學習Swing的最佳途徑,先簡化模型,然后逐步擴展,直到了解Swing模型的全部圖像。

    =====================================================================================
    昨晚回去后還是覺得Component對象本身說的太簡單,想來想去,覺得內容實在是太多,有必要補充兩個續文說明Component的其它概念。今天介紹Swing組件paint方法的處理流程,這個流程能使我們理解許多Swing機制。明天續文講述Swing事件處理器、雙緩沖和布局管理器等原理。

    =====================================

             Swing組件的paint方法是內部接口方法,一般用戶不要直接調用這個方法,它總是在事件調度線程中調用。一般說來除了系統刷新事件觸發這個方法,Component的repaint也觸發這個方法的調用。repaint方法常用于當組件狀態發生變化時刷新界面使用。repaint方法是Swing中少數幾個線程安全的方法,可以在任何線程中調用它。它的原理是往事件隊列中post一個PAINT事件。由于事件隊列的事件是被事件調度線程同步執行的,所以這個方法總是線程安全的。事件調度線程從PAINT事件中獲取事件源組件,從系統申請到圖形設備資源后,調用該組件的update方法。update是AWT時代遺留下來的產物,本意是AWT組件畫好組件背景后,再調用paint方法畫出組件的前景。Swing出現后這個方法就被棄用了,所有邏輯都轉到paint方法里。Update只是簡單地調用paint方法來完成組件的渲染。老的Java教材上經常可以看到,所謂repaint調度update方法,update接著調用paint方法,自定義組件需要重載paint方法等話語,就是因為這個歷史造成的。

             上篇文章中的MyButton的paint方法實現是一個非常老式的做法。現在JComponent的實現已經把paint方法改造成可以嵌套多重機制地方,這些機制包括層次渲染、邊框、透明背景、雙緩沖以及皮膚等。這些機制分別實現不同目的的組件提供了方便。

             圖形用戶界面的組件按照其在組件樹上的角色可以分為容器組件和葉組件。Swing模型把葉組件當作是特殊、沒有子組件的容器組件,只是JComponent繼承Container類,所有Swing組件繼承JComponent的原因。

             JComponent在paint方法中首先根據組件是否需要使用雙緩沖,封裝好圖形設備對象,然后經過一番處理后調用paintComponent方法畫出自身,然后調用paintBorder畫出邊框,最后調用paintChildren來完成子組件的渲染。

             paintComponent意思是畫出組件自身,不包括子組件。因此前一文章中的MyButton可以通過覆蓋paintComponent方法來完成MyButton的重畫。在JComponent實現中,JDK 6的paintComponent的代碼為:

        protected void paintComponent(Graphics g) {
            if (ui != null) {
                Graphics scratchGraphics = (g == null) ? null : g.create();
                try {
                    ui.update(scratchGraphics, this);
                }
                finally {
                    scratchGraphics.dispose();
                }
            }
        }

             這個方法首先檢測組件是否安裝了UI Delegate,如果安裝了就將渲染過程代理給UI Delegate。這兒是嵌入皮膚的地方。JDK 6中JComponent對應的UI Delegate的update方法缺省的實現是:

    public void update(Graphics g, JComponent c) {
     if (c.isOpaque()) {
         g.setColor(c.getBackground());
         g.fillRect(0, 0, c.getWidth(),c.getHeight());
     }
     paint(g, c);
    }

             可以看出,背景透明機制在這兒實現。首先UI Delegate對象判斷Component是否背景透明的,如果不是透明的,則使用背景色填充整個Component區域,然后調用paint(g, c)來完成組件在這種LookAndFeel種的渲染。了解了這些后,我們幾乎就明白了Swing如何實現背景透明和如何切換皮膚。由于后面的文章還會對UI Delegate和皮膚機制詳細描述,這兒就到此為止。

             目前還不要求實現皮膚,在這種情況下只需要重載paintComponent方法就行了,如果需要背景透明機制,可以模仿上面代碼,MyButton的paintComponent可以這樣寫:

    public void paintComponent(Graphics g) {
     if (isOpaque()) {
         g.setColor(getBackground());
         g.fillRect(0, 0, getWidth(), getHeight());
     }
     if(pressed){//按鈕按下去了
                    //畫出按下的樣子
     }else{
                    //畫出抬起的樣子
     }
    }

             paintBorder意思是畫出組件的邊框。Swing所有組件都有邊框的概念,就是說可以為任何組件添加各種邊框,包括自定義的邊框。JDK 6中JComponent的paintBorder的實現是這樣的:

    protected void paintBorder(Graphics g) {
            Border border = getBorder();
            if (border != null) {
                border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
            }
    }

             非常直接,如果自己有border,就將畫自己邊框的任務代理給了這個border,并傳給它圖形設備和邊界參數。Swing缺省提供了大量的各種各樣的邊框。同樣可以定義自己的邊框,實現方法就是繼承Border類,Border類中有三個方法要實現,它們的含義如下:

    public interface Border
    {
        //這兒是畫出組件邊框的地方。
        void paintBorder(Component c, Graphics g, int x, int y, int width, int height);
      //這兒是定義邊框邊界的地方,組件可以根據這信息,安排它的內容。
        Insets getBorderInsets(Component c);
    //邊框的背景是不是透明的?不是透明的要負責畫出邊框的背景。是透明的使用組件的背景。
        boolean isBorderOpaque();
    }

             這兒實現一個簡單的紅線邊框作為演示:

    public class RedLineBorder implements Border{
        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height){     
            g.setColor(Color.red);//設置為紅色
            g.drawRect(x,y, width, height);//畫出邊框
        }
        public Insets getBorderInsets(Component c){
            return new Insets(1,1,1,1); //四周都是1
        }
        public boolean isBorderOpaque(){
            return false; //背景透明
        }
    }

             paintChildren完成容器類組件的子組件的渲染。JDK缺省的實現是調用各個自組件的paint方法。一般來說不需要重載這個方法。如果想改變諸如組件Z-order遮擋順序,可以覆蓋這個方法,從相反順序調用組件的paint方法。

             到這兒我們對Swing的結構有了更深化的理解,UI Delegate機制也已經初露倪端。還有幾個重要Swing Component概念或者機制沒有講,明天的續文再對它們做出說明。


    =================================================================================

             Swing的事件處理過程為:事件調度線程(Event Dispatch Thread)從事件隊列(EventQueue)中獲取底層系統捕獲的原生事件,如鼠標、鍵盤、焦點、PAINT事件等。接著調用該事件源組件的dispachEvent。該方法過濾出特殊事件后,調用processEvent進行處理。processEvent方法根據事件類型調用注冊在這個組件上的相應事件處理器函數。事件處理器函數根據這些事件的特征,判斷出用戶的期望行為,然后根據期望行為改變組件的狀態,然后根據需要刷新組件外觀,觸發帶有特定語義的高級事件。此事件繼續傳播下去,直至調用應用程序注冊在該組件上的處理器函數。下圖是這個過程的示意圖:

     

             上圖所示意的過程簡要說就是:

    Pump an Event->Dispatch & Process Event->MouseListener.mousePressed->fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to a table->Return from actionPerformed->Return from fireActionPerformed->Return from MouseListener.mousePressed->Pump another Event.

             事件調度線程在應用程序事件處理函數actionPerformed沒有完成之前是不能處理下一個事件的,如果應用程序處理函數是一個時間復雜的任務(比如查詢數據庫并將結果顯示到表格中),后面包括PAINT事件將在長時間內得不到執行。由于PAINT事件負責將界面更新,所以這就使用戶界面失去響應。

              打一個比方,事件處理線程就像進入某城唯一的單行道一樣,事件相當于汽車。有種PAINT汽車負責為城市運輸非常重要的生活物資。但是有一天,PAINT前面有一輛汽車突然壞掉了,司機下來修車。但是這車太難修,一修就是幾天,結果后面的PAINT汽車無法前進,物資無法按時運到城里。市民急了,市長雖然不停的打電話催PAINT公司,但即使PAINT公司多添加幾輛車也沒用。由于進城的唯一條路被那輛車給占著,所以再多的PAINT車也只能堵在路上。

             不了解Swing的這種事件處理模型的人往往將時間復雜的任務放在處理函數中完成,這是造成Swing應用程序速度很慢的原因。用戶觸發這個動作,用戶界面就失去了響應,于是給用戶的感覺就是Swing太慢了。其實這個錯誤是程序員造成的,并不是Swing的過失。

             說點題外話,所有采用這種事件模型的用戶界面工具都會產生這種問題,包括SWT、GTK、MFC等流行的用戶界面工具。之所以只有Swing被誤解,主要是和Swing的歷史、市場時機、商業宣傳策略和心理學相關的。

             首先Swing的歷史和市場時機極差。Swing出現早期性能也差、錯誤也多,而Java程序員脫身于傳統圖形界面工具,對于Swing這種新的事件處理模型并不太了解,而此時正處于Java第一輪狂熱的時期,大家都滿懷希望做了大量的Swing應用程序,而這些程序中大量存在這種錯誤方法。于是市場上涌現了大批的這種程序。自從那個時代,因為這些程序,Swing被貼上了慢的標簽。又由于當時的Swing界面也丑,和一般的Windows程序風格炯異,更加深人們的這種印象。這種印象一直持續到現在,像烙印一樣深深的刻在人們的腦海里。

               其次,Swing還有一個致命的問題,就是沒有涌現出一個具有標識性的好程序,這是造成它比SWT印象慘的原因。為什么SWT采用相同的事件處理模型,而獲得了速度快的聲譽呢?這是因為人們當時對于Java做桌面應用的期望心理達到了低谷,而SWT的出現恰恰是伴隨Eclipse出現的,早期的Eclipse的確是在速度快、界面漂亮,這一掃當時人們認為Java慢,Java界面丑陋,Java無法做桌面應用的印象,繼而這個印象被加在SWT身上,人們認為Eclipse速度快、漂亮是因為SWT,其實如果你知道Swing/SWT事件處理模型的話,你就明白功勞是Eclipse開發者的,Eclipse界面漂亮其實要歸功于Eclipse界面設計專家,他們的高水平造就了這個好的IDE,從而也抬起了SWT的聲譽。而Swing的名譽恰恰就被早期Swing低水平開發者給毀了。

            再次, 這和商業宣傳策略有關。IBM和Eclipse很懂得市場宣傳,人們不是認為Java慢嗎,就宣傳SWT使用原生組件,人們不是認為Swing丑陋、風格炯異吧,就宣傳SWT風格一致性,人們不是認為Java不能做桌面應用嗎,就宣傳基于SWT的Eclipse。其實這一切的背后原因只是“人”的不同,Eclipse的開發者和Swing應用程序的開發者,Swing和SWT技術差異并沒有造成那么大的差別,如果是相近能力的人使用他們開發的話,應該能做出相近的產品。這可以從現在Eclipse和NetBeans、Intellij IDEA、JDeveloper和JBuilder看的出來。

             最后,人類有一個心理學現象,就是一旦形成對某種事物的印象,很難擺脫舊的認識,有時甚至人們不愿意承認擺在眼前的事實。總而言之,Swing和SWT不同遭遇是因為歷史、市場時機、商業宣傳策略、心理學的種種原因造成的。

              那么如何避免這個問題,編寫響應速度快的Swing應用程序呢?在SwingWorker的javadoc中有這樣兩條原則:

    Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the application becomes unresponsive. 耗時任務不要放到事件調度線程上執行,否則程序就會失去響應。

    Swing components should be accessed on the Event Dispatch Thread only. Swing組件只能在事件調度線程上訪問。

             因此處理耗時任務時,首先要啟動一個專門線程,將當前任務交給這個線程處理,而當前處理函數立即返回,繼續處理后面未決的事件。這就像前面塞車的例子似的,那個司機只要簡單的把車開到路邊或者人行道上修理,整個公路系統就會恢復運轉。

             其次,在為耗時任務啟動的線程訪問Swing組件時,要使用SwingUtilties. invokeLater或者SwingUtilities.invokeAndWait來訪問,invokeLater和invokeAndWait的參數都是一個Runnable對象,這個Runnable對象將被像普通事件處理函數一樣在事件調度線程上執行。這兩個函數的區別是,invokeLater不阻塞當前任務線程,invokeAndWait阻塞當前線程,直到Runnable對象被執行返回才繼續。在前面塞車的例子中,司機在路邊修車解決了塞車問題,但是他突然想起來要家里辦些事情,這時他就可以打個電話讓家里開車來。假如修車不受這件事情的影響,比如叫家人送他朋友一本書,他可以繼續修車,這時就相當于invokeLater;假如修車受影響,比如缺少某個汽車零件,叫家人給他送過來,那么在家人來之前,他就沒法繼續修車,這時就相當于invokeAndWait。

             下面舉一個例子說明這兩點,比如按下查詢按鈕,查詢數據量很大的數據庫,并顯示在一個表中,這個過程需要給用戶一個進度提示,并且能動態顯示表格數據動態增加的過程。假設按鈕的處理函數是myButton_actionPerformed,則:

    void myButton_actionPerformed(ActionEvent evt){
         new MyQueryTask().start();
    }
    public class MyQueryTask extends Thread{
        public void run(){
            //查詢數據庫
            final ResultSet result=...;
           / /顯示記錄
          for(;result.next();){
              //往表的Model中添加一行數據,并更新進度條,注意這都是訪問組件
             SwingUtilities.invokeLater(new Runnable(){
                  public void run(){
                        addRecord(result);
                  }
             });
          }
       ....
       }
       void addRecord(ResultSet result){
           //往表格中添加數據
           jTable.add....
          //更新進度條
          jProgress.setValue(....);
        }
    }

             JDK1.6以后,Swing提供了一個專門的類SwingWorker能幫你解決這個編程范式,你所需要做的就是繼承這個類,重載doInBackground,然后在actionPeformed中調用它的execute方法,并通過publish/process方法來更新界面。SwingWorker的主要方法和它們的作用在下面的示意圖:

              從上面示意圖可以看出,SwingWorker實際上不過是封裝了前面我所說的例子中的MyQueryTask,并做了更詳盡的考慮。execute方法相當于MyQueryTask線程start,它啟動這個后臺線程并立刻返回。SwingWorker可以注冊PropertyChangeListener,這些listener都被在事件調度線程上執行,相當于MyQueryTask中的那些訪問組件的Runnable對象。另外,publish、setProgress只不過是特殊的property事件吧,process和done不過是響應publish和PropertyChangeEvent.DONE這個事件的方法罷了。因此我們很容易將上面的例子改成SwingWorker的版本:

    void myButton_actionPerformed(ActionEvent evt){
        new MyQueryTask().execute();
    }

    public class MyQueryTask extends SwingWorker{
        public void doInBackground(){
            //查詢數據庫
            final ResultSet result=...;
            //顯示記錄
            for(;result.next();){
                //往表的Model中添加一行數據,并更新進度條,注意這都是訪問組件
                publish(result);
            }
            ....
        }
        public void process(Object ... result){
            //往表格中添加數據
            jTable.add....
            //更新進度條
            jProgress.setValue(....);
        }
    }

             對于一般的耗時任務這樣做是比較普遍的,但是有一些任務是一旦觸發之后,會周期性的觸發,如何做處理這種任務呢?JDK中提供了兩個Timer類幫你完成定時任務,一個是javax.swing.Timer,一個java.util.Timer。使用它們的方法很簡單,對于Swing的timer,使用方法如下:

    public void myActionPerformed(){
        //假設點擊了某個按鈕開始記時
        Action myAction=new AbstractAction(){
            public void actionPerformed(ActionEvent e){
                //做周期性的活動,比如顯示當前時間
                Date date=new Date();
                jMyDate.setDate(date);//jMyDate是個假想的組件,能顯示日期時間
            }
        };
        new Timer(1000, myAction).start();
    }

             java.util.Timer類似,只不過使用TimerTask完成動作封裝。注意這兩個Timer有一個關鍵的區別:Swing的Timer的事件處理都是在事件調度線程上進行的,因而它里面的操作可以直接訪問Swing組件。而java.util.Timer則可能在其他線程上,因而訪問組件時要使用SwingUtilities.invokeLater和invokeAndWait來進行。這一點要記住。

             如果要了解更詳細的信息,可以查閱SwingWorker、Swing Timer和util Timer這些類javadoc文檔和其他網上資料。最重要的是要記住了那兩條原則。

    ============================================================================

    Swing事件與事件處理器模型

             Component在Swing模型中是事件觸發源。前一篇文章在描述Swing的事件處理模型時就已經提到了這個事件處理過程。簡單來說,Swing組件在偵聽到原生事件并處理后,往往產生新的邏輯事件。邏輯事件是某些組件所特有的、具有特定語義的事件,比如JButton按下時產生ActionEvent、JComboBox一項被選中時產生ItemEvent,等等。和原生事件不同,它們并不被派發到系統事件隊列中,而是由組件直接觸發。事件處理器作為組件的觀察者添加到組件上并偵聽觸發的事件。假設事件名叫XXX,Swing中實現這個模式的一般模式是:

    1.定義一個XXXEvent

    public class XXXEvent extends Event{
        ...
        public void XXXEvent(Object src){
            super(src);
            ...
        }
       
    ...
    }

    2.定義一個事件處理器接口XXXListener,聲明所有和該事件相關的處理方法:

    public interface XXXListener extends EventListener{
        void action1(XXXEvent evt);
        void action2(XXXEvent evt);
        ...
    }

    3.在觸發它的組件中定義一下方法:

    public class MyComponent extends Jcomponent{
       
    ...
       
    //存放事件處理器的隊列
       
    private ArrayList<XXXListener>xxxListeners=new ArrayList<XXXListener>();
           
    //定義以下各種方法,訪問符號用public,以方便添加刪除處理器
           
    public void addXXXListener(XXXListener listener){
               
    xxxListeners.add(listener);
           
    }
           
    public void removeXXXListener(XXXListener listener){
               
    xxxListeners.remove(listener);
           
    }
           
    //定義各種觸發(fire)action1、action2...的方法,注意一般使用protected,以便繼承和擴展
           
    //每一個action都要定義一個相應觸發(fire)的方法
           
    protected void fireAction1(XXXEvent evt){
               
    for(XXXListener listener:xxxListeners){
                   
    listener.action1(evt);
               
    }
           
    }
           
    protected void fireAction2(XXXEvent evt){
               
    for(XXXListener listener:xxxListeners){
                   
    listener.action2(evt);
           
    }
       
    }
       
    ...
       
    //在某些地方,比如鼠標處理函數中觸發相應的動作
       
    void myMouseReleased(MouseEvent evt){
           
    ...
           
    if(應該觸發action1)
               
    fireAction1(new XXXEvent(this));
           
    ...
           
    if(應該觸發action2)
               
    fireAction2(new XXXEvent(this));
           
    ...
        
    }
    }

             XXXEvent、XXXListener、addXXXListener、removeXXXListener以及各種fireAction函數多是重復性代碼,有些Java IDE如JBuilder中能夠根據開發者的指定參數的自動生成這些代碼。

             實際上這個觀察者模式的編程范式可以推廣到任何JavaBeans,不一定是可視化的Swing組件。以前曾經見過JBuilder做的一個所謂數據庫操作的JavaBeans,它沒有界面,但它和Swing組件完全一樣添加刪除處理器。它的功能是異步操作數據庫,在數據操作完了之后觸發注冊在上面的事件處理器,該事件處理器就可以將查詢結果展現在表格中,或者輸出成報表等等。

             在這個模型中,JavaBeans本身既可以是事件源(被觀察對象),也可以是事件處理器(觀察者),JavaBeans也可以偵聽自身的事件并且處理。比如前面文章所提的MyButton在處理鼠標事件時就是自己偵聽自己發出的鼠標事件,自己既是事件源,又是事件處理器,形成自反系統。各種各樣的JavaBeans通過這種機制聯系成一張事件網,各種JavaBeans就是這個網上的節點,而它們之間的事件觸發與事件處理關系就是這張網絡上的線。當某個節點被外界或自身發出的事件所觸發時,行成了事件的傳播。這個過程很像網絡上節點的振動引起周圍周圍節點振動的模型。下圖示意了這種JavaBeans之間的事件網:

             例如new JscrollPane(new JtextArea())這個系統,它里面包括兩個JScrollBar和一個JTextArea,當鼠標拖動事件觸發JScrollBar時,JScrollBar處理了這個鼠標拖動事件,并發出滾動條拖動事件,這個事件傳播給JTextArea,JTextArea處理這個拖動事件,相應的更新自己顯示的內容,如果JTextArea之后又根據更新發出了一個新的事件,這個事件便會繼續傳播下去。

    Swing布局管理器

             現在高級圖形用戶界面工具一般都包括布局管理器機制。什么叫做布局管理器?如果所有窗口的大小是不變的,那么我們在往窗口中添加組件時,只要將組件的拖放到固定位置、調整好尺寸就可以了,就像VB的界面工具一樣。可大多數情況并非如此,用戶經常需要調整窗口的大小,以便和其他程序協同工作。這種情況下,在傳統界面工具中,比如VB,就需要顯式的偵聽窗口尺寸調整事件,根據當前窗口的大小重新計算并調整各個組件的大小和位置。AWT/SWT/Swing將這個過程自動化、模塊化了,抽象出一個布局管理器來負責管理界面組件的布局。

             它們實現原理是相似的:容器類組件偵聽初始化、invalide/validate以及容器尺寸調整等事件,一旦發生這些事件,容器類組件檢查自己是否配置了布局管理器,如果沒有,則不做任何事情;如果有,則將容器內組件的布局代理給布局管理器,讓它來完成容器內組件的重新布局。

             容器管理器對象對實現兩類接口:LayoutManager和LayoutManager2,LayoutManager2是LayoutManager的一個擴展,允許組件在添加時指定位置參數。它們的定義和含義如下:

    public interface LayoutManager {
       
    //添加組件comp,并和name關聯起來,name可以作為位置等特殊含義參數來使用
       
    void addLayoutComponent(String name, Component comp);
       
    //刪除組件comp
       
    void removeLayoutComponent(Component comp);
       
    //根據容器內的當前組件,計算容器parent的最優尺寸。
       
    Dimension preferredLayoutSize(Container parent);
       
    //根據容器內的當前組件,計算容器parent的最小尺寸。
       
    Dimension minimumLayoutSize(Container parent);
       
    //重新布局容器parent,這兒是主要布局邏輯所在。
       
    void layoutContainer(Container parent);
    }
    public interface LayoutManager2 extends LayoutManager {
       
    //添加組件comp,constraints用作指定如何以及位置的參數,這個函數主要是彌補LayoutManager版的addLayoutComponent表達能力欠缺而添加。
       
    void addLayoutComponent(Component comp, Object constraints);
       
    //根據容器內的當前組件,計算容器parent的最大尺寸。看來除了最優、最小,某些情況下還是需要知道最大。
       
    public Dimension maximumLayoutSize(Container target);
       
    //指定水平方向上組件之間的相對對齊方式,0表示和源組件對齊,1表示遠離源組件。
       
    public float getLayoutAlignmentX(Container target);
       
    //指定垂直方向上組件之間的相對對齊方式,0表示和源組件對齊,1表示遠離源組件。
       
    public float getLayoutAlignmentY(Container target);
       
    //invalidate這個布局管理器,有時布局管理器為了計算迅速,可能第一次計算之后就將一些數據給緩沖,但是后容器內的組件數目發生變化,這兒的緩沖值就需要調用這個方法通知更新
       
    public void invalidateLayout(Container target);


    }

             Swing在java.awt和javax.swing中都分別提供大量的布局管理器,這些布局管理器有簡單的如FlowLayout,有復雜的如GridBadLayout。用戶還可以自己定義自己的布局管理器,由于篇幅原因,這兒略去例子。

             Java 6中在布局管理中引入了BaseLine / Anchor的概念,能協助Java IDE的用戶界面設計工具,方便用戶來設計布局組件。NetBeans的Matisse組件首先引入了一個GroupLayout布局管理器,結合Matisse使用,提供了非常方便的布局管理和界面設計。GroupLayout和BaseLine/Anchor概念以及Matisse可以說是Java界面設計工具的一大進步,可以說足以成為Java桌面應用史上的一個里程碑。在這之前,缺乏有力的界面設計工具是Java在桌面應用失敗的一個重要原因。雖然Anchor概念早就在Delphi界面設計工具出現過,但是這個工具的出現還是Java界面設計史上的一大事件。隨著Java 6桌面應用支持的增強,以及NetBeans Matisse之類界面設計工具的出現,使得Java桌面應用時代已經到來。Seeing is believing,你不妨試一下就知道了。

    =====================================

             本想再加一節講述Swing雙緩沖機制,但是想到雙緩沖并不是Swing模型的核心概念,沒有它并不影響理解Swing的總體模型,因此打算把它作為以后的一篇專門技術文章來寫。

             這樣Swing模型中的Component部分就算是描述完了,從明天開始,講述Swing模型中的另外三個重要概念:Model、UI Delegate和Renderer。



    主站蜘蛛池模板: 免费国产叼嘿视频大全网站 | 精品一区二区三区高清免费观看| 毛片免费vip会员在线看| 亚洲精品国产手机| 91成人免费观看网站| 亚洲国产精品综合久久20| 黄色成人免费网站| 亚洲色成人四虎在线观看| 在线观看无码的免费网站| 亚洲综合小说另类图片动图| 香蕉高清免费永久在线视频| 国产成人人综合亚洲欧美丁香花 | 久久精品a一国产成人免费网站| 亚洲综合av一区二区三区不卡| 日韩视频免费在线| 人禽伦免费交视频播放| 久久亚洲精品无码播放| 99爱免费观看视频在线| 亚洲五月丁香综合视频| 免费在线看片网站| 国产一区二区三区免费| 亚洲精品国产精品国自产网站| 国产成人3p视频免费观看 | 亚洲国产精品专区| 免费无码看av的网站| 72pao国产成视频永久免费| 日本久久久久亚洲中字幕| 男人的好看免费观看在线视频| 偷自拍亚洲视频在线观看99| 国产精一品亚洲二区在线播放| 亚洲成人免费电影| 农村寡妇一级毛片免费看视频| 亚洲成av人在线视| 在线免费观看一级毛片| 三级黄色片免费看| 亚洲成A人片在线播放器| 亚洲精品无码鲁网中文电影| 动漫黄网站免费永久在线观看| 一级毛片大全免费播放下载| 亚洲欧洲日本精品| 亚洲中文字幕无码一区二区三区|