@lambeta
2016-09-20T03:36:12.000000Z
字数 20226
阅读 257
translation
Swing是一个平台独立,基于模型-视图-控制器的GUI工具集,它被用来创建Java应用程序的图形界面。在本附录中,我首先会探索Swing的线程架构,之后会探索Swing的API,这些API用于防止在图形上下文中使用额外线程导致的问题。最后,我给出一个基于Swing的幻灯片应用程序作为本附录内容的重要例子,同时用这种好玩的方式结束本书。
注意 我会假设你对Swing的API及其应用程序的架构有一定的经验。
Swing遵循单线程编程模型。它被设计成单线程而非多线程的原因在于设计多线程图形工具集的经验已经表明多线程必然会导致死锁和竞态条件。如果想了解更多相关问题,可以查看“为何GUIs是单线程的?”博客(http://codeidol.com/java/java-concurrency/GUI-Applications/Why-are-GUIs-Single-threaded/)
用于渲染图像和处理事件的线程被称为事件-分派(event-dispatch)线程(EDT)。EDT处理来自底层抽象窗口工具集的事件队列中事件,同时调用GUI组件(例如按钮)的事件监听器,这些监听器在此线程上处理事件。组件甚至会在EDT上重绘它们自身(响应paint()方法调用时会导致paintComponent()、paintBorder()、paintChildren()方法被调用)。
一定得小心关注你的代码如何同EDT交互,确保Swing程序能正确工作。有两条原则需要记住:
单线程的Swing导致的一个结果就是你只能在EDT上创建Swing应用程序的GUI。在其它线程中创建GUI是不对的,包括用于运行Java应用程序中main()方法的默认主线程。
大部分Swing对象(如javax.swing.JFrame对象,描述了GUI中拥有菜单条和边界的顶级“框架”窗口)都不是线程安全的。从多条线程中访问这些对象会招致线程干涉的风险或者内存不一致的错误:
线程干涉:当两条线程作用在相同的数据上,执行了不同的操作。举个例子,一条线程在另一条线程更新基于长整型的计数变量时,读取此变量。由于长整型在32位的机器上读取和写入操作需要两次读写访问,很可能读线程读取了该变量部分当前值,然后写线程更新了这个变量,再然后读线程读完了该变量的剩余部分。结果就是读线程读到了不正确的值。
内存不一致错误:运作在不同的处理器或者核心的两条及其以上线程拥有相同数据的非一致视图。举个例子,某个处理器或者核心上的一条写线程更新了一个计数变量,之后另一个处理器或者核心上的读线程读取了该变量。不过,由于存在用于提升性能的缓存机制,线程不会去访问主存当中单一的变量拷贝,取而代之地,每条线程都会从它们自己的本地存储(缓存)中访问这个变量的拷贝。
如果GUI不是在EDT之上创建的会发生什么问题呢?John Zukowski在JavaWorld上发表的《SWing线程化以及事件分派线程》一文中给出了一个场景。(www.javaworld.com/article/2077754/core-java/swing-threading-and-the-event-dispatch-thread.html)。
Zukowski展示了一个例子,往frame窗口容器组件中添加一个容器的监听器。当组件从frame当中添加或者移除时,监听器的方法就会被调用。他演示了默认主线程意识到frame窗口之前,EDT在监听器方法中已经开始运行代码。
注意 能被感知到意味着一个组件的paint()方法要么已经被调用,要么可能被调用。frame窗口中setVisible(true)、show()和pack()其中任意一个方法在该容器组件上被调用,这个frame窗口就被感知到了。在frame窗口被感知之后,所有它包含的组件也都被感知到了。另外一种感知组件的方式就是把它添加到已经被感知的容器当中。
EDT在一个监听器方法中开始运行之后,如果默认主线程继续初始化GUI,组件可能会被默认主线程创建并被EDT访问。EDT可能试图在这些组件存在之前访问它们。这样做可能会使应用程序崩溃。
即便在EDT从监听器方法中访问这些组件之前,默认主线程创建了它们,EDT还是可能看到不一致的视图(由于缓存)而无法访问指向新组件的引用。应用程序崩溃(可能抛出java.lang.NullPointerException对象)十有八九会发生。
清单B-1展示了ViewPage的源码,即一个浏览web页面HTML的Swing应用程序。这个应用程序就遭受了以上两个问题。
清单B-1 一个有问题的Swing应用,用于浏览Web页面HTML
import java.awt.BorderLayout;import java.awt.Dimension;import java.awt.EventQueue;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.io.InputStream;import java.io.IOException;import java.net.URL;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;public class ViewPage{public static void main(String[] args){final JFrame frame = new JFrame("View Page");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JPanel panel = new JPanel();panel.add(new JLabel("Enter URL"));final JTextField txtURL = new JTextField(40);panel.add(txtURL);frame.getContentPane().add(panel, BorderLayout.NORTH);final JTextArea txtHTML = new JTextArea(10, 40);frame.getContentPane().add(new JScrollPane (txtHTML),BorderLayout.CENTER);ActionListener al = (ae) ->{InputStream is = null;try{URL url = new URL(txtURL.getText());is = url.openStream();StringBuilder sb = new StringBuilder();int b;while ((b = is.read()) != -1)sb.append((char) b);txtHTML.setText(sb.toString());}catch (IOException ioe){txtHTML.setText(ioe.getMessage());}finally{txtHTML.setCaretPosition(0);if (is != null)try{is.close();}catch (IOException ioe){}}};txtURL.addActionListener(al);frame.pack();frame.setVisible(true);}}
清单B-1中的main()方法创建了一个GUI,它由一个用于输入web页面URL的文本框以及一个用于显示页面HTML的可滚动文本区域组成。在输入URL之后按下回车键,触发ViewPage获取并显示HTML。
照下面编译清单B-1:
javac ViewPage.java
运行程序:
java ViewPage
你应该能观测到如图B-1展示的GUI(由一个样例URL和部分结果web页面的HTML构成)。
[图]
图B-1. 文本框输入一个URL并且在可滚动文本区域浏览web页面输出
该应用程序的第一个问题是GUI在默认的主线程被创建出来而非EDT。尽管再运行ViewPage时你可能不会遇到问题,但是依然会有潜在的线程干涉以及内存不一致问题。
第二个问题是那个运行动作监听器的EDT,它用于响应文本框之上按下回车的操作。针对这个URL打开一个input stream及读取其内容到一个string builder中的代码会推迟EDT。GUI在此期间不会响应。
Swing提供了一系列的API用于克服前面提及的EDT的问题。本章节中,我会介绍这些API。同时,我也会介绍Swing版本的定时器,它和第4章中展示的定时器框架有很大不同。
类javax.swing.SwingUtilities提供了一组在Swing上下文中很有用的静态方法。其中有三个对于使用EDT和避免提及的问题很管用:
void invokeAndWait(Runnable doRun):使得doRun.run()方法在EDT中同步地执行。这个调用块会一直阻塞直到所有的等待事件都已经被处理完,之后doRun.run()方法返回。当该方法在等待EDT执行完doRun.run()时被中断了,invokeAndWait()方法会抛出java.lang.InterruptedException。当有异常从doRun.run()方法中抛出时,该方法会抛出java.lang.reflect.InvocationTargetException。invokeAndWait()方法应该在一个应用程序线程需要从其它有别于EDT的线程中更新GUI的情况下被使用。它不应该从EDT中被调用。
void invokeLater(Runnable doRun):在所有等待事件都已经被处理完之后,使得doRun.run()方法将在EDT上同步执行。invokeLater()方法应该在一个应用程序线程需要更新GUI时才被使用。它可以从任意线程中被调用。
boolean isEventDispatchThread():当调用线程是EDT时,返回true;否则,返回false。
invokeAndWait()、invokeLater()以及isEventDispatchThread()方法就是调用类java.awt.EventQueue中同等方法的包装器。尽管你给这些方法加上SwingUtilities的前缀,而我则使用EventQueue作为前缀(出于习惯)。
你通常会根据以下模式使用invokeLater()方法构建一个Swing的GUI:
Runnable r = ... // ... refers to the runnable's anonymous class or lambdaEventQueue.invokeLater(r);
清单B-2展示了第二版ViewPage的源代码,它使用了invokeLater()方法在EDT之上构造了Swing GUI。
清单B-2 在EDT之上构造HTML查看器的Swing应用程序的GUI
import java.awt.BorderLayout;import java.awt.Dimension;import java.awt.EventQueue;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.io.InputStream;import java.io.IOException;import java.net.URL;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;public class ViewPage{public static void main(String[] args){Runnable r = () ->{final JFrame frame = new JFrame("View Page");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JPanel panel = new JPanel();panel.add(new JLabel("Enter URL"));final JTextField txtURL = new JTextField(40);panel.add(txtURL);frame.getContentPane().add(panel, BorderLayout.NORTH);final JTextArea txtHTML = new JTextArea(10, 40);frame.getContentPane().add(new JScrollPane (txtHTML),BorderLayout.CENTER);ActionListener al = (ae) ->{InputStream is = null;try{URL url = new URL(txtURL.getText());is = url.openStream();StringBuilder sb = new StringBuilder();int b;while ((b = is.read()) != -1)sb.append((char) b);txtHTML.setText(sb.toString());}catch (IOException ioe){txtHTML.setText(ioe.getMessage());}finally{txtHTML.setCaretPosition(0);if (is != null)try{is.close();}catch (IOException ioe){}}};txtURL.addActionListener(al);frame.pack();frame.setVisible(true);};EventQueue.invokeLater(r);}}
清单B-2解决了一个问题,但是我们依旧得防止EDT被延迟。我们可以通过在EDT之上创建一条工作线程去读取页面,而后使用invokeAndWait()方法把页面内容更新到滚动的文本区域来解决这一问题。
清单B-3 在非延迟的EDT之上构造HTML查看器的Swing应用程序的GUI
import java.awt.BorderLayout;import java.awt.Dimension;import java.awt.EventQueue;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.io.InputStream;import java.io.IOException;import java.lang.reflect.InvocationTargetException;import java.net.URL;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;public class ViewPage{public static void main(String[] args){Runnable r = () ->{final JFrame frame = new JFrame("View Page");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JPanel panel = new JPanel();panel.add(new JLabel("Enter URL"));final JTextField txtURL = new JTextField(40);panel.add(txtURL);frame.getContentPane().add(panel, BorderLayout.NORTH);final JTextArea txtHTML = new JTextArea(10, 40);frame.getContentPane().add(new JScrollPane (txtHTML),BorderLayout.CENTER);ActionListener al = (ae) ->{txtURL.setEnabled(false);Runnable worker = () ->{InputStream is = null;try{URL url = new URL(txtURL.getText());is = url.openStream();final StringBuilder sb = new StringBuilder();int b;while ((b = is.read()) != -1)sb.append((char) b);Runnable r1 = () ->{txtHTML.setText(sb.toString());txtURL.setEnabled(true);};try{EventQueue.invokeAndWait(r1); }catch (InterruptedException ie){}catch (InvocationTargetException ite){}}catch (final IOException ioe){Runnable r1 = () ->{txtHTML.setText(ioe.getMessage());txtURL.setEnabled(true);};try {EventQueue.invokeAndWait(r1); }catch (InterruptedException ie){}catch (InvocationTargetException ite){}}finally{Runnable r1 = () ->{txtHTML.setCaretPosition(0);txtURL.setEnabled(true);};try{EventQueue.invokeAndWait(r1); }catch (InterruptedException ie){}catch (InvocationTargetException ite){}if (is != null)try{is.close();}catch (IOException ioe){}}};new Thread(worker).start();};txtURL.addActionListener(al);frame.pack();frame.setVisible(true);};EventQueue.invokeLater(r);}}
我在获取页面时禁用文本框而后再启用。而你仍然可以在任意时刻关闭GUI。
尽管清单B-3解决了GUI无响应的问题,但是解决方案还是稍微冗余了点。幸运的是,这儿有替代方案。
Swing提供了类javax.swing.SwingWorker以较少的冗余来容纳长时间运行的任务(如读取URL的内容)。你必须继承这个抽象类并重写一个或多个方法来有效地完成工作。
SwingWorker的泛型类型是SwingWorker<T, V>。参数T和V区分了最终以及中间任务的结果类型。
你可以重写protected abstract T doInBackground()方法在一条工作线程中执行长时间的任务并且返回一个T类型的结果(当没有结果时,返回Void类型)。当该方法结束,protected void done()方法就会在EDT上被调用。默认情况下,该方法不做任何事。不过,你也可以重写done()方法来安全地更新GUI。
当任务在运行时,你可以通过调用protected void publish(V... chunks)方法周期性地将结果发布到EDT中。这些结果会被运行在EDT中重写过的protected void process(List<V> chunks)方法获取到。如果没有中间结果需要处理,你可以为V指定Void类型(或者不使用publish()及process()方法)。
SwingWorker有两个及以上的方法需要你了解。首先,void execute()方法在一条工作线程调度被调用的SwingWorker对象。其次,T get()方法在需要时会等待doInBackground()方法完成,而后返回最终结果。
注意 尝试获取从doInBackground()方法中获取对象过程中如果发生异常,那么SwingWorker的get()方法就会抛出类java.util.concurrent.ExecutionException的实例。它也可以抛出InterruptedException。
清单B-4展示了最终ViewPage应用程序的源码,它使用SwingWorker取代了invokeAndWait()方法。
清单B-4 重新在非延迟的EDT之上构造HTML查看器的Swing应用程序的GUI
import java.awt.BorderLayout;import java.awt.Dimension;import java.awt.EventQueue;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.io.InputStream;import java.io.IOException;import java.net.URL;import java.util.concurrent.ExecutionException;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;import javax.swing.SwingWorker;public class ViewPage{public static void main(String[] args){Runnable r = () ->{final JFrame frame = new JFrame("View Page");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JPanel panel = new JPanel();panel.add(new JLabel("Enter URL"));final JTextField txtURL = new JTextField(40);panel.add(txtURL);frame.getContentPane().add(panel, BorderLayout.NORTH);final JTextArea txtHTML = new JTextArea(10, 40);frame.getContentPane().add(new JScrollPane (txtHTML),BorderLayout.CENTER);ActionListener al = (ae) ->{txtURL.setEnabled(false);class GetHTML extends SwingWorker<StringBuilder, Void>{private final String url;GetHTML(String url){this.url = url;}@Overridepublic StringBuilder doInBackground(){StringBuilder sb = new StringBuilder();InputStream is = null;try{URL url = new URL(this.url);is = url.openStream();int b;while ((b = is.read()) != -1)sb.append((char) b);return sb;}catch (IOException ioe){sb.setLength(0);sb.append(ioe.getMessage());return sb;}finally{if (is != null)try{is.close();}catch (IOException ioe){}}}@Overridepublic void done(){try{StringBuilder sb = get();txtHTML.setText(sb.toString());txtHTML.setCaretPosition(0);}catch (ExecutionException ee){txtHTML.setText(ee.getMessage());}catch (InterruptedException ie){txtHTML.setText("Interrupted");}txtURL.setEnabled(true);}}new GetHTML(txtURL.getText()).execute();};txtURL.addActionListener(al);frame.pack();frame.setVisible(true);};EventQueue.invokeLater(r);}}
最终版本的ViewPage依赖于GetHTML这个声明在动作监听器lambda表达式中的本地SwingWorker子类,用于在工作线程中读取web页面(保持用户持续响应),然后用HTML更新EDT(Swing的代码必须在这里执行)之上的用户界面。
当lambda表达式运行时(用户在文本框中输入一个URL之后按下回车键),它用文本框中的文本初始化了GetHTML类(由于Swing是单线程的,所以无法从工作线程中访问文本框)并且调用了SwingWorker的execute()方法。
execute()方法导致GetHTML中重写的doInBackground()方法在工作线程中被调用,这会产生并返回一个包裹HTML或者错误文本的java.lang.StringBuilder对象。EDT之后会调用重写的done()方法,该方法会调用SwingWorker的get()方法访问这个StringBuilder对象并且把内容输出到文本区域。
Swing提供了类javax.swing.Timer(很像一个简化版的定时器框架——请见第4章)在EDT上周期性地执行Swing的代码。它会在一个初始的延时之后触发一个动作事件到注册过的监听器当中,并且此后以固定的事件间隔重复触发。
调用构造函数Timer(int delay, ActionListener listener)创建一个有初始延时以及事件间隔延时的定时器,并且每隔delay毫秒,事件就会被发送到初始的动作监听器(可能为空)中。
delay这个参数值会被用于初始延时以及固定的事件间隔。当然你也可以分别将这些值通过void setInitialDelay(int initialDelay)和void setDelay(int delay)方法设置进去。
注意 用false作为参数调用Timer的void setRepeats(boolean flag)方法会导致该定时器仅发送一条动作事件。
调用void addActionListener(ActionListener listener)方法可以添加另外一个动作监听器,而调用void removeActionListener(ActionListener listener)方法则移除了之前注册的动作监听器。调用ActionListener[] getActionListeners()方法会获取所有注册的监听器。
新创建的定时器处于停止状态。为了启动这个定时器,需要调用其void start()方法。相对地,你需要调用void stop()方法来终止这个定时器。你或许也想要调用boolean isRunning()方法来确定这个定时器是否正在运行。
清单B-5展示了一个计数器应用程序的源码,它会创建一个定时器来持续地在一个标签(label)上显示跳动的计数。
清单B-5 开始和停止计数
import java.awt.EventQueue;import java.awt.FlowLayout;import java.awt.event.ActionListener;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.Timer;public class Counter extends JFrame{int count;public Counter(String title){super(title);setDefaultCloseOperation(EXIT_ON_CLOSE);JPanel pnl = new JPanel();((FlowLayout) pnl.getLayout()).setHgap(20);final JLabel lblCount = new JLabel("");pnl.add(lblCount);final JButton btnStartStop = new JButton("Start");ActionListener al = (ae) ->{++count;lblCount.setText(count + " ");};final Timer timer = new Timer(30, al);al = (ae) ->{if (btnStartStop.getText().equals("Start")){btnStartStop.setText("Stop");timer.start();}else{btnStartStop.setText("Start");timer.stop();}};btnStartStop.addActionListener(al);pnl.add(btnStartStop);setContentPane(pnl);setSize(300, 80);setVisible(true);}public static void main(String[] args){EventQueue.invokeLater(() -> new Counter("Counter"));}}
清单B-5中main()方法创建一个由标签和开始、停止按钮构成的GUI。这个标签展示count变量的当前值而按钮上的文字则在开始和停止之间切换。当按钮显示开始时,点击会触发定时器启动;而按钮显示停止时,点击会导致定时器停止。定时器的动作监听器递增count变量并将值显示到标签上。加在count变量之后的空格字符串会把表达式转换成一个字符串从而保证不会截断最右边的像素。
照下面编译清单B-5:
javac Counter.java
运行程序:
java Counter
图B-2展示了结果GUI。
[图]
图B-2. 面板中的组件水平居中
幻灯片展示就是在投影仪屏幕之上一组静态图片的演示,通常会以预先安排好的顺序播放。每张图片一般都会至少显示几秒而后被下一张图片替换。
幻灯片的展示会涉及一个投影仪、一个屏幕和一组幻灯片。投影仪含有将被投影的幻灯片,屏幕用于展示被投影的幻灯片,而幻灯片则包含一张图片和其它属性(例如一个文本的标题)。
我创建了一个名为SlideShow的Java应用程序,它可以让你投影任意的幻灯片。清单B-6是它的源码。
清单B-6 描述一个基于定时器的幻灯片展示
import javax.imageio.ImageIO;import javax.swing.*;import java.awt.*;import java.awt.event.ActionListener;import java.awt.event.WindowAdapter;import java.awt.event.WindowEvent;import java.awt.image.BufferedImage;import java.io.BufferedReader;import java.io.File;import java.io.FileReader;import java.io.IOException;import java.util.ArrayList;import java.util.List;class Projector {private volatile List<Slide> slides;private Screen s;private Timer t;private volatile int slideIndexC, slideIndexN;private volatile float weight;Projector(List<Slide> slides, Screen s) {this.slides = slides;this.s = s;t = new Timer(1500, null);t.setDelay(3000);slideIndexC = 0;slideIndexN = 1;}void start() {s.drawImage(Slide.blend(slides.get(0), null, 1.0f));ActionListener al = (ae) ->{weight = 1.0f;Timer t2 = new Timer(0, null);t2.setDelay(10);ActionListener al2 = (ae2) ->{Slide slideC = slides.get(slideIndexC);Slide slideN = slides.get(slideIndexN);BufferedImage bi = Slide.blend(slideC, slideN, weight);s.drawImage(bi);weight -= 0.01f;if (weight <= 0.0f) {t2.stop();slideIndexC = slideIndexN;slideIndexN = (slideIndexN + 1) % slides.size();}};t2.addActionListener(al2);t2.start();};t.addActionListener(al);t.start();}void stop() {t.stop();}}class Screen extends JComponent {private Dimension d;private BufferedImage bi;private String text;Screen(int width, int height) {d = new Dimension(width, height);}void drawImage(BufferedImage bi) {this.bi = bi;repaint();}@Overridepublic Dimension getPreferredSize() {return d;}@Overridepublic void paint(Graphics g) {int w = getWidth();int h = getHeight();g.drawImage(bi, Slide.WIDTH <= w ? (w - Slide.WIDTH) / 2 : 0,Slide.HEIGHT <= h ? (h - Slide.HEIGHT) / 2 : 0, null);}}class Slide {static int WIDTH, HEIGHT;private static int TEXTBOX_WIDTH, TEXTBOX_HEIGHT, TEXTBOX_X, TEXTBOX_Y;private BufferedImage bi;private String text;private static Font font;private Slide(BufferedImage bi, String text) {this.bi = bi;this.text = text;font = new Font("Arial", Font.BOLD, 20);}static BufferedImage blend(Slide slide1, Slide slide2, float weight) {BufferedImage bi1 = slide1.getBufferedImage();BufferedImage bi2 = (slide2 != null)? slide2.getBufferedImage(): new BufferedImage(Slide.WIDTH, Slide.HEIGHT,BufferedImage.TYPE_INT_RGB);BufferedImage bi3 = new BufferedImage(Slide.WIDTH, Slide.HEIGHT,BufferedImage.TYPE_INT_RGB);Graphics2D g2d = bi3.createGraphics();g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,weight));g2d.drawImage(bi1, 0, 0, null);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1.0f - weight));g2d.drawImage(bi2, 0, 0, null);g2d.setColor(Color.BLACK);g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,0.5f));g2d.fillRect(TEXTBOX_X, TEXTBOX_Y, TEXTBOX_WIDTH, TEXTBOX_HEIGHT);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,weight));g2d.setColor(Color.WHITE);g2d.setFont(font);FontMetrics fm = g2d.getFontMetrics();g2d.drawString(slide1.getText(), TEXTBOX_X + (TEXTBOX_WIDTH -fm.stringWidth(slide1.getText())) / 2,TEXTBOX_Y + TEXTBOX_HEIGHT / 2 + fm.getHeight() / 4);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1.0f - weight));if (slide2 != null)g2d.drawString(slide2.getText(), TEXTBOX_X + (TEXTBOX_WIDTH -fm.stringWidth(slide2.getText())) / 2, TEXTBOX_Y +TEXTBOX_HEIGHT / 2 + fm.getHeight() / 4);g2d.dispose();return bi3;}BufferedImage getBufferedImage() {return bi;}String getText() {return text;}static List<Slide> loadSlides(String imagesPath) throws IOException {File imageFilesPath = new File(imagesPath);if (!imageFilesPath.isDirectory())throw new IOException(imagesPath + " identifies a file");List<Slide> slides = new ArrayList<>();try (FileReader fr = new FileReader(imagesPath + "/index");BufferedReader br = new BufferedReader(fr)) {String line;while ((line = br.readLine()) != null) {String[] parts = line.split(",");File file = new File(imageFilesPath + "/" + parts[0] + ".jpg");System.out.println(file);BufferedImage bi = ImageIO.read(file);if (WIDTH == 0) {WIDTH = bi.getWidth();HEIGHT = bi.getHeight();TEXTBOX_WIDTH = WIDTH / 2 + 10;TEXTBOX_HEIGHT = HEIGHT / 10;TEXTBOX_Y = HEIGHT - TEXTBOX_HEIGHT - 5;TEXTBOX_X = (WIDTH - TEXTBOX_WIDTH) / 2;}slides.add(new Slide(bi, parts[1]));}}if (slides.size() < 2)throw new IOException("at least one image must be loaded");return slides;}}public class SlideShow {public static void main(String[] args) throws IOException {if (args.length != 1) {System.err.println("usage: java SlideShow ssdir");return;}List<Slide> slides = Slide.loadSlides(args[0]);final Screen screen = new Screen(Slide.WIDTH, Slide.HEIGHT);final Projector p = new Projector(slides, screen);Runnable r = () ->{final JFrame f = new JFrame("Slide Show");WindowAdapter wa = new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent we) {p.stop();f.dispose();}};f.addWindowListener(wa);f.setContentPane(screen);f.pack();f.setVisible(true);p.start();};EventQueue.invokeLater(r);}}
清单B-6基于类Projector、Screen、Slide以及SlideShow构建了一个幻灯片演示。Projector声明了几个private属性、一个用一组(java.util.List)Slide对象和单个Screen对象初始化投影仪对象的Projector(List slides, Screen s)构造函数、一个用于启动投影仪的void start()方法和一个停止投影仪的stop()方法。
Screen继承自javax.swing.JComponent,这使得Screen变成了一个特殊类型的Swing组件。它声明了几个private属性、一个接收width和height参数来初始化该组件的Screen(int width, int height)构造函数以及一个drawImage(BufferedImage bi)方法用于在屏幕的表面绘制传递进来的缓冲图片。这个类也重写了Dimension getPreferredSize()和void paint(Graphics g)方法,它们分别用于返回该组件的偏好大小和绘制图片。
Slide声明了多个常量、几个私有属性,一个用于初始化Slide对象的Slide(BufferedImage bi, String text)的私有构造函数、两个用于返回幻灯片的缓冲图片和文字的getter方法BufferedImage getBufferedImage()和String getText(),一个用于混合一对缓冲图片来实现幻灯片之间的过渡效果的类方法BufferedImage blend(Slide slide1, Slide slide2, float weight)以及用于加载所有幻灯片的图片的方法List loadSlides(String imagesPath)。
blend()方法抽取了与其slide参数相关联的缓冲图片,并且用weight值(必须落在0.0到1.0这个区间内)决定的混合度把这些图片混合到一起。传递给weight的值越高,slide1的图片就会更多地融入返回的缓冲图片中。混合图片之后,blend()方法在已混合的图片之上再混入一对文字字符串。类java.awt.AlphaComposite参与到每次混合操作中。
我已经设计blend()方法处理了slide2是null的特殊场景。这会发生在Projector的start()方法开始的时候,这时它执行了s.drawImage(Slide.blend(slides.get(0), null, 1.0f));方法来显示第一张幻灯片,并且不会有过渡。
loadSlides()方法查找字符串参数所指定目录底下名为index的文本文件,然后创建一组被文本文件排序的Slides——你可以选择一种不同于该目录下文件所确定的图片顺序来展示幻灯片。文本文件中的每一行都被组织成一个文件名后跟一个分号,再跟一个文字的描述(例如:earth, Terran System)。在指定文件名字的时候,你不必指定文件的扩展名,loadSlides()方法只能识别JPEG文件。
SlideShow声明了一个main()方法用于驱动这个应用程序。这个方法首先验证一个单一的命令行参数确定幻灯片的目录(目录中包含了index和JPEG文件)已经就绪。之后它调用了loadSlides()方法从这个目录中加载index和所有幻灯片图片。当loadSlides()方法无法加载图片或者图片的数量小于2时,它抛出java.io.IOException。毕竟,你如何用少于2张的图片来做幻灯片演示呢?
main()方法接下来创建一个Screen组件对象用于展示幻灯片图片。它把每个slide(事实上,是每个幻灯片图片的宽度和高度)的宽度和高度传递给Screen的构造函数,保证了屏幕能够充分展示这些幻灯片。(尽管我在loadSlides()方法中没做强制,但是所有的幻灯片图片必须用于同样的宽度和高度。)
剩下唯一需要创建的主要模型对象就是这个投影仪,main()方法通过把一组从loadSlides()方法中返回的Slide对象和前面创建的Screen对象传递到Projector的构造函数中完成了这一任务。
main()方法最后的任务就是触发GUI能在EDT上构造出来。这个线程把内容面板设置到Screen对象中并且调用了Projector的void start()方法开始播放幻灯片。同时,它也创建了一个窗口监听器用在用户试图关闭窗口时调用Projector的void stop()方法。之后窗口就会被销毁掉。
Projector使用了一对Timer对象来管理幻灯片演示。主定时器负责推进投影仪进入下一张幻灯片,而次定时器对象(在每次主定时器触发一个动作事件被创建出来)则负责过渡当前播放的幻灯片到下一张(借助blend()方法)。
每个定时器实例都运行在EDT之上。次定时器在运行时主定时器不可以执行定时器任务。如果打破这个规则,这个幻灯片演示就会发生故障。我在主定时器任务相继执行之间选取了3000毫秒,而次定时器任务相继执行之间选取了10毫秒,这样运行100次,大约需要1000毫秒。当次定时器结束了,它会自动停止。
照下面编译清单B-6:
javac SlideShow.java
假如是Windows操作系统,运行结果应用程序如下:
java SlideShow ..\ss
ss标志这是一个太阳系的幻灯片演示(包含在本书的代码当中),放置于当前目录的父级目录下。
图B-3 展示结果GUI
[图]
图B-3. SlideShow靠近slide底部的文字水平居中