数据加载模糊进度指示面板的实现与应用
当在加载数据(或其它耗时工作)时,需要显示一个进度指示面板,本文介绍了一种简易的实现方式。(2009.11.30最后更新)
对于许多Swing应用,在与用户的交互过程中可能需要与数据库进行通信(如,加载数据)。而这个过程往往比较耗时,为了不造成"假死"现象,一般都会显示一个模糊进度指示器(不一定使用JProgressBar,简单地用一个图片代替即可),当数据加载完毕后,该进度指示器自动消失。
一般地,该模糊进度指示器不会展示在一个弹出的对话框中(因为这样不美观),而是直接显示在需要展示被加载数据的面板中,并且对该面板进行模糊处理。实现这一功能的关键就在于,在屏幕的同一区域内展示两层面板:一层是展示数据的面板;另一层是展示进度指示器的面板。当加载数据时,显示进度指示器面板,并模糊数据面板;当数据加载完毕后,隐藏进度指示器面板,并使数据面板清晰显示。下面将使用org.jdesktop.swingx.StackLayout方式来实现上述功能。
1. LoadingPanel--加载指示器面板
首先创建一个加载指示器面板。如前所述,我们不必使用真正的进度条作为进度指示器,仅需要使用一张动态图片来代替即可。LoadingPanel的完整代码如下所示,
public class LoadingPanel extends JPanel {
private static final long serialVersionUID = 1962748329465603630L;
private String mesg = null;
public LoadingPanel(String mesg) {
this.mesg = mesg;
initUI();
interceptInput();
setOpaque(false);
setVisible(false);
}
private void initUI() {
JLabel label = new JLabel(mesg);
label.setHorizontalAlignment(JLabel.CENTER);
label.setIcon(new ImageIcon(getClass().getResource("/path/to/spinner.gif")));
setLayout(new BorderLayout());
add(label, BorderLayout.CENTER);
}
private void interceptInput() {
addMouseListener(new MouseAdapter() {});
addMouseMotionListener(new MouseMotionAdapter() {});
addKeyListener(new KeyAdapter() {});
addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
requestFocusInWindow();
}
});
setFocusTraversalKeysEnabled(false);
}
}
上述代码很容易理解,LoadingPanel中仅有一个JLabel,它会展示一张图片(spinner.gif)及一段信息。但有两段代码需要特别说明:
[1]构造器中的两行代码
setVisible(false);
setOpaque(false);
LoadingPanel只在加载数据时才显示,其它时候是不显示的,所以它默认不可见。另外,在显示LoadingPanel的同时,我们仍然希望能看到数据面板,所以LoadingPanel应该是透明的。
[2]interceptInput方法
当LoadingPanel显示之后,我们不希望用户还能够操作数据面板,那么就需要屏蔽掉用户(鼠标,键盘)输入。
addMouseListener(new MouseAdapter() {});
addMouseMotionListener(new MouseMotionAdapter() {});
addKeyListener(new KeyAdapter() {});
上述三行代码就使得LoadingPanel能捕获所有的鼠标与键盘事件,并忽略掉它们。但仅仅如此还不够,在展示LoadingPanel时,数据面板中的某个UI组件很可能已经获得焦点了,那么用户仍然可以通过键盘操控数据面板中的组件(因为系统会把键盘事件发送给当前获取焦点的组件)。而且,即使数据面板中没有任何组件获得焦点,用户仍然可以通过Tab键把焦点转移到数据面板中的组件上。为了阻止这一操作,还需要加上如下几行代码,
addComponentListener(new ComponentAdapter() { // 一旦LoadingPanel可见,即获取焦点
@Override
public void componentShown(ComponentEvent e) {
requestFocusInWindow();
}
});
setFocusTraversalKeysEnabled(false); // 阻止用户转移焦点
2. 示例程序
在此处的示例程序中,数据面板(dataPanel)中仅有一个按钮,当点击该按钮时会显示loadingPanel,且模糊掉dataPanel,并会启动一个新的线程,该线程会在睡眠大约3秒(模拟耗时的数据加载工作)之后隐藏loadingPanel,且使dataPanel重新清晰可见。
值得注意的是,该示例程序使用了SwingX中的两个组件:JXPanel和StackLayout。JXPanel提供了一个方法(setAlpha)以方便地设置Panel的透明度(Alpha值);而StackLayout允许在同一块区域内添加多层组件,并能同时展示所有层的组件(而,CardLayout一次只能显示某一层的组件)。完整的示例程序如下所示,
public class LoadDataDemo extends JFrame {
private static final long serialVersionUID = 5927602404779391420L;
private JXPanel dataPanel = null; // 使用org.jdesktop.swingx.JXPanel,以方便设置清晰度
private LoadingPanel loadingPanel = null;
public LoadDataDemo() {
super("LoadData Demo");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
initUI();
}
private void initUI() {
JButton button = new JButton("Load Data");
button.addActionListener(handler);
dataPanel = new JXPanel(new FlowLayout(FlowLayout.CENTER));
dataPanel.add(button);
loadingPanel = new LoadingPanel("Loading");
// 使用org.jdesktop.swingx.StackLayout,将loadingPanel置于dataPanel的上方
JPanel centerPanel = new JXPanel(new StackLayout());
centerPanel.add(dataPanel, StackLayout.TOP);
centerPanel.add(loadingPanel, StackLayout.TOP);
Container container = getContentPane();
container.setLayout(new BorderLayout());
container.add(centerPanel, BorderLayout.CENTER);
}
transient private ActionListener handler = new ActionListener() {
public void actionPerformed(ActionEvent e) {
// 将dataPanel及其子组件的清晰度设置为50%;并显示loadingPanel
dataPanel.setAlpha(0.5F);
loadingPanel.setVisible(true);
Thread thread = new Thread() {
public void run() {
try {
Thread.sleep(3000L); // 睡眠约3秒钟,以模拟加载数据的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
// 数据加载完毕后,重新隐藏loadingPanel;并使dataPanel及其子组件重新清晰可见
loadingPanel.setVisible(false);
dataPanel.setAlpha(1F);
};
};
thread.start();
}
};
public static void main(String[] args) {
LoadDataDemo demo = new LoadDataDemo();
demo.setSize(new Dimension(400, 300));
demo.setVisible(true);
}
}
3. 不使用SwingX
SwingX为我们提供了一系列功能强大,使用简易的Swing扩展组件,我强烈建议你去使用它。但若因故,你不准备使用它时,我们仍然有替代的解决方案,但此处仅简述一二。
[1]对于设置Alpha值,需要创建一个继承自JPanel的DataPanel类,覆写paintComponent方法,在其中使用Alpha合成,
Graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
[2]对于StackLayout,我们可以使用GlassPane(玻璃窗格)或LayeredPane(分层窗格)进行替换,将LoadingPanel设置为GlassPane或LayeredPanel中的一层。由于一个JFrame只有一个GlassPane,为了程序的灵活性,一般首选使用LayeredPane。