当前位置: 代码迷 >> 报表 >> 第十四章 高级特点-海纳百川:BIRT报表扩展点(续2)
  详细解决方案

第十四章 高级特点-海纳百川:BIRT报表扩展点(续2)

热度:3030   发布时间:2013-02-26 00:00:00.0
第十四章 高级特性-海纳百川:BIRT报表扩展点(续2)

14.3 报表项的扩展

同上两节所述,BIRT报表扩展点实际上是利用了eclipse的扩展点

BIRT本身提供了显示成水平或者垂直文本的标签和文本,本节扩展报表项,让它能显示有一定旋转角度向四周扩散的标签和文本。

为了实现扩展报表项,我们需要查看以下的报表扩展点,实现其中的接口方法:

org.eclipse.birt.report.model.reportItemModel

org.eclipse.birt.report.designer.ui.reportitemUI

org.eclipse.birt.report.engine.reportitemPresentation

以上三个扩展点提供了基本的报表项的扩展点,基本上看

org.eclipse.birt.report.model.reportItemModel提供报表模型的扩展性

 org.eclipse.birt.report.designer.ui.reportitemUI 提供报表设计器的扩展性

 org.eclipse.birt.report.engine.reportitemPresentation 提供报表引擎的扩展性.

org.eclipse.birt.report.model.reportItemModel

这是BIRT报告模型提供的扩展点,一般而言用户利用这个扩展点来扩展BIRT报告模型

首先我们定义一个报表项扩展模型,我们把reportItemextensionName命名为“RotatedText”,这个名字是这个扩展报表项的标识符,也是连接引擎和报表设计器的唯一标识。

然后我们定义报表工厂实现类如下 org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextItemFactory. 这个工厂类用于create 和initialize  IReportItem 实例, 同样也提供可选的本地化支持:

public class RotatedTextItemFactory extends ReportItemFactory{    public IReportItem newReportItem( DesignElementHandle modelHandle )    {        if ( modelHandle instanceof ExtendedItemHandle && RotatedTextItem.EXTENSION_NAME.equals( ( (ExtendedItemHandle) modelHandle ).getExtensionName( ) ) )        {            return new RotatedTextItem( (ExtendedItemHandle) modelHandle );        }        return null;    }    public IMessages getMessages( )    {        // TODO implement this to support localization        return null;    }}

在报表项的属性页,我们能看到其它的属性例如默认样式,继承自等等,这些属性可以让报表项拥有更多的特性。我们在这节没使用这些属性,所以我们省略了这些实现。

我们为RotatedText扩展报表项定义了两个新的属性 

rotationAngle, the rotation angle(旋转角度), integer(类型), 默认为0

text, the text content(文本内容), string (类型), 默认为 "Rotated Text".

我们在属性页能看到很多的属性,但在本节我们不涉及他们。这些属性代表更复杂的属性和特性。定义完了扩展报表项之后,我们就需要实现扩展这个报表项需要实现的方法,报表项扩展点类实际上是一个封装类,但提供了便利的API接口来扩展更多的属性

public class RotatedTextItem extends ReportItem{    public static final String EXTENSION_NAME = "RotatedText"; //$NON-NLS-1$    public static final String TEXT_PROP = "text"; //$NON-NLS-1$    public static final String ROTATION_ANGLE_PROP = "rotationAngle"; //$NON-NLS-1$    private ExtendedItemHandle modelHandle;    RotatedTextItem( ExtendedItemHandle modelHandle )    {        this.modelHandle = modelHandle;    }    public String getText( )    {        return modelHandle.getStringProperty( TEXT_PROP );    }    public int getRotationAngle( )    {        return modelHandle.getIntProperty( ROTATION_ANGLE_PROP );    }    public void setText( String value ) throws SemanticException    {        modelHandle.setProperty( TEXT_PROP, value );    }    public void setRotationAngle( int value ) throws SemanticException    {        modelHandle.setProperty( ROTATION_ANGLE_PROP, value );    }}

我们可以看到大部分的实现就是类似java bean一样的settergetter方法,但这对于一般的扩展已经足够了。org.eclipse.birt.report.designer.ui.reportitemUI

这个扩展点是报表设计器提供的,用户实现这个扩展点来扩展报表设计器中的用户界面等

首先我们需要创建一个reportitemUI模型,用来绑定reportitemUI和前文中创建的模型,取名必须和模型中的一致,就是RotatedText

下一步就是定义在面板,大纲,设计器编辑器中显示的UI

面板定制上的属性主要是定义它的图标,门类。如果门类没有定制,那么就默认放在报表项这个门类下。

编辑器定制主要是设置在不同的编辑窗口下(主页,设计器)能否显示,以及能否重置大小

Outline 定制是指在大纲视图下的的图标,一般和面板上一致即可

最后我们还要实现IReportItemLabelProvider

这个 UI provider 定义在编辑器中怎么显示和交互这个报表项

