November 10, 2013

Delphi uses Excel to create a chart in a PDF document

Microsoft Excel exposes all his features thru a COM interface which can easily be used from a Delphi application. In this article, I use that feature to create a 3D pie chart from data available within a Delphi program and produce a PDF document.

I already talked about using Microsoft Office applications in this article. I gave examples using Word. In this article I use Excel for what it does very well: take an array of data and produce a nice chart.

Excel exposes a number of objects and this makes programming it a little bit confusing at first. The three most important objects are:

  • ExcelApplication: this is the whole Excel application.
  • WorkBook: This is a spreadsheet file
  • WorkSheet: This is a page within a workbook.

There are a lot of other objects or object collections. In this article we will use “Cells” and “Charts”. They are exactly what their names imply.

Each object or collection has a lot of properties and methods. This is where it becomes quite complex. Although most names are explicit, their use isn’t. Microsoft publishes a lot of documentation (http://msdn.microsoft.com/en-us/library/office/bb726434(v=office.12).aspx). Of course none of this documentation is written using Delphi syntax. Nevertheless it is of great help even if most samples are VBA or C#.

There are a large number of Office versions. The programming interface change slightly between each version but all in all, upward compatibility is excellent. The gold rule is to always use the oldest API version suitable for what you need to do. Because of upward compatibility, your application will generally work for the version you selected and all more recent versions.

For my sample application, I used Excel 2010. Microsoft reference is here.

In Delphi, you must use the correct components. See discussion in this article. What I said then for XE4 is valid for XE5 as well as previous versions.

My demo application is simple: A single VCL form with a single button. The button’s OnClick handler connect to excel, create a workbook having a worksheet, fill cells with simple data, create a new chart with the data, export the chart as a PDF file, close the workbook and Excel.

I hardcoded the data to keep the code simple. It is quite trivial to fetch data from anywhere, including some database. How the data is fetched is not today’s article object.

There are a number of traps when writing this kind of application. Most Office API functions have a lot of arguments. Most of them can be left empty. When you specify some argument the code may triggers an access violation or an OLE error. For example, when adding a chart, on argument specifies the chart type. I’ve found that using it will trigger an OLE error. I had to left it empty and then change the property ChartType to actually change the type of chart. This is really annoying because error messages are not explicit at all! It is a try and error play. It is time consuming.

The resulting code is very short and simple indeed:

procedure TForm1.Button1Click(Sender: TObject);
var
    WBook  : ExcelWorkbook;
    WSheet : ExcelWorksheet;
    Row    : Integer;
    WChart : ExcelChart;
    LCID   : Integer;
begin
    // Get the locale identifier for the user default locale
    LCID := GetUserDefaultLCID;
    //Connect to Excel application, this will launch excel
    ExcelApplication1.Connect;
    // Make excel visible (This is not required)
    ExcelApplication1.Visible[LCID] := TRUE;
    // Create a new workbook with a new sheet
    WBook  := ExcelApplication1.Workbooks.Add(xlWBATWorksheet, LCID);
    WSheet := WBook.ActiveSheet as ExcelWorksheet;
    // Add some data to the sheet
    WSheet.Cells.Item[1, 1] := 'Item';
    WSheet.Cells.Item[1, 2] := 'Quantity';
    for Row := 0 to High(Data) do begin
        WSheet.Cells.Item[2 + Row, 1] := Data[Row].Item;
        WSheet.Cells.Item[2 + Row, 2] := Data[Row].Quantity;
    end;
    // Create a new chart
    WChart := WBook.Charts.Add(EmptyParam, EmptyParam,
                               EmptyParam, EmptyParam, LCID) as ExcelChart;
    // Set the chart type
    WChart.ChartType := xl3DPie;
    // Set the tab name
    WChart.Location(xlLocationAsNewSheet, 'MyChart');
    // Export the chart as a PDF file
    WChart.ExportAsFixedFormat(xlTypePDF, 'MyChart.pdf', xlQualityStandard,
                               TRUE, FALSE, EmptyParam, EmptyParam,
                               TRUE,         // Open after published
                               EmptyParam);
    // Close the workbook, quit excel and disconnect
    WBook.Close(FALSE, EmptyParam, EmptyParam, LCID);
    ExcelApplication1.Quit;
    ExcelApplication1.Disconnect;
end;
--
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
This article is available from http://francois-piette.blogspot.be

October 29, 2013

Delphi XE5 update 1 is available

You already know that the update is available if you turned on auto-updates in your Delphi setup. If you didn't, now you know there is a new update...

Delphi and C++Builder XE5 Update 1 is available as an MSI patch or an ISO.

You can also download the update from the registered users web page.

--
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
This article is available from http://francois-piette.blogspot.be

October 15, 2013

Where is that MenuItem displayed ?

I had the need to display a form when the user clicks on an item in a menu. Easy! But the form, which
is small had to be displayed where the MenuItem was displayed. After Googleling a little bit, I did not
find what I needed. So I wrote it. Here is the result.

In Delphi, a TMenuItem has no property to tell where is has been displayed. I had to find how to get
that information. Among TMenuItem events is OnDrawItem. This event is called when the menu has
the OwnDraw property set to true. The corresponding event handler has a TCanvas property and a
TRect property. That was enough. A TCanvas has a handle which can be used to get hand on the
window where it is drawn. And the rectangle gives the position and size of the menu item in that
window. I had everything I needed.

But wait! When the OwnerDraw property of a menu is set to true, all menu items having an
OnDrawItem event assigned are no more drawn. That was annoying because I want it to be drawn as
usual.

Looking at TMenuItem source code, I found that drawing an item is done by AdvancedDrawItem
method. So I had to call it. There was a problem: this is a protected method and I cannot call it from
my TForm1 class. I can’t? Not really.

A protected class member can only be accessed by the class itself or any derived class, or any class in
the same source code. This feature has been considered as a bug by OOP purist but it is “as
designed”. In recent Delphi version, there is now a new “strict” keyword which restrict the
accessibility like OOP purist want it. This isn’t used for AdvancedDrawItem. To access a protected
method, you just have to declare a class deriving from the target class without changing anything. If
that declaration is located in the same source file as you code that need to call it, it can be called.

That empty class is declared like this:
  THackMenuItem = class(TMenuItem)
    // Intentionally left empty
  end;

The final code is the following:
procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
    Form2.Top  := FServiceMnuRect.Top;
    Form2.Left := FServiceMnuRect.Left;
    Form2.ShowModal;
end;

procedure TForm1.ServiceMnuDrawItem(
    Sender   : TObject;
    ACanvas  : TCanvas;
    ARect    : TRect;
    Selected : Boolean);
var
    State    : TOwnerDrawState;
    MenuItem : TMenuItem;
    Handler  : TMenuDrawItemEvent;
begin
    MenuItem := Sender as TMenuItem;
    GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
    FServiceMnuRect.Top := FServiceMnuRect.Top  + ARect.Top;

    Handler := MenuItem.OnDrawItem;
    try
        MenuItem.OnDrawItem := nil;
        if Selected then
            State := [odSelected]
        else
            State := [];
        THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
    finally
        MenuItem.OnDrawItem := Handler;
    end;
end;

ServiceMnuClick is where the secondary form is displayed by calling ShowModal. His position is first
set from the top-left corner of FServiceMnuRect (A member variable of TForm1).

ServiceMnuDrawItem has the responsibility of initializing FServiceMnuRect. Basically, it make use of
the canvas handle (which is a “HDC” or Handle to Device Context in Windows terminology) to get the
rectangle of the window behind the canvas. I used two Windows API calls: WindowFromDc and
GetWindowRect. WindowFromDc fetch the window handle given an HDC and GetWindowRect fetch
the bounding rectangle of a given window.
The bounding rectangle has to be fixed by the drawing rectangle passed to the OnDrawItem event
handler. This drawing rectangle is where the menu item is really drawn.

To call AdvancedDrawItem which drawn the menu item, I used the trick explained above. But there
was another issue: Normally AdvancedDrawItem will call OnDrawItem event handler, so we end up
with a recursive call and a nice stack overflow. So before calling AdvancedDrawItem, I save and clear
the event handler and restore it after.

Complete source file is as follow:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, Menus, Unit2;

type
  THackMenuItem = class(TMenuItem)
    // Intentionally left empty
  end;

  TForm1 = class(TForm)
    MainMenu1: TMainMenu;
    FileMnu: TMenuItem;
    OptionsMnu: TMenuItem;
    BrolMnu: TMenuItem;
    N1: TMenuItem;
    ExitMnu: TMenuItem;
    ServiceMnu: TMenuItem;
    TestMnu: TMenuItem;
    procedure ServiceMnuClick(Sender: TObject);
    procedure ServiceMnuDrawItem(Sender: TObject; ACanvas: TCanvas; ARect: TRect;
      Selected: Boolean);
  private
    FServiceMnuRect : TRect;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
    Form2.Top  := FServiceMnuRect.Top;
    Form2.Left := FServiceMnuRect.Left;
    Form2.ShowModal;
end;

procedure TForm1.ServiceMnuDrawItem(
    Sender   : TObject;
    ACanvas  : TCanvas;
    ARect    : TRect;
    Selected : Boolean);
var
    State    : TOwnerDrawState;
    MenuItem : TMenuItem;
    Handler  : TMenuDrawItemEvent;
begin
    MenuItem := Sender as TMenuItem;
    GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
    FServiceMnuRect.Top := FServiceMnuRect.Top  + ARect.Top;

    Handler := MenuItem.OnDrawItem;
    try
        MenuItem.OnDrawItem := nil;
        if Selected then
            State := [odSelected]
        else
            State := [];
        THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
    finally
MenuItem.OnDrawItem := Handler; end; end; end.



Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be