前言
本文前言部分为我的一些感想,假设你仅仅对本文介绍的Java有用技巧感兴趣,能够跳过前言直接看正文的内容。
本文的写作动机来源于近期接给人家帮忙写的一个小程序,主要用于管理分期付款的货款的一系列管理,包含过期款的纪录,过期款利息的计算,为提前付款的用户提供一些返款奖励等等,这些与本文无关自不必细说。为了尽快完毕任务,我自然选择了我用得最多的Java来实现。经过2周的劳动,顺利完毕了任务,明天就能够去交差,可是这一刻我却忽然有些其它的想法。诚然这种活原本属于体力劳动,相似的活我也做过不止一次,对于非常多高人来说,没什么值得一提的,曾经我也仅仅是交差收钱了事,但这一次我却多了一些想法,使我不吐不快。
在程序的实现过程中,我遇到了个小问题,就是计算两个日期的差。因为曾经经常使用的Date类的大多数方法都被标记为“deprecate”,所以我决定用Calender作为计算日期的主力。可是大多数參考书上都是由关于Calender的日期格式,Locale的设置,常量的含义等方面的解说,却怎么也找不到这样一个简单却经常使用的任务怎么实现(注:这也不能怪我懒惰,作为这样一个程序来说,假设有正确且成熟的方法,谁还会去花大量时间细致研究API呢?反正这个类可能在今后的几个月甚至几年都用不上,如今记住到时候也都忘了L)。于是在我google了好一阵之后,最终在某人的Blog上找到了用Calender计算日期差的方法。在那一刻我真有久旱逢甘雨之感。博主可能是一时兴起,也有可能是兴趣所在,但不管是什么原因,他的工作都为我提供了非常大的方便。有了他的代码演示样例,我能够不再去逐个查找Java-Doc里面的API,然后挑出几个来尝试解决这个问题,最后再写个demo验证这一繁复的过程了。
再回忆一下我完毕这个程序的过程,因为曾经做过一些相似的程序,我能够将里面的非常多部分以直接应用到这个程序中,节省了大量的时间,让我能够更专注于核心业务的实现其中。然而也许是出于懒惰,也许是没有时间,又也许原来的是Blog没有多少人关注,我都没有将这些大多数人都可能会用得上的东西放到网上。
再联想一下国外开源工作者对中国程序猿的评价—“仅仅获取,不贡献”,就认为人家说得十分对。自己就用着免费的J2SDK语言,免费的Eclipse,免费的JFreeChart,免费的JasperReport……,却从来没可以给人家贡献哪怕一行代码。这样也就算了,可是相似于一些力所能及的东西,比如可能每一个Java程序猿都会碰到的一些小问题,小技巧,经常出现的错误,为什么我就不能把他们贴出来供人分享呢?说不定就会帮到某位哥们解决大问题,更有可能你的几句话就能节省别人几分钟甚至几小时的时间。假设每一个人都能在业余时间把自己的一些心得体会贴出来,相信很多其它的人将因此受益。当你遇到问题的时候,才干心安理得的去Google或Baidu。相信这也是技术论坛和技术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) {
/** 防止用户多次点击“关闭”button造成反复保存 **/
if( !isClosing ) isClosing = true;
else return;
//处理JFrame关闭事件……
}else{
//忽略其它事件,交给JFrame处理
super.processWindowEvent(pEvent);
}
}
如此一来,我们就能够在窗体被关闭之前通知用户程序正在保存数据的信息,比如后面提到的InfiniteProgressPanel能够显示的内容。
3 日期选择组件与JDialog的冲突问题
因为非常多应用程序都须要用户输入日期,却又怕用户输入的日期格式错误,所以日期选择组件便应运而生。尽管我们非常须要它,可是网上绝大多数的组件都是须要给钱的。在找到SwingX之前,我找到的唯一可以免费使用的日历组件就是一个名为DateChooser的JDialog:
看样子非常不错,它支持中文,对于今天高亮显示,能够调整年分和月份……一切都非常符合要求。可是这么好的组件却不能用在我的程序里,原因是在我的程序中,调用这个组件的组件也是一个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 间隔色表格及单元格/表头居中显示
JTable的API并没有为我们提供更改表格行或列的颜色的能力。可是我们知道,表格的表头和内容的呈现形式都是由对应的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 让某些单元格不可编辑
有些时候,我们希望有些行/列能够被编辑,而有些行/列不能被编辑。例如以下就是一例,我的程序希望第一列(编号列)的内容能够被用户通过双击进行编辑,而其它列则不能被用户编辑。单元格是否能被编辑取决于JTable的isCellEditable(int row,int column)。假设该函数返回true则(row,column)所代表的单元格能够被编辑,否则该单元格不能被编辑。于是我建立了一个名为SingleUnitEditableTable的类,他继承自JTable,并Overwrite了isCellEditable(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,为啥人家就能玩出花样呢?差距!
事实上现原理非常easy,说白了就是用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. 在有些时候会出现圆圈“四处乱窜”的现象,只是不太常见。