 BIRT设计器支持以下三种UI provider

The Label UI Provider(标签UI) 实现了IReportItemLabelProvider 接口, 简化了操作和显示一个文本内容

The Image UI Provider (图形UI)实现了 IReportItemImageProvider 接口, 简化了操作和显示一个图像内容

the Figure UI Provider(图像UI) 实现了 IRportItemFigureProvider, 使用了GEF提供的Figure 接口, 提供了更丰富的扩展性和交互性的支持。

这一例中我们仅仅实现了标签UI,后面还会介绍别的UI

为了注册这个UI provider,我们定义了 "reportItemLabelUI" 项来实现IReportItemLabelProvider 接口.

org.eclipse.birt.sample.reportitem.rotatedText.RotatedTextLabelUI源码很简单,如下所示

public class RotatedTextLabelUI implements IReportItemLabelProvider{    public String getLabel( ExtendedItemHandle handle )    {        try        {            IReportItem item = handle.getReportItem( );            if ( item instanceof RotatedTextItem )            {                return ( (RotatedTextItem) item ).getText( );            }        }        catch ( ExtendedElementException e )        {            e.printStackTrace( );        }        return null;    }}

仅仅从模型中读取文本属性值,范围一个字符串类型

完成了设计器的标签的扩展实现,我们可以重启设计器测试以下,发现新增了我们需要的标签

在编辑器中右击插入,也能看到我们扩展实现的RotatedText

在布局中插入这个标签之后,我们在大纲视图中也能看到

选择RotatedText 项, 打开所有属性视图,我们能看到所有的属性列表,除了继承的属性,我们能看到Rotation Angle属性和Text Content属性,在这儿我们可以设置这两个属性值

设计器的扩展点做完了之后,我们来实现报表引擎的扩展点-报表项展示扩展

org.eclipse.birt.report.engine.reportitemPresentation

这个扩展点是 BIRT Report Engine(BIRT报表引擎)提供的, 用户利用这个扩展点来实现报表的展示

同样我们需要绑定引擎扩展点和模型扩展点

在引擎扩展点类别下创建一个reportItem,取名必须也是RotatedText同模型中取名一致。和前面一样,我们也要实现一个报表引擎展示层的扩展点类,命名为org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextPresentationImpl. 这个实现类必须实现theIReportItemPresentation 接口

我们创建一个图片来动态实现我们想要的旋转效果,调用java swing的图形API方法来完成这个实现,源码如下:

public class RotatedTextPresentationImpl extends ReportItemPresentationBase{    private RotatedTextItem textItem;        public void setModelObject( ExtendedItemHandle modelHandle )    {        try        {            textItem = (RotatedTextItem) modelHandle.getReportItem( );        }        catch ( ExtendedElementException e )        {            e.printStackTrace( );        }    }        public int getOutputType( )    {        return OUTPUT_AS_IMAGE;    }        public Object onRowSets( IRowSet[] rowSets ) throws BirtException    {        if ( textItem == null )        {            return null;        }                int angle = textItem.getRotationAngle( );        String text = textItem.getText( );        BufferedImage rotatedImage = SwingGraphicsUtil.createRotatedTextImage( text, angle, new Font( "Default", 0, 12 ) ); //$NON-NLS-1$        ByteArrayInputStream bis = null;                try        {            ImageIO.setUseCache( false );            ByteArrayOutputStream baos = new ByteArrayOutputStream( );            ImageOutputStream ios = ImageIO.createImageOutputStream( baos );            ImageIO.write( rotatedImage, "png", ios ); //$NON-NLS-1$            ios.flush( );            ios.close( );            bis = new ByteArrayInputStream( baos.toByteArray( ) );        }        catch ( IOException e )        {            e.printStackTrace( );        }        return bis;    }}

这个实现动态的在 onRowSets() 方法中产生了一个图像. 打开图片,把字节流放置在内存中便于显示

下面的Swing工具类是图像旋转显示的核心算法:

public class SwingGraphicsUtil{    public static BufferedImage createRotatedTextImage( String text, int angle, Font ft )    {        Graphics2D g2d = null;        try        {            if ( text == null || text.trim( ).length( ) == 0 )            {                return null;            }            BufferedImage stringImage = new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB );            g2d = (Graphics2D) stringImage.getGraphics( );            g2d.setFont( ft );            FontMetrics fm = g2d.getFontMetrics( );            Rectangle2D bounds = fm.getStringBounds( text, g2d );            TextLayout tl = new TextLayout( text, ft, g2d.getFontRenderContext( ) );            g2d.dispose( );            g2d = null;            return createRotatedImage( tl, (int) bounds.getWidth( ), (int) bounds.getHeight( ), angle );        }        catch ( Exception e )        {            e.printStackTrace( );            if ( g2d != null )            {                g2d.dispose( );            }        }        return null;    }        private static BufferedImage createRotatedImage( Object src, int width, int height, int angle )    {        angle = angle % 360;        if ( angle < 0 )        {            angle += 360;        }        if ( angle == 0 )        {            return renderRotatedObject( src, 0, width, height, 0, 0 );        }        else if ( angle == 90 )        {            return renderRotatedObject( src, -Math.PI / 2, height, width, -width, 0 );        }        else if ( angle == 180 )        {            return renderRotatedObject( src, Math.PI, width, height, -width, -height );        }        else if ( angle == 270 )        {            return renderRotatedObject( src, Math.PI / 2, height, width, 0, -height );        }        else if ( angle > 0 && angle < 90 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );            int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );            return renderRotatedObject( src, angleInRadians, dW, dH, -width * sineTheta * sineTheta, width * sineTheta * cosTheta );        }        else if ( angle > 90 && angle < 180 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );            int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );            return renderRotatedObject( src, angleInRadians, dW, dH, -( width + height * sineTheta * cosTheta ), -height / 2 );        }        else if ( angle > 180 && angle < 270 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );            int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );            return renderRotatedObject( src, angleInRadians, dW, dH, -( width * cosTheta * cosTheta ), -( height + width * cosTheta * sineTheta ) );        }        else if ( angle > 270 && angle < 360 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );            int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );            return renderRotatedObject( src, angleInRadians, dW, dH, ( height * cosTheta * sineTheta ), -( height * sineTheta * sineTheta ) );        }        return renderRotatedObject( src, 0, width, height, 0, 0 );    }        private static BufferedImage renderRotatedObject( Object src, double angle, int width, int height, double tx, double ty )    {        BufferedImage dest = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );        Graphics2D g2d = (Graphics2D) dest.getGraphics( );        g2d.setColor( Color.black );        g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );        g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );        AffineTransform at = AffineTransform.getRotateInstance( angle );        at.translate( tx, ty );        g2d.setTransform( at );        if ( src instanceof TextLayout )        {            TextLayout tl = (TextLayout) src;            tl.draw( g2d, 0, tl.getAscent( ) );        }        else if ( src instanceof Image )        {            g2d.drawImage( (Image) src, 0, 0, null );        }        g2d.dispose( );        return dest;    }}

测试一下,在网格中插入Rotated Text

设置他们不同的角度,以45度为单位顺时针旋转,以HTML格式预览他们如下:

