24.6 将WAV编辑器转变为多视图编辑器
本小节将介绍如何将WavComponent改为多视图组件。关于多视图的详细介绍,请参考第12章。
WavOpenSupport需要返回的是一个多视图组件,而不是现在比较精简的WavComponent。
protected CloneableTopComponent createCloneableTopComponent()
{
// Create an array of MV descriptors with only one view of the
// data (the one weve already created - the waveform view)
WavPanelMultiViewDescriptor main =
new WavPanelMultiViewDescriptor();
MultiViewDescription[] descArry = { main };
// Initialize the view with data
WavDataObject dobj = (WavDataObject)entry.getDataObject();
main.setWavDataObject(dobj);
// Create the multiview
CloneableTopComponent tc = MultiViewFactory.
createCloneableMultiView(descArry, main, new CloseHandler());
tc.setDisplayName(dobj.getName());
return tc;
}
这段代码引入了两个新类:CloseHandler 和WavPanelMultiViewDescriptor。CloseHandler的名称表明了自己的作用。它负责处理多视图组件的关闭。下面的实现会询问用户文件是否应该在关闭前保存。根据用户对此的选择,对每个元素调用ProceedAction或者DiscardAction。在没有看到其他不同的实现之前,这个实现应该是默认设置:
private static class CloseHandler
implements CloseOperationHandler, Serializable {
private static final long serialVersionUID = 1L;
public boolean resolveCloseOperation(
CloseOperationState[] elements) {
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
"Save before closing?");
DialogDisplayer.getDefault().notify(nd);
if (nd.getValue().equals(NotifyDescriptor.YES_OPTION))
{
for (CloseOperationState element : elements)
{
element.getProceedAction().actionPerformed(
new ActionEvent(this,
ActionEvent.ACTION_PERFORMED, "xxx"));
}
return true;
}
else if (nd.getValue().equals(NotifyDescriptor.NO_OPTION))
{
for (CloseOperationState element : elements)
{
element.getDiscardAction().actionPerformed(
new ActionEvent(this,
ActionEvent.ACTION_PERFORMED, "xxx"));
}
return true;
}
else
{
// Cancel
return false;
}
}
}
WavPanelMultiViewDescriptor除了作为MultiViewElement的工厂类之外,还为每个视图提供一些描述(名称、图标等):
public class WavPanelMultiViewDescriptor
implements MultiViewDescription, Serializable {
private static final long serialVersionUID = 1L;
public static Image ICON =
Utilities.loadImage("org/foo/wavutils/sampleGraph.gif");
private WavDataObject dobj;
public int getPersistenceType() {
return TopComponent.PERSISTENCE_ALWAYS;
}
public String getDisplayName() {
return "Waveform";
}
public Image getIcon() {
return ICON;
}
public HelpCtx getHelpCtx() {
return null;
}
public String preferredID() {
return "wavEditor";
}
public MultiViewElement createElement() {
return new WavComponent(dobj);
}
public void setWavDataObject(WavDataObject wav) {
dobj = wav;
}
private void writeObject(ObjectOutputStream out)
throws IOException
{
out.defaultWriteObject();
}
}
现在,把老的WavComponent转变为一个MultiViewElement。元素本身再也不必是一个TopComponent的实例(不过,即使是的话也无妨)。
public class WavComponent implements MultiViewElement {
private static final CloseOperationState CLOSE_OPERATION_STATE
= createCloseOperationState();
private transient WavPanel wavPanel;
public WavComponent(DataObject dobj)
{
super();
wavPanel = new WavPanel(dobj);
}
public Action[] getActions() {
return new Action[0];
}
public Lookup getLookup() {
return wavPanel.getWavDataObject().getNodeDelegate().
getLookup();
}
public UndoRedo getUndoRedo() {
return new UndoRedo.Manager();
}
public JComponent getVisualRepresentation() {
return wavPanel;
}
public JComponent getToolbarRepresentation() {
// We dont need any widgets on the toolbar
return new JPanel();
}
public CloseOperationState canCloseElement() {
if (wavPanel.getWavDataObject().isModified())
{
return CLOSE_OPERATION_STATE;
}
else
{
return CloseOperationState.STATE_OK;
}
}
public void setMultiViewCallback(MultiViewElementCallback
multiViewElementCallback) {
// Dont need this
}
// Semantics similar to the equivalent methods in TopComponent
public void componentDeactivated() {}
public void componentActivated() {}
public void componentHidden() {}
public void componentShowing() {}
public void componentClosed() {}
public void componentOpened() {}
public Object writeReplace() {
return null;
}
private static CloseOperationState createCloseOperationState()
{
return MultiViewFactory.createUnsafeCloseState(
"xxx", new ProceedAction(), new DiscardAction());
}
private static class ProceedAction extends NodeAction
{
protected void performAction(Node[] node) {
try
{
if (node != null && node.length > 0)
{
SaveCookie sc =
(SaveCookie)node[0].getCookie(SaveCookie.class);
sc.save();
}
}
catch(IOException ex)
{
ErrorManager.getDefault().notify(ex);
}
}
protected boolean enable(Node[] node) {
return true;
}
public String getName() {
return "Save";
}
public HelpCtx getHelpCtx() {
return null;
}
}
private static class DiscardAction extends NodeAction
{
protected void performAction(Node[] node) {
if (node != null && node.length > 0)
{
DataObject dobj =
(DataObject)node[0].getCookie(DataObject.class);
try
{
// Throw away whats in memory.
// The DataObject will be recreated from disk.
dobj.setValid(false);
}
catch (PropertyVetoException ex)
{
ErrorManager.getDefault().notify(ex);
}
}
}
protected boolean enable(Node[] node) {
return true;
}
public String getName() {
return "Discard";
}
public HelpCtx getHelpCtx() {
return null;
}
}
}
此时,编辑器应该和前一小节中的完全一样,但它是在多视图窗口中,如图24-10所示。
图24-10 只有一个视图的“多视图”WAV编辑器
接下来定义一个扩展点,让其他模块可以在多视图窗口中插入新的视图。
24.7 创建插入额外视图的API
在NetBeans平台上创建API的第一步是新建一个独立的包(例如,org.foo.wavsupport.api))。依赖于wavsupport的模块应该只能访问这个包中的类。为了确保这一点,请在wavsupport的“项目属性”窗口中,把这个包指定为“公共包”。如图24-11所示。
图24-11 一个公共的API包
这个API的目的是让其他的模块可以提供它们自己的MultiViewDescriptions。这意味着至少能够从其他模块中收集MultiViewDescription的实例,然后把它们插入多视图窗口中。
但是,为了让这些其他模块获得足够的信息以创建有意义的界面,它们需要访问WavDataObject。所以,在API包中创建一个子接口:
public interface WavViewDescriptor
extends MultiViewDescription, Serializable {
void setWavDataObject(DataObject dobj);
}
请注意,setWavDataObject()方法接受一个通用的DataObject类型对象作为参数,而不是更加特殊的WavDataObject。这是因为WavDataObject并不在API包中。最好能够让客户模块需要的任何数据都能够在DataObject的cookie集找到。
在API包中创建一个新的子接口,命名为WavCookie,将WavDataObject中所有公共常量都移到这个接口中。同时,再为所有希望WavDataObject对外公开的方法声明公共API方法:
public interface WavCookie extends Node.Cookie {
public static final String PROP_WAVEFORM = "waveform";
AudioFormat getAudioFormat();
WrappedAudioInputStream getAudioInputStream();
void setAudioInputStream( WrappedAudioInputStream is );
void addPropertyChangeListener(PropertyChangeListener l);
void removePropertyChangeListener(PropertyChangeListener l);
}
然后,让WavDataObject实现这个接口:
public class WavDataObject extends MultiDataObject
implements WavCookie {
...
}
请注意,不需要像在执行打开和保存cookie操作时一样把WavDataObject显式式添加到它的cookie集中。这是一个特例。所有DataObject都会自动地加入到它们自己的cookie集中。目前WavOpenSupport类仅仅实例化WavPanelMultiViewDescriptor,然后把它放在一个单元素的数组中传递给MultiViewFactory。现在,将描述符放进一个未知大小的列表中,作为第一个元素,使用Lookup(有关Lookup的详细信息,请参阅第4章的4.3节)填充列表中的其他元素:
WavViewDescriptor main = new WavPanelMultiViewDescriptor();
List all = new ArrayList();
all.add(main);
Lookup.Template template =
new Lookup.Template(WavViewDescriptor.class);
Lookup.Result result = Lookup.getDefault().lookup(template);
for (Object wvd : result.allInstances())
{
all.add((WavViewDescriptor)wvd);
}
然后把数据对象的引用传递给所有描述符:
for (WavViewDescriptor wvd : all)
{
wvd.setWavDataObject(dobj);
}
最后,把列表转换成数组,传递给MultiViewFactory:
WavViewDescriptor[] allArray = new WavViewDescriptor[all.size()];
all.toArray(allArray);
CloneableTopComponent tc =
MultiViewFactory.createCloneableMultiView(allArray, main,
new CloseHandler());
至此,客户模块就可以实现公开的WavViewDescriptor接口,并使用WavCookie提供的信息在多视图窗口中提供一个新的标签页——这些都完全不需要编辑wavsupport模块的源代码。
24.8 实现API,提供新视图
现在是时候成为我们自己API的客户,创建一个新模块,为WAV文件提供一个不同的可视化视图。为了方便读者,wavutils模块包含了组件FFTGraph,它基于一个在网上找到的公开域FFT库(感谢来自宾夕法尼亚大学的Tsan-Kuang Lee!),可以绘制频率域视图。
首先在模块套件中创建一个新模块,命名为fftview。别忘记将wavsupport 和wavutils加入到它依赖的模块集中。
接着,创建API接口 WavViewDescriptor的实现:
public class FFTViewDescriptor implements WavViewDescriptor {
private static final long serialVersionUID = 1L;
public static Image ICON =
Utilities.loadImage("org/foo/wavutils/sampleGraph.gif");
private DataObject dobj;
public int getPersistenceType() {
return TopComponent.PERSISTENCE_ALWAYS;
}
public String getDisplayName() {
return "Frequency Domain";
}
public Image getIcon() {
return ICON;
}
public HelpCtx getHelpCtx() {
return null;
}
public String preferredID() {
return "FFT";
}
public MultiViewElement createElement() {
return new FFTComponent(dobj);
}
public void setWavDataObject(DataObject wav) {
dobj = wav;
}
private void writeObject(ObjectOutputStream out)
throws IOException
{
out.defaultWriteObject();
}
}
然后,还要再创建一个类,提供真正的组件:
public class FFTComponent implements MultiViewElement {
private DataObject dobj;
private final FFTGraph graph = new FFTGraph();
public FFTComponent(DataObject dobj)
{
super();
this.dobj = dobj;
final WavCookie c =
(WavCookie)dobj.getCookie(WavCookie.class);
assert(c != null);
graph.createGraph(c.getAudioInputStream());
c.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().
equals(WavCookie.PROP_WAVEFORM))
{
WrappedAudioInputStream wais = c.getAudioInputStream();
if (wais == null)
graph.clearGraph();
else
graph.createGraph(wais);
}
}
});
}
public JComponent getVisualRepresentation() {
return graph;
}
public JComponent getToolbarRepresentation() {
return new JPanel();
}
public void setMultiViewCallback(
MultiViewElementCallback multiViewElementCallback) {
// Do nothing (we dont need the callback)
}
public CloseOperationState canCloseElement() {
// The main wav component handles asking the user to save.
// _This_ component is OK, whatever the outcome.
// If the main component needed to provide any visual
// feedback before saving/closing, this component could
// have its own Proceed/Discard actions
return CloseOperationState.STATE_OK;
}
public void componentDeactivated() {}
public void componentActivated() {}
public void componentHidden() {}
public void componentShowing() {}
public void componentClosed() {}
public void componentOpened() {}
public Object writeReplace() {
return null;
}
public Action[] getActions() {
return new Action[0];
}
public Lookup getLookup() {
return dobj.getNodeDelegate().getLookup();
}
public UndoRedo getUndoRedo() {
return new UndoRedo.Manager();
}
}
剩下的最后一步是发布这个实现,让WavOpenSupport中的Lookup代码知道在哪里能找到它。最简单的方法是在src/META-INF/services/org/foo/wavsupport/api/WavViewDescriptor中新建一个文件,其中包含下列内容:
org.foo.fftview.FFTViewDescriptor
现在运行应用程序,应该能够看到两个视图,如图24-12所示。
图24-12 一个WAV文件的频率域视图
在全局Lookup中查询作为API的接口的实例是非常有用的技术。特别是对于一些特殊情况就更加有帮助——例如,必须集成API经常改变的产品或库,或者集成相互竞争的厂家的多个产品。开发者定义的接口会成为一个稳定的“桥梁”,每一个客户模块都是一个接口的实现,它们内部可以使用不同特定厂家的API。
例如,假设正在构建一个填写税务表格的软件,开发者可能拥有一个接口,用来向客户模块查询税率、法律和表格要求的格式。然后为不同的权限提供不同的模块。到第二年,只需要更新这些模块即可,而应用程序的核心照常工作,无需改变。
展开
——Tim Boudreau
在不久的将来,模块化应用程序会变得越来越重要。在本书中,我们尽力介绍了模块化所需的基础知识,并且洋细描述了如何存NetBeans中实现它。我衷心期望尊敬的读者们都能轻松地读完这本书,并且学到对今后的职业生涯都非常有用的知识。
——Jaroslav Tulach
随着应用程序的规模和复杂程度不断增加,您会慢慢发现NetBeans平台所提供的服务极其有用。我希望这本书能够提供您所需的绝大多数知识,以及观察应用程序的新视角。最重要的是,我希望本书给您带来的不仅是有用的知识,还有快乐。
——Geertjan Wielenga