前言

本文前言部分為我的一些感想,如果你只對本文介紹的Java實用技巧感興趣,可以跳過前言直接看正文的內容。

本文的寫作動機來源于最近接給人家幫忙寫的一個小程序,主要用于管理分期付款的貨款的一系列管理,包括過期款的紀錄,過期款利息的計算,為提前付款的用戶提供一些返款獎勵等等,這些與本文無關自不必細說。為了盡快完成任務,我自然選擇了我用得最多的Java來實現。經過2周的勞動,順利完成了任務,明天就可以去交差,但是這一刻我卻忽然有些其他的想法。誠然這樣的活原本屬于體力勞動,類似的活我也做過不止一次,對于很多高人來說,沒什么值得一提的,以前我也只是交差收錢了事,但這一次我卻多了一些想法,使我不吐不快。

在程序的實現過程中,我遇到了個小問題,就是計算兩個日期的差。由于以前常用的Date類的大多數方法都被標記為“deprecate”,所以我決定用Calender作為計算日期的主力。但是大多數參考書上都是由關于Calender的日期格式,Locale的設置,常量的含義等方面的講解,卻怎么也找不到這樣一個簡單卻常用的任務怎么實現(注:這也不能怪我懶惰,作為這樣一個程序來說,如果有正確且成熟的方法,誰還會去花大量時間仔細研究API呢?反正這個類可能在今后的幾個月甚至幾年都用不上,現在記住到時候也都忘了L)。于是在我google了好一陣之后,終于在某人的Blog上找到了用Calender計算日期差的方法。在那一刻我真有久旱逢甘雨之感。博主可能是一時興起,也有可能是興趣所在,但無論是什么原因,他的工作都為我提供了很大的方便。有了他的代碼示例,我可以不再去逐個查找Java-Doc里面的API,然后挑出幾個來嘗試解決問題,最后再寫個demo驗證這一繁復的過程了。

再回想一下我完成這個程序的過程,由于以前做過一些類似的程序,我可以將里面的很多部分以直接應用到這個程序中,節省了大量的時間,讓我可以更專注于核心業務的實現當中。然而或許是出于懶惰,或許是沒有時間,又或許原來的是Blog沒有多少人關注,我都沒有將這些大多數人都可能會用得上的東西放到網上。

再聯想一下國外開源工作者對中國程序員的評價“只獲取,不貢獻”,就覺得人家說得十分對。自己就用著免費的J2SDK語言,免費的Eclipse,免費的JFreeChart,免費的JasperReport……,卻從來沒能夠給人家貢獻哪怕一行代碼。這樣也就算了,但是類似于一些力所能及的東西,例如可能每個Java程序員都會碰到的一些小問題,小技巧,常常出現的錯誤,為什么我就不能把他們貼出來供人分享呢?說不定就會幫到某位哥們解決大問題,更有可能你的幾句話就能節省別人幾分鐘甚至幾小時的時間。如果每個人都能在業余時間把自己的一些心得體會貼出來,相信更多的人將因此受益。當你遇到問題的時候,才能心安理得的去GoogleBaidu。相信這也是技術論壇和技術Blog的初衷吧,畢竟這個世界并不是只有錢才是最重要的原動力。


1       改變Swing應用程序的默認字體/字號

經常使用Swing作為程序UI的人可能會注意到,Swing組件默認顯示文字的字號為11。這對于英文顯示毫無問題,但是如果用這個字號顯示中文的話,這么小的字號就會使程序變得很難看。我當年在用IReport0.56的時候就發現他的菜單欄和彈出的Dialog里的字很難看,但是將字號調大之后就好多了。雖然在最近版本的JDK里似乎修正了這個字體問題,但是如果你的程序必須使用以前版本的JDK的話,這個問題就需要處理一下,下面就是一個不錯的解決方案:

Font vFont = new Font("Dialog", Font.PLAIN, 13);

           UIManager.put("ToolTip.font", vFont);

           UIManager.put("Table.font", vFont);

           UIManager.put("TableHeader.font", vFont);

           UIManager.put("TextField.font", vFont);

           UIManager.put("ComboBox.font", vFont);

           UIManager.put("TextField.font", vFont);

           UIManager.put("PasswordField.font", vFont);

           UIManager.put("TextArea.font", vFont);

           UIManager.put("TextPane.font", vFont);

           UIManager.put("EditorPane.font", vFont);

           UIManager.put("FormattedTextField.font", vFont);

           UIManager.put("Button.font", vFont);

           UIManager.put("CheckBox.font", vFont);

           UIManager.put("RadioButton.font", vFont);

           UIManager.put("ToggleButton.font", vFont);

           UIManager.put("ProgressBar.font", vFont);

           UIManager.put("DesktopIcon.font", vFont);

           UIManager.put("TitledBorder.font", vFont);

           UIManager.put("Label.font", vFont);

           UIManager.put("List.font", vFont);

           UIManager.put("TabbedPane.font", vFont);

           UIManager.put("MenuBar.font", vFont);

           UIManager.put("Menu.font", vFont);

           UIManager.put("MenuItem.font", vFont);

           UIManager.put("PopupMenu.font", vFont);

           UIManager.put("CheckBoxMenuItem.font", vFont);

           UIManager.put("RadioButtonMenuItem.font", vFont);

           UIManager.put("Spinner.font", vFont);

           UIManager.put("Tree.font", vFont);

           UIManager.put("ToolBar.font", vFont);

           UIManager.put("OptionPane.messageFont", vFont);

           UIManager.put("OptionPane.buttonFont", vFont);

這段代碼用在程序的開始部分,可以有效地將Swing組件的顯示字體設置為我們在vFont所設定的內容。

1.1    讓窗口更好地居中顯示

無論是頂層組件JFrame還是對話框JDialog,讓他們的窗口居中顯示是一個很常見的問題,因為他們默認總是從左上角彈出來,這也太不爽了!對于這個問題,JBuilder應用程序生成向導給出了解決方案:

Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

Dimension frameSize = frame.getSize();

if (frameSize.height > screenSize.height)

    frameSize.height = screenSize.height;       

if (frameSize.width > screenSize.width)

 frameSize.width = screenSize.width;       

frame.setLocation((screenSize.width-frameSize.width)/2,screenSize.height-frameSize.height) / 2);

這個方法對于大多數窗口組件來說都足夠了,但是還有其他問題存在,比如說分辨率和顯示器的尺寸都會導致應用程序窗口“變形”,明明在17寸顯示器1024*768分辨率下顯示好好的窗口到了19寸的1280*800的寬屏下就會被“拉”得很“長”。于是,雖然有布局管理器幫我們管理拉伸后組件的放置,但仍然解決不了拉長后帶來的美觀問題。我的經驗是,對于某些窗口,由于它被“拉長”之后由于其內部組件之間的間隙變大,會顯得很難看。所以應該為他們設定一個最合適的顯示大小。在居中顯示的時候只調整位置而不改變大小,這樣就不會影響窗口的美觀。所以我們只需要對上面的代碼小改一下即可,以JFrame為例:

Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

screenSize = Toolkit.getDefaultToolkit().getScreenSize();

frame.setPreferredSize(new Dimension(512,450));           

int frameWidth = this.getPreferredSize().width;

int frameHeight = this.getPreferredSize().height;

frame.setSize(frameWidth, frameHeight);

frame.setLocation((screenSize.width - frameWidth) / 2,(screenSize.height - frameHeight) / 2);

2       自定義JFrame的關閉事件

有的時候,當用戶關閉應用程序窗口的時候,我們可能希望程序在結束之前保存一些必要的數據。對于這種需求,我們有兩種備選方案:

2.1    獲取程序關閉的“鉤子”

Runtime.getRuntime().addShutdownHook(shutdownHook);

protected Thread shutdownHook = new PlatformShutdownHook();   

    protected class PlatformShutdownHook extends Thread {

        public void run()

        {

            //一些清理工作在這里進行……

        }

}

通過這種方法,我們就可以在程序結束時獲得通知,以便進行一些保存或清理的工作。然而這種方法的缺點是,在程序收到結束通知的時候,所有的UI組件已經被銷毀了,用戶此時看到的是程序已經結束。而事實上如果程序保存需要花很長的時間的話,用戶是不能獲取任何信息的,這是一個很糟糕的用戶體驗。因為如果這時用戶關機的話,程序就有可能丟失尚未保存的信息,而對于這一切,用戶并不知情。

2.2    處理JFrame關閉事件

為了在UI被銷毀之前收到程序結束的消息,我們需要自行處理窗口關閉的事件。注意在這里我們沒有采用addActionListener(……)方法,因為這樣做只能讓我們在窗口關閉之后收到通知,這樣就與上面的方法沒什么區別了。

我們需要在JFrame的構造函數中設置:

//設定標志,讓MainFrame自己接收窗口事件

enableEvents(AWTEvent.WINDOW_EVENT_MASK);

然后再實現下面的函數:

protected void processWindowEvent(final WindowEvent pEvent) {

        if (pEvent.getID() == WindowEvent.WINDOW_CLOSING) {

            /** 防止用戶多次點擊關閉按鈕造成重復保存 **/

            if( !isClosing ) isClosing = true;

            else return;

 

            //處理JFrame關閉事件……

        }else{

            //忽略其他事件,交給JFrame處理

            super.processWindowEvent(pEvent);

        }

}

如此一來,我們就可以在窗口被關閉之前通知用戶程序正在保存數據的信息,例如后面提到的InfiniteProgressPanel可以顯示的內容。

3       日期選擇組件與JDialog的沖突問題

由于很多應用程序都需要用戶輸入日期,卻又怕用戶輸入的日期格式錯誤,所以日期選擇組件便應運而生。雖然我們很需要它,但是網上絕大多數的組件都是需要給錢的。在找到SwingX之前,我找到的唯一能夠免費使用的日歷組件就是一個名為DateChooserJDialog

看樣子很不錯,它支持中文,對于今天高亮顯示,可以調整年分和月份……一切都非常符合要求。但是這么好的組件卻不能用在我的程序里,原因是在我的程序中,調用這個組件的組件也是一個JDialog,并且設置了setAlwaysOnTop(true)—即總在最前端顯示。由于DateChooser也設定了在最前端顯示,這就導致了它和其父組件的顯示沖突,最終結果是DateChooser不能正常顯示。對于這個問題,我最終使用SwingX的組件DatePicker來代替DateChooser完成選擇日期的使命,慣于DatePicker的使用我將來會在“SwingX使用詳解”中提到,這里就不再細說。但是這個問題仍然值得我們注意,即如果一個窗口組件是設置了總在最前端顯示的JDialog,那么就不要以這個JDialog為父組件來彈出其他JDialog,以避免沖突的發生。

4       JTable的實用技巧

無論對于什么樣的一個應用程序來說,用表格的形式來顯示數據是再平常不過的事情了。于是JTable就成為我們在所有Swing組件中最不可或缺的朋友。對于JTable的操作,大多數情況下我們都可以不假外求,因為JDK自帶的例子SwingSet2給我們展示了足夠多的功能。

在這個例子里,我們可以改變單元格的間距,行高,選擇類型(Selection Style),是否顯示水平線,甚至可以將表格內容打印出來。其中,表格除了文字之外還可以包含其他組件和內容,如SwingSet2種就加入了可以選擇顏色的JComboBox和喜愛的食物所代表的圖片。

但有些時候,我們還會有一些其他的需求。例如說為了保護我們的眼睛,我們希望表格的內容是帶有間隔色的,如奇數行顯示藍色,而偶數行顯示白色。又或者我們希望表格中某些列的內容是可編輯的,而且他列的內容是不可編輯的。又或者讓表格中的列帶有排序的功能,能讓我們點一下表頭它就自己按照從低到高或從高到低的順序自行排列。最后我們希望表格的表頭和單元格力的內容能夠居中顯示。讓我們一個一個來實現這些功能!

4.1    間隔色表格及單元格/表頭居中顯示

JTableAPI并沒有為我們提供更改表格行或列的顏色的能力。但是我們知道,表格的表頭和內容的呈現形式都是由相應的Renderer來控制的,所以我們只需要繼承單元格默認的Renderer并作相應的修改就可以達到目的:

由于實現了接口TableCellRenderer,我們只需要實現唯一的函數getTableCellRendererComponent(…)。在上例中我們看到,在函數中我們判斷當前行是奇數還是偶數,如果是奇數,就設置其背景色為淡藍色,否則就設其背景色為白色。在每次更新表格內容的時候,我們只需要調用下面的函數,就可以保證表格在內容被更改之后依然正確顯示間隔色。

/** 為所有表格設置間隔色 **/

    private void setRenderColor(){

for( int i = 0; i < table.getColumnModel().getColumnCount(); i++ )                   table.getColumn( colname[i] ).setCellRenderer(colorRender );       

}

另外,如果我們想要讓單元格中的內容居中顯示的話,請注意到在設置間隔色部分下面的函數,通過setHorizontalAlignment(SwingConstants.CENTER)我們就可以讓單元格內容居中顯示。

雖然JTable表格的表頭在默認情況下應該是居中顯示的,但不知道為什么,在我的應用程序中表格的表頭總是左對齊顯示,這讓我惱火不已。由于和單元格一樣,表頭的各項顯示指標也是由其Renderer控制的,所以只需要設置一下表頭的Renderer就能達到目的:

DefaultTableCellRenderer renderer = (DefaultTableCellRenderer) table.getTableHeader().getDefaultRenderer();

renderer.setHorizontalAlignment(renderer.CENTER);

利用這種方法,如果我們需要讓他右對齊似乎也不是什么難事,對嗎?

4.2    讓某些單元格不可編輯

有些時候,我們希望有些行/列可以被編輯,而有些行/列不能被編輯。如下就是一例,我的程序希望第一列(編號列)的內容可以被用戶通過雙擊進行編輯,而其他列則不能被用戶編輯。單元格能否被編輯取決于JTableisCellEditable(int row,int column)。如果該函數返回true(row,column)所代表的單元格可以被編輯,否則該單元格不能被編輯。于是我建立了一個名為SingleUnitEditableTable的類,他繼承自JTable,并OverwriteisCellEditable(int row,int column)方法:

//設置單元格不可編輯,為缺省實現

    public boolean isCellEditable(int row, int column) {

        if( editableColumn != -1){

            if( column == editableColumn )

                return true;

            return false;

        }

        return false;

}

其中的editableColumn是一個內部屬性,用來指定哪個列可以被編輯。通過這個例子,我相信,如果你想實現奇數行/列可編輯而偶數行/列不可被編輯或者滿足特定條件的單元格不可被編輯這樣的JTable易如反掌了吧?下面就是我的應用程序的結果:

第一列可編輯

其他列均不可編輯

4.3    JTable自排序

這個問題已經由JDK6.0幫我們解決了,在這個版本,JDK為我們提供了一個名為TableRowSorter的類,在程序中我們只需要寫2行代碼即可實現表格內容的排序:

TableRowSorter sorter = new TableRowSorter(tableModel);

       accAllTable.setRowSorter(sorter);

看到“編號”列旁邊的箭頭了嗎?如果我們用鼠標點擊表頭,JTable就會自動為我們由小到大排序,再點一下,表格就會從大到小排序,真是十分方便。而對于JDK6.0之前的應用程序就沒有這么好的運氣了,我們需要自己實現一個TableRowSorter,并且自己生成一個表頭的Renderer來實現排序小箭頭,真是繁瑣啊!我這里倒是有一個不錯的實現,如果有人需要的話可以給我留言。不過自己實現Renderer采用的是JLabel,會改變表頭的模樣,不如默認的表頭好看,所以可能的話還是升級吧J

5       JEditorPane顯示HTML描述的文本

JDK1.4開始,Swing的很多組件(如JLabel)都可以顯示HTML語言寫的文本。這是一個巨大的進步,因為我們可以將所要顯示的文字的配置信息如字體,字號,顏色,換行等信息直接以HTML寫入到組件的setText()方法當中,不但免去了事后對這些信息進行繁雜配置的煩惱,而且還豐富和簡化了所要顯示文本的形式。而JEditorPane則有所不同,它天生就是用來分析并顯示格式化文本的,由一些Java寫的開源Web瀏覽器甚至都采用改進后的JEditorPane作為Web頁的顯示器。下圖就是SwingSet2中的JEditorPane相關的例子。我們可以看到JEditorPane可以顯示大多數的HTML元素,包括圖片,格式化文字,URL鏈接等。

然而通過JEditorPane顯示HTML描述的文本有兩種方式:

第一種是直接使用JEditorPane.setPage(String htmlTxt);來顯示用html語言寫成的文本。但是這種方法的缺點是無法顯示HTML文本中所描述的對外部資源(如圖片,CSS等)的引用。所以如果要顯示更為豐富的信息,僅僅用第一種方法是不夠的。

       所以第二種方法就呼之欲出:將用HTML語言描述的動態文本信息寫到文件中,使之成為真正的HTML文件,再用JEditorPane.setPage(URL)JEditorPane.setPage(String htmlFilePath)JEditorPane方法讀入這個動態生成的內容文件就可以讓JEditorPane自動為我們顯示豐富的信息了。

   String vNewReportFileName = "file:///c:/temp.html";

   JEditorPane reportPane = new JEditorPane();

   File f = new File(FileUtil.reportDir,vNewReportFileName);

 FileWriter fw = new FileWriter(f, false);

   fw.write("<html>");

 fw.write("<head>");

 …………

  fw.write("</body></html>");

//清理操作

  fw.flush();

  fw.close();

  f = null;

reportPane.setPage(vNewReportFileName);

下圖就是我的程序所顯示的結果,從圖中我們可以清楚地看到由CSS文件定義的表格的Title,這個Title是由一個藍色的圖片作為背景的。

讓人遺憾的是用JEditorPane顯示的表格的邊框都很粗,雖然我已經將了表格的border設置為1,可是JEditorPane依然我行我素。但是在IE下,表格的邊框的表現就要好的多:

網上有人說這是一個Bug,但是沒有人給過解決這個問題的方法,如果有人又解決方法的話請留言,我將不勝感激!

6       InfiniteProgressPanel實現GlassPane

俗話說重要人物都最后出場,作為Swing篇的完結部分,我為大家隆重推薦一個GlassPane的實現—InfiniteProgressPanel,它的效果如圖所示:

怎么樣,很酷吧?這是在程序進行更新的時候能夠給用戶以提示,可以屏蔽用戶操作而且十分美觀的特殊進度條。它源于一個超級Java大牛的手筆,此君的《Swing Hacker》在去年如帶給我的震撼到現在還揮之不去。從那以后,誰再敢說Java不能做出好看的用戶界面之前都需要自己好好掂量一下自己是否有這么說的資格。這本書讓我真正認識到,只有想不到沒有做不到。都是一樣用Swing,為啥人家就能玩出花樣呢?差距!

其實現原理很簡單,說白了就是用Java2D畫圈!至于源碼各位可以到網上自己搜。他的使用十分簡單:

InfiniteProgressPanel glassPane = new InfiniteProgressPanel();

frame.setGlassPane(glassPane)

在需要它顯示的時候,就這樣做:

 Thread myThread = new Thread(new Runnable(){

         public void run() {

              InfiniteProgressPanel gl = thisRef.glassPane;

              gl.start();

              gl.setText("正在保存數據請稍候....");

              try {

                 //這里放要做的事情……

                 gl.setText("保存完畢,歡迎使用!");

                 Thread.sleep(1000);

               }catch(InterruptedException ex) {

               }finally{

                 gl.stop();

               }

            }

    });

myThread.start();       

這里有幾個問題需要注意:

1.         必須要將InfiniteProgressPanel的顯示放到一個線程里,相信大家都知道原因,我不用多說。

2.         InfiniteProgressPanel結束之前的Thread.sleep(1000);是必要的,如果時間設得太短或不設將會導致InfiniteProgressPanel死掉。至于原因我沒有時間深究,各位有興趣可以自行察看其源碼,如果你能找到原因高訴我,我會非常感激。

3.         在有些時候會出現圓圈“四處亂竄”的現象,不過不太常見。