以PDF格式预览如下:

这样我们就成功的通过实现模型,设计器,引擎的扩展点完成了自定义的报表项的扩展。

上文我们实现了 Label UI provider. 缺点很明显,我们在布局中显示的是水平的尽管属性中是有角度的,预览时显示的是有角度的,它不是所见即所得的样式。用户无法在编辑器中获得直观的感受。现代的编辑器,所见即所得几乎是默认的标准,完美一点,我们也想做到编辑器中和预览中一样的所见即所得的感受

为了实现这个效果,我们需要看看另外两个UI Provider

Image UI Provider(图形UI)

Figure UI Provider(图像UI)

The Image UI provider是个直观的选择,它需要一个图片来作为UI的展示. 和在引擎中做得一样,我们需要产生一个图形返回给UI provider

注册 Image UI provider, 在这儿我们不另外重做了,就删除原来的Label UI provider,加入新的Image UI provider 扩展, 然后实现我们需要实现的扩展类org.eclipse.birt.report.designer.ui.extensions.IReportItemImageProvider.

我们把这个实现类取名org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextImageUI, 代码如下:

public class RotatedTextImageUI implements IReportItemImageProvider{    public void disposeImage( ExtendedItemHandle handle, Image image )    {        if ( image != null && !image.isDisposed( ) )        {            image.dispose( );        }    }        public Image getImage( ExtendedItemHandle handle )    {        try        {            IReportItem item = handle.getReportItem( );                        if ( item instanceof RotatedTextItem )            {                int angle = ( (RotatedTextItem) item ).getRotationAngle( );                String text = ( (RotatedTextItem) item ).getText( );                            return SwtGraphicsUtil.createRotatedTextImage( text, angle, null );            }        }        catch ( ExtendedElementException e )        {            e.printStackTrace( );        }        return null;    }}

这儿我们用SWT来提供图形生成的核心算法工具类:

public class SwtGraphicsUtil{    public static Image createRotatedTextImage( String text, int angle, Font ft )    {        GC gc = null;        try        {            if ( text == null || text.trim( ).length( ) == 0 )            {                return null;            }                        Display display = Display.getCurrent( );                        gc = new GC( display );            if ( ft != null )            {                gc.setFont( ft );            }                        Point pt = gc.textExtent( text );                        gc.dispose( );                        TextLayout tl = new TextLayout( display );            if ( ft != null )            {                tl.setFont( ft );            }            tl.setText( text );                        return createRotatedImage( tl, pt.x, pt.y, angle );        }        catch ( Exception e )        {            e.printStackTrace( );                        if ( gc != null && !gc.isDisposed( ) )            {                gc.dispose( );            }        }        return null;    }        /**     * @return Returns as [rotatedWidth, rotatedHeight, xOffset, yOffset]     */    public static double[] computedRotatedInfo( int width, int height, int angle )    {        angle = angle % 360;                if ( angle < 0 )        {            angle += 360;        }                if ( angle == 0 )        {            return new double[]{ width, height, 0, 0 };        }        else if ( angle == 90 )        {            return new double[]{ height, width, -width, 0 };        }        else if ( angle == 180 )        {            return new double[]{ width, height, -width, -height };        }        else if ( angle == 270 )        {            return new double[]{ height, width, 0, -height };        }        else if ( angle > 0 && angle < 90 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );                        int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );                        return new double[]{ dW, dH, -width * sineTheta * sineTheta, width * sineTheta * cosTheta };        }        else if ( angle > 90 && angle < 180 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );                        int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );                        return new double[]{ dW, dH, -( width + height * sineTheta * cosTheta ), -height / 2 };        }        else if ( angle > 180 && angle < 270 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );                        int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );                        return new double[]{ dW, dH, -( width * cosTheta * cosTheta ), -( height + width * cosTheta * sineTheta ) };        }        else if ( angle > 270 && angle < 360 )        {            double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );            double cosTheta = Math.abs( Math.cos( angleInRadians ) );            double sineTheta = Math.abs( Math.sin( angleInRadians ) );                        int dW = (int) ( width * cosTheta + height * sineTheta );            int dH = (int) ( width * sineTheta + height * cosTheta );                        return new double[]{ dW, dH, ( height * cosTheta * sineTheta ), -( height * sineTheta * sineTheta ) };        }                return new double[]{ width, height, 0, 0 };    }        private static Image createRotatedImage( Object src, int width, int height, int angle )    {        angle = angle % 360;                if ( angle < 0 )        {            angle += 360;        }                double[] info = computedRotatedInfo( width, height, angle );                return renderRotatedObject( src, -angle, (int) info[0], (int) info[1], info[2], info[3] );    }        private static Image renderRotatedObject( Object src, double angle, int width, int height, double tx, double ty )    {        Display display = Display.getCurrent( );                Image dest = null;        GC gc = null;        Transform tf = null;                try        {            dest = new Image( Display.getCurrent( ), width, height );            gc = new GC( dest );                        gc.setAdvanced( true );            gc.setAntialias( SWT.ON );            gc.setTextAntialias( SWT.ON );                        tf = new Transform( display );            tf.rotate( (float) angle );            tf.translate( (float) tx, (float) ty );                        gc.setTransform( tf );                        if ( src instanceof TextLayout )            {                TextLayout tl = (TextLayout) src;                tl.draw( gc, 0, 0 );            }            else if ( src instanceof Image )            {                gc.drawImage( (Image) src, 0, 0 );            }        }        catch ( Exception e )        {            e.printStackTrace( );        }        finally        {            if ( gc != null && !gc.isDisposed( ) )            {                gc.dispose( );            }                        if ( tf != null && !tf.isDisposed( ) )            {                tf.dispose( );            }        }        return dest;    }}

测试一下,在属性中输入45

(Image UI)图形UI provider已经提供了基本的所见即所得的实现,这对于一般的扩展应用已经足够了 

如果用户还想提供更丰富多彩的扩展实现,Figure UI(图像UI provider是另一种选择

由于图像UI继承自GEF,故而更复杂,需要用户非常熟悉GEF的生命周期,有产生,刷新,展示三个阶段,一旦模型的状态改变了,就会刷新图像的展示。

现在我们利用图像UI来实现一个简单的特性,就是添加鼠标中键的单击事件,每次单击之后旋转报表项文本45度。

和利用Image UI provider一样,我们首先移除 Label UI 或者Image UI provider, 添加一个新的扩展点来实现org.eclipse.birt.report.designer.ui.extensions.IReportItemFigureProvider.

 

我们把这个实现类命名为org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextFigureUI, 源码如下

public class RotatedTextFigureUI extends ReportItemFigureProvider{    public IFigure createFigure( ExtendedItemHandle handle )    {        try        {            IReportItem item = handle.getReportItem( );                        if ( item instanceof RotatedTextItem )            {                return new RotatedTextFigure( (RotatedTextItem) item );            }        }        catch ( ExtendedElementException e )        {            e.printStackTrace( );        }        return null;    }        public void updateFigure( ExtendedItemHandle handle, IFigure figure )    {        try        {            IReportItem item = handle.getReportItem( );                        if ( item instanceof RotatedTextItem )            {                RotatedTextFigure fig = (RotatedTextFigure) figure;                                fig.setRotatedTextItem( (RotatedTextItem) item );            }        }        catch ( ExtendedElementException e )        {            e.printStackTrace( );        }    }        public void disposeFigure( ExtendedItemHandle handle, IFigure figure )    {        ( (RotatedTextFigure) figure ).dispose( );    }}

我们新建一个 RotatedTextFigure 实例,把所有的算法逻辑都放置在这个类中:

public class RotatedTextFigure extends Figure{    private String lastText;    private int lastAngle;    private Image cachedImage;    private RotatedTextItem textItem;        RotatedTextFigure( RotatedTextItem textItem )    {        super( );                this.textItem = textItem;                addMouseListener( new MouseListener.Stub( ) {                    public void mousePressed( MouseEvent me )            {                if ( me.button == 2 )                {                    try                    {                        RotatedTextFigure.this.textItem.setRotationAngle( normalize( RotatedTextFigure.this.textItem.getRotationAngle( ) + 45 ) );                    }                    catch ( SemanticException e )                    {                        e.printStackTrace( );                    }                }            }        } );    }        private int normalize( int angle )    {        angle = angle % 360;                if ( angle < 0 )        {            angle += 360;        }                return angle;    }        public Dimension getMinimumSize( int hint, int hint2 )    {        return getPreferredSize( hint, hint2 );    }        public Dimension getPreferredSize( int hint, int hint2 )    {        Display display = Display.getCurrent( );                GC gc = null;                try        {            String text = textItem.getText( );            int angle = textItem.getRotationAngle( );                        gc = new GC( display );                        Point pt = gc.textExtent( text == null ? "" : text ); //$NON-NLS-1$                        double[] info = SwtGraphicsUtil.computedRotatedInfo( pt.x, pt.y, angle );                        if ( getBorder( ) != null )            {                Insets bdInsets = getBorder( ).getInsets( this );                                return new Dimension( (int) info[0] + bdInsets.getWidth( ), (int) info[1] + bdInsets.getHeight( ) );            }            return new Dimension( (int) info[0], (int) info[1] );        }        finally        {            if ( gc != null && !gc.isDisposed( ) )            {                gc.dispose( );            }        }    }        protected void paintClientArea( Graphics graphics )    {        final Rectangle r = getClientArea( ).getCopy( );                String text = textItem.getText( );        int angle = textItem.getRotationAngle( );                if ( text == null )        {            text = ""; //$NON-NLS-1$        }                if ( !text.equals( lastText ) || angle != lastAngle || cachedImage == null || cachedImage.isDisposed( ) )        {            lastText = text;            lastAngle = angle;                        if ( cachedImage != null && !cachedImage.isDisposed( ) )            {                cachedImage.dispose( );            }                        cachedImage = SwtGraphicsUtil.createRotatedTextImage( text, angle, null );        }                if ( cachedImage != null && !cachedImage.isDisposed( ) )        {            graphics.drawImage( cachedImage, r.x, r.y );        }    }        void setRotatedTextItem( RotatedTextItem item )    {        this.textItem = item;    }        void dispose( )    {        if ( cachedImage != null && !cachedImage.isDisposed( ) )        {            cachedImage.dispose( );        }    }}

核心的展示逻辑和Image UI一样, 我们就调用 SwtGraphicsUtil 来产生图像并展示它,唯一的附加逻辑就是鼠标监听, 用来监听鼠标中键的单击事件, 每一次在原来的值得基础上增加45度。

测试以下,在编辑器中,每一次单击图标就会增加45度。

我们也能实现更复杂的逻辑,这需要参考官方的GEF的接口来实现。

大家可以注意到,很多的属性并不是在属性标签视图下看到的,而是用户双击布局下的图表弹出来的对话框,这种方式更加友好。要实现这种友好的功能,我们需要实现clipse.birt.report.designer.ui.extensions.IReportItemBuilderUI扩展点

如下所示:

我们把实现类命名为org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextBuilder, 源码如下:

public class RotatedTextBuilder extends ReportItemBuilderUI{    public int open( ExtendedItemHandle handle )    {        try        {            IReportItem item = handle.getReportItem( );                        if ( item instanceof RotatedTextItem )            {                RotatedTextEditor editor = new RotatedTextEditor( Display.getCurrent( ).getActiveShell( ), (RotatedTextItem) item );                return editor.open( );            }        }        catch ( Exception e )        {            e.printStackTrace( );        }        return Window.CANCEL;    }}

这个源码很简单,因为调用了另一个 RotatedTextEditor 类来产生真正的UI

class RotatedTextEditor extends TrayDialog{    protected RotatedTextItem textItem;    protected Text txtText;    protected Scale sclAngle;    protected Label lbAngle;        protected RotatedTextEditor( Shell shell, RotatedTextItem textItem )    {        super( shell );        this.textItem = textItem;    }        protected void configureShell( Shell newShell )    {        super.configureShell( newShell );        newShell.setText( "Rotated Text Builder" ); //$NON-NLS-1$    }        protected void createTextArea( Composite parent )    {        Label lb = new Label( parent, SWT.None );        lb.setText( "Text Content:" ); //$NON-NLS-1$                txtText = new Text( parent, SWT.BORDER );        GridData gd = new GridData( GridData.FILL_HORIZONTAL );        gd.horizontalSpan = 2;        txtText.setLayoutData( gd );    }        protected Control createDialogArea( Composite parent )    {        Composite composite = new Composite( parent, SWT.NONE );        GridLayout layout = new GridLayout( 3, false );        layout.marginHeight = convertVerticalDLUsToPixels( IDialogConstants.VERTICAL_MARGIN );        layout.marginWidth = convertHorizontalDLUsToPixels( IDialogConstants.HORIZONTAL_MARGIN );        layout.verticalSpacing = convertVerticalDLUsToPixels( IDialogConstants.VERTICAL_SPACING );        layout.horizontalSpacing = convertHorizontalDLUsToPixels( IDialogConstants.HORIZONTAL_SPACING );        composite.setLayout( layout );        composite.setLayoutData( new GridData( GridData.FILL_BOTH ) );                createTextArea( composite );                Label lb = new Label( composite, SWT.None );        lb.setText( "Rotation Angle:" ); //$NON-NLS-1$                sclAngle = new Scale( composite, SWT.None );        sclAngle.setLayoutData( new GridData( GridData.FILL_HORIZONTAL ) );        sclAngle.setMinimum( 0 );        sclAngle.setMaximum( 360 );        sclAngle.setIncrement( 10 );                lbAngle = new Label( composite, SWT.None );        GridData gd = new GridData( );        gd.widthHint = 20;        lbAngle.setLayoutData( gd );                sclAngle.addSelectionListener( new SelectionListener( ) {                    public void widgetDefaultSelected( SelectionEvent e )            {                lbAngle.setText( String.valueOf( sclAngle.getSelection( ) ) );            }                        public void widgetSelected( SelectionEvent e )            {                lbAngle.setText( String.valueOf( sclAngle.getSelection( ) ) );            }        } );                applyDialogFont( composite );                initValues( );                return composite;    }        private void initValues( )    {        txtText.setText( textItem.getText( ) );        sclAngle.setSelection( textItem.getRotationAngle( ) );        lbAngle.setText( String.valueOf( textItem.getRotationAngle( ) ) );    }        protected void okPressed( )    {        try        {            textItem.setText( txtText.getText( ) );            textItem.setRotationAngle( sclAngle.getSelection( ) );        }        catch ( Exception ex )        {            ex.printStackTrace( );        }                super.okPressed( );    }}

我们在这个builder UI中仅仅提供了两个属性(文本内容和角度)来定义标签

测试一下,双击图标,出现Rotated Text Builder

内嵌的图表编辑器就是一个非常优秀的图像UI 编辑器的案例,这可能是目前最复杂的图形UI编辑器

对于属性构建器(builder), 又提供了两个扩展点: org.eclipse.birt.core.ui.tasks 和org.eclipse.birt.core.ui.taskWizards. 这两个扩展点并不直接和属性构建器builder UI 扩展点关联, 但提供了一个完全的复杂的方法为属性构建器集成对话框UI. 内嵌的图表属性构建器就是基于这种机制。在此不作更深的讲解,用户如果想更深入去学习,研究图表属性构建器的方法即可。

另外一个常见的UI就是右键菜单,为了实现用户定制的右键菜单,用户需要实现以下扩展点

org.eclipse.birt.report.designer.ui.menuBuilders, 实现类需要实现以下接口org.eclipse.birt.report.designer.ui.extensions.IMenuBuilder

同样我们把右键菜单命名为 "RotatedText" 用来绑定模型扩展点,实现类命名为 org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextMenuBuilder.

我们仅仅提供四种角度的用户快捷方式(90度,-90度,0度,180度),代码如下

public class RotatedTextMenuBuilder implements IMenuBuilder{    public void buildMenu( IMenuManager menu, List selectedList )    {        if ( selectedList != null && selectedList.size( ) == 1 && selectedList.get( 0 ) instanceof ExtendedItemHandle )        {            ExtendedItemHandle handle = (ExtendedItemHandle) selectedList.get( 0 );                        if ( !RotatedTextItem.EXTENSION_NAME.equals( handle.getExtensionName( ) ) )            {                return;            }                        RotatedTextItem item = null;            try            {                item = (RotatedTextItem) handle.getReportItem( );            }            catch ( ExtendedElementException e )            {                e.printStackTrace( );            }                        if ( item == null )            {                return;            }                        Separator separator = new Separator( "group.rotatedtext" ); //$NON-NLS-1$            if ( menu.getItems( ).length > 0 )            {                menu.insertBefore( menu.getItems( )[0].getId( ), separator );            }            else            {                menu.add( separator );            }                        menu.appendToGroup( separator.getId( ), new RotateAction( item, -90 ) );            menu.appendToGroup( separator.getId( ), new RotateAction( item, 90 ) );            menu.appendToGroup( separator.getId( ), new RotateAction( item, 0 ) );            menu.appendToGroup( separator.getId( ), new RotateAction( item, 180 ) );        }    }        static class RotateAction extends Action    {        private RotatedTextItem item;        private int angle;                RotateAction( RotatedTextItem item, int angle )        {            this.item = item;            this.angle = angle;                        setText( "Rotate as " + angle + "\u00BA" ); //$NON-NLS-1$ //$NON-NLS-2$        }                public void run( )        {            try            {                item.setRotationAngle( angle );            }            catch ( SemanticException e )            {                e.printStackTrace( );            }        }    }}

每一次用户单击右键菜单快捷方式都能触发一次变换角度的方法,测试一下,效果如下:

通过这种方式可以给用户添加一些自定义的快捷菜单,当然也可以重写原有的菜单,提供不一样的用户属性设置。

还有一个用户UI就是属性页,属性页提供了多种多样的用户属性。

用户同样可以定制这个属性编辑器UI,复用原有的属性和增加自定义属性

在开始之前,先熟悉一下属性编辑器,分为属性栏和类别栏:

一般而言属性栏的第一栏支持多类别,其他的栏目仅仅只有一个类别。

定制属性编辑页我们需要完成以下的扩展点:

和之前的扩展点不一样,这一次扩展点名字为 org.eclipse.birt.report.designer.ui.elementAdpaters,实际上这个扩展点很常用,不仅仅用在报表属性编辑页,报表设计器的很多地方都用到了这个扩展点

首先我们来定制 adaptable 类. 在本例中我们实现这个扩展点org.eclipse.birt.report.model.api.ExtendedItemHandle, 因为用户定制的报表项在模型中的展示通常是通过这个类实现的。

同上,进行必要的设置:

说明如下:

Id:唯一标识这个扩展点,这儿命名为ReportDesign.AttributeView.RotatedTextPageGenerator.

Type:类别,用来实现的UI类别,这儿是普通页的标签实现org.eclipse.birt.report.designer.ui.views.IPageGenerator.

class:实现类,必须有一个默认的无参数构造器。由于这儿我们使用工厂构建,所以这儿留空

Factory:同上例,定义扩展点工厂,命名为org.eclipse.birt.sample.reportitem.rotatedtext.views.RotatedTextPageGeneratorFactory.

singleton:是否单例模式,如果单例模式,则可以缓存重用。由于本例是工厂模式,所以false

priority:属性的优先级,为空即可。优先级越高,覆盖其他的同类型的属性。

Overwrite:用分号分隔需要重写的adapterID,本例默认为空

Comments:评论,注释,描述性信息

如果这儿既输入了类,又输入了工厂类,那么类的优先级比较高,会覆盖工厂类。

还没完,这儿要定义page需要的常量,用来限制这个扩展点仅仅能被RotatedText使用。

我们在adapter 节点的enablement 元素下创建类型为 test ,属性为 ExtendItemHandle.extensionName 值为 RotatedText的扩展点. 这个说明代表这个扩展点仅仅只能被RotatedText使用。

工厂类源码如下:

public class RotatedTextPageGeneratorFactory implements IAdapterFactory{    public Object getAdapter( Object adaptableObject, Class adapterType )    {        return new RotatedTextPageGenerator( );    }        public Class[] getAdapterList( )    {        return new Class[]{            IPageGenerator.class        };    }}

这个工厂类每次在调用时简单的产生一个实例,这个实例类需要实现这个接口org.eclipse.birt.report.designer.ui.views.IPageGeneratorinterface,但通常我们继承以下类org.eclipse.birt.report.designer.ui.views.attributes.AbstractPageGenerator class, 它提供了一些基本的样式

在本例中我们重写了General 类别的 Properties 标签, 重用了Border, Margin, Page Break等等标签 另外还加了一个标签如下:

public class RotatedTextPageGenerator extends AbstractPageGenerator{    private static final String CUSTOM_PAGE_TITLE = "Custom"; //$NON-NLS-1$    private IPropertyTabUI generalPage;        protected void buildItemContent( CTabItem item )    {        if ( itemMap.containsKey( item ) && itemMap.get( item ) == null )        {            String title = tabFolder.getSelection( ).getText( );                        if ( CUSTOM_PAGE_TITLE.equals( title ) )            {            TabPage page = new RotatedTextCustomPage( ).getPage( );            if ( page != null )            {                setPageInput( page );                refresh( tabFolder, page, true );                item.setControl( page.getControl( ) );                itemMap.put( item, page );            }            }        }        else if ( itemMap.get( item ) != null )        {            setPageInput( itemMap.get( item ) );            refresh( tabFolder, itemMap.get( item ), false );        }    }        public void refresh( )    {        createTabItems( input );                generalPage.setInput( input );        addSelectionListener( this );        ( (TabPage) generalPage ).refresh( );    }        public void createTabItems( List input )    {        if ( generalPage == null || generalPage.getControl( ).isDisposed( ) )        {            tabFolder.setLayout( new FillLayout( ) );            generalPage = AttributesUtil.buildGeneralPage( tabFolder,            new String[]{                null,                AttributesUtil.BORDER,                AttributesUtil.MARGIN,                AttributesUtil.SECTION,                AttributesUtil.VISIBILITY,                AttributesUtil.TOC,                AttributesUtil.BOOKMARK,                AttributesUtil.USERPROPERTIES,                AttributesUtil.NAMEDEXPRESSIONS,                AttributesUtil.ADVANCEPROPERTY            },            new String[]{ "General" }, //$NON-NLS-1$            new String[]{ "General" }, //$NON-NLS-1$            new AttributesUtil.PageWrapper[]{ new RotatedTextGeneralPage( ) },            input );                        CTabItem tabItem = new CTabItem( tabFolder, SWT.NONE );            tabItem.setText( ATTRIBUTESTITLE );            tabItem.setControl( generalPage.getControl( ) );        }                this.input = input;        generalPage.setInput( input );        addSelectionListener( this );        ( (TabPage) generalPage ).refresh( );                createTabItem( CUSTOM_PAGE_TITLE, ATTRIBUTESTITLE );                if ( tabFolder.getSelection( ) != null )        {            buildItemContent( tabFolder.getSelection( ) );        }    }}

在generator 类, 我们继承AbstractPageGenerator方法 重写了三个方法,createTabItems 和buildItemContent 方法是核心方法放置了用户逻辑,其他方法都是继承的,可以重用。

并不是所有的方法都可以重写或者删除,这里面要考虑依赖性和兼容性。

最后是 property page 的实现. 

public class RotatedTextGeneralPage extends AttributesUtil.PageWrapper{    protected FormToolkit toolkit;    protected Object input;    protected Composite contentpane;    private Text txtText, txtAngle;        public void buildUI( Composite parent )    {        if ( toolkit == null )        {            toolkit = new FormToolkit( Display.getCurrent( ) );            toolkit.setBorderStyle( SWT.NULL );        }                Control[] children = parent.getChildren( );                if ( children != null && children.length > 0 )        {            contentpane = (Composite) children[children.length - 1];                        GridLayout layout = new GridLayout( 2, false );            layout.marginLeft = 8;            layout.verticalSpacing = 12;            contentpane.setLayout( layout );                        toolkit.createLabel( contentpane, "Text Content:" ); //$NON-NLS-1$            txtText = toolkit.createText( contentpane, "" ); //$NON-NLS-1$            GridData gd = new GridData( );            gd.widthHint = 200;                        txtText.setLayoutData( gd );            txtText.addFocusListener( new FocusAdapter( ) {                            public void focusLost( org.eclipse.swt.events.FocusEvent e )                {                    updateModel( RotatedTextItem.TEXT_PROP );                };            } );                        toolkit.createLabel( contentpane, "Rotation Angle:" ); //$NON-NLS-1$            txtAngle = toolkit.createText( contentpane, "" ); //$NON-NLS-1$            gd = new GridData( );            gd.widthHint = 200;                        txtAngle.setLayoutData( gd );            txtAngle.addFocusListener( new FocusAdapter( ) {                            public void focusLost( org.eclipse.swt.events.FocusEvent e )                {                    updateModel( RotatedTextItem.ROTATION_ANGLE_PROP );                };            } );        }    }        public void setInput( Object input )    {        this.input = input;    }        public void dispose( )    {        if ( toolkit != null )        {            toolkit.dispose( );        }    }        private void adaptFormStyle( Composite comp )    {        Control[] children = comp.getChildren( );        for ( int i = 0; i < children.length; i++ )        {            if ( children[i] instanceof Composite )            {                adaptFormStyle( (Composite) children[i] );            }        }                toolkit.paintBordersFor( comp );        toolkit.adapt( comp );    }        protected RotatedTextItem getItem( )    {        Object element = input;                if ( input instanceof List && ( (List) input ).size( ) > 0 )        {            element = ( (List) input ).get( 0 );        }                if ( element instanceof ExtendedItemHandle )        {            try            {                return (RotatedTextItem) ( (ExtendedItemHandle) element ).getReportItem( );            }            catch ( Exception e )            {                e.printStackTrace( );            }        }                return null;    }        public void refresh( )    {        if ( contentpane != null && !contentpane.isDisposed( ) )        {            if ( toolkit == null )            {                toolkit = new FormToolkit( Display.getCurrent( ) );                toolkit.setBorderStyle( SWT.NULL );            }                        adaptFormStyle( contentpane );                        updateUI( );        }    }        public void postElementEvent( )    {        if ( contentpane != null && !contentpane.isDisposed( ) )        {            updateUI( );        }    }        private void updateModel( String prop )    {        RotatedTextItem item = getItem( );                if ( item != null )        {            try            {                if ( RotatedTextItem.ROTATION_ANGLE_PROP.equals( prop ) )                {                    item.setRotationAngle( Integer.parseInt( txtAngle.getText( ) ) );                }                else if ( RotatedTextItem.TEXT_PROP.equals( prop ) )                {                    item.setText( txtText.getText( ) );                }            }            catch ( Exception e )            {                e.printStackTrace( );            }        }    }        protected void updateUI( )    {        RotatedTextItem item = getItem( );                if ( item != null )        {            String text = item.getText( );            txtText.setText( text == null ? "" : text ); //$NON-NLS-1$                        txtAngle.setText( String.valueOf( item.getRotationAngle( ) ) );        }    }}

对于用户自定义的栏目的页面,我们仅仅做个只读的页面显示,

public class RotatedTextCustomPage extends RotatedTextGeneralPage{    private Label lbText, lbAngle;        public void buildUI( Composite parent )    {        if ( toolkit == null )        {            toolkit = new FormToolkit( Display.getCurrent( ) );            toolkit.setBorderStyle( SWT.NULL );        }                Control[] children = parent.getChildren( );                if ( children != null && children.length > 0 )        {            contentpane = (Composite) children[children.length - 1];                        GridLayout layout = new GridLayout( 2, false );            layout.marginTop = 8;            layout.marginLeft = 8;            layout.verticalSpacing = 12;            contentpane.setLayout( layout );                        toolkit.createLabel( contentpane, "Text Content:" ); //$NON-NLS-1$            lbText = toolkit.createLabel( contentpane, "" ); //$NON-NLS-1$            GridData gd = new GridData( );            gd.widthHint = 200;            lbText.setLayoutData( gd );                        toolkit.createLabel( contentpane, "Rotation Angle:" ); //$NON-NLS-1$            lbAngle = toolkit.createLabel( contentpane, "" ); //$NON-NLS-1$            gd = new GridData( );            gd.widthHint = 200;            lbAngle.setLayoutData( gd );        }    }        protected void updateUI( )    {        RotatedTextItem item = getItem( );                if ( item != null )        {            String text = item.getText( );            lbText.setText( text == null ? "" : text ); //$NON-NLS-1$            lbAngle.setText( String.valueOf( item.getRotationAngle( ) ) );        }    }}

测试一下:

切换到 Custom page, 出现只读的用户页面:

更复杂的用户定制是,我们还想在输入文本的时候调用表达式生成器和数据绑定,这个时候就不能用静态文本,而是要用表达式,所以模型和引擎都要做相应的修改。

模型的修改如下:

引擎的修改如下,修改RotatedTextPresentationImpl 类的onRowSets() 方法:

public Object onRowSets( IBaseResultSet[] results ) throws BirtException{    if ( textItem == null )    {        return null;    }        int angle = textItem.getRotationAngle( );    String text = textItem.getText( );        // XXX added to support expression    if ( results != null && results.length > 0 )    {    if ( results[0] instanceof IQueryResultSet && ( (IQueryResultSet) results[0] ).isBeforeFirst( ) )    {        ( (IQueryResultSet) results[0] ).next( );    }    text = String.valueOf( results[0].evaluate( text ) );    }    else    {    text = String.valueOf( context.evaluate( text ) );    }    // end new code        BufferedImage rotatedImage = SwingGraphicsUtil.createRotatedTextImage( text, angle, new Font( "Default", 0, 12 ) ); //$NON-NLS-1$        ByteArrayInputStream bis = null;        try    {        ImageIO.setUseCache( false );        ByteArrayOutputStream baos = new ByteArrayOutputStream( );        ImageOutputStream ios = ImageIO.createImageOutputStream( baos );                ImageIO.write( rotatedImage, "png", ios ); //$NON-NLS-1$        ios.flush( );        ios.close( );        bis = new ByteArrayInputStream( baos.toByteArray( ) );    }    catch ( IOException e )    {        e.printStackTrace( );    }    return bis;}

另外UI也要改变,在构建器中内嵌一个表达式生成器的按钮:

为了实现这个,我们需要修改 RotatedTextBuilder

public class RotatedTextBuilder extends ReportItemBuilderUI{    public int open( ExtendedItemHandle handle )    {        try        {            IReportItem item = handle.getReportItem( );                        if ( item instanceof RotatedTextItem )            {                RotatedTextEditor editor = new RotatedTextEditor2( Display.getCurrent( ).getActiveShell( ), (RotatedTextItem) item );                return editor.open( );            }        }        catch ( Exception e )        {            e.printStackTrace( );        }        return Window.CANCEL;    }}class RotatedTextEditor2 extends RotatedTextEditor{    protected RotatedTextEditor2( Shell shell, RotatedTextItem textItem )    {        super( shell, textItem );    }        protected void createTextArea( Composite parent )    {        Label lb = new Label( parent, SWT.None );        lb.setText( "Text Content:" ); //$NON-NLS-1$                txtText = new Text( parent, SWT.BORDER );        GridData gd = new GridData( GridData.FILL_HORIZONTAL );        txtText.setLayoutData( gd );                Button btnExp = new Button( parent, SWT.PUSH );        btnExp.setText( "..." ); //$NON-NLS-1$        btnExp.setToolTipText( "Invoke Expression Builder" ); //$NON-NLS-1$                btnExp.addSelectionListener( new SelectionAdapter( ) {                    public void widgetSelected( SelectionEvent event )            {                openExpression( txtText );            }        } );    }        private void openExpression( Text textControl )    {        String oldValue = textControl.getText( );                ExpressionBuilder eb = new ExpressionBuilder( textControl.getShell( ), oldValue );        eb.setExpressionProvier( new ExpressionProvider( textItem.getModelHandle( ) ) );                String result = oldValue;                if ( eb.open( ) == Window.OK )        {            result = eb.getResult( );        }                if ( !oldValue.equals( result ) )        {            textControl.setText( result );        }    }}

另外在属性编辑页也要修改实现类RotatedTextGeneralPage 如下:

public class RotatedTextGeneralPage extends AttributesUtil.PageWrapper{    //.........        public void buildUI( Composite parent )    {        if ( toolkit == null )        {            toolkit = new FormToolkit( Display.getCurrent( ) );            toolkit.setBorderStyle( SWT.NULL );        }                Control[] children = parent.getChildren( );                if ( children != null && children.length > 0 )        {            contentpane = (Composite) children[children.length - 1];                        GridLayout layout = new GridLayout( 3, false );            layout.marginLeft = 8;            layout.verticalSpacing = 12;            contentpane.setLayout( layout );                        toolkit.createLabel( contentpane, "Text Content:" ); //$NON-NLS-1$            txtText = toolkit.createText( contentpane, "" ); //$NON-NLS-1$            GridData gd = new GridData( );            gd.widthHint = 200;                        txtText.setLayoutData( gd );            txtText.addFocusListener( new FocusAdapter( ) {                            public void focusLost( org.eclipse.swt.events.FocusEvent e )                {                    updateModel( RotatedTextItem.TEXT_PROP );                };            } );                        Button btnExp = toolkit.createButton( contentpane, "...", SWT.PUSH ); //$NON-NLS-1$            btnExp.setToolTipText( "Invoke Expression Builder" ); //$NON-NLS-1$            btnExp.addSelectionListener( new SelectionAdapter( ) {                        public void widgetSelected( SelectionEvent e )            {                openExpression( txtText );            }            } );                        toolkit.createLabel( contentpane, "Rotation Angle:" ); //$NON-NLS-1$            txtAngle = toolkit.createText( contentpane, "" ); //$NON-NLS-1$            gd = new GridData( );            gd.widthHint = 200;            gd.horizontalSpan = 2;            txtAngle.setLayoutData( gd );            txtAngle.addFocusListener( new FocusAdapter( ) {                            public void focusLost( org.eclipse.swt.events.FocusEvent e )                {                    updateModel( RotatedTextItem.ROTATION_ANGLE_PROP );                };            } );        }    }        private void openExpression( Text textControl )    {        RotatedTextItem item = getItem( );                if ( item != null )        {            String oldValue = textControl.getText( );            ExpressionBuilder eb = new ExpressionBuilder( textControl.getShell( ), oldValue );            eb.setExpressionProvier( new ExpressionProvider( item.getModelHandle( ) ) );                        String result = oldValue;                        if ( eb.open( ) == Window.OK )            {                result = eb.getResult( );            }                        if ( !oldValue.equals( result ) )            {                textControl.setText( result );                updateModel( RotatedTextItem.TEXT_PROP );            }        }    }        //..........}

这个时候效果如下(右边增加了表达式生成器按钮)

测试一下:

定制表达式生成器,内容为row[“CUSTOMERNAME”]:

预览如下:

修改表达式如下:

再次预览,效果如下:

至此,我们完成了定制报表项的所有的功能,用户可以据此实现更复杂的实用的功能。

例如,如下的官方网站提供了一种扩展的excel emitter方案,能够解决目前birt分组或者合并等等导出excel出现空行的问题。

https://bitbucket.org/yaytay/spudsoft-birt-excel-emitters/wiki/Home