[Erlang Systems]

3 Examples

Detailed examples on how to use Comet

3.1 Comet Examples

This chapter describes in detail som examples on Comet usage; the simpler ones first and the most advanced last.

Four examples are given:

Source code for these are included in the distribution, in the directory comet/examples .

The abbreviations VB and VBA are used for Visual Basic and Visual Basic for Applications.

3.2 Requirements

The first example requires that Internet Explorer 4.0 or later is installed.

Example two requires Excel from Office 97 or Office 2000.

The last example can be run as it is, but to modify the COM-library, Visual C++ 5.0 or later is required.

3.3 Example One, Opening a Browser to a Specific URL

This example shows how to open a browser (Internet Explorer), and navigate through it to a specific address.

To get the COM interface for the browser, we use a tool such as OLE/COM Object Viewer, which is included in Microsoft's Windows Platform SDK, Visual C and Visual Basic.

Checking the interface for Internet Explorer, we find a couple of things that we need. First, we need the class ID. Then we need the name and parameter list of the funcions and properties required to create and use a browser.

Since starting a browser is not a performance-critical task, we can use the slowest and safest way to do it from Erlang. This means starting the erl_com as a port process, and using the IDispatch interface to access Internet Explorer.

Although Internet Explorer provides a dual interface, (that is an interface with both a method table and an IDispatch-interface), the IDispatch interface is safer and slower. Giving it a bad parameter list, returns an error code, rather than a core dump.

To use a COM object, we have to start the server (which starts the port) and start a thread. Then we can create the object, and do what we want with it.

To be able to use constants, we put the source in a module, rather than call it interactively in the Erlang shell.

%% an example of using COM with generated code
%%
%% the code was generated with these commands:
%%    erl_com:get_program(a),
%%    erl_com:gen_typelib({a, "c:\\program files\\microsoft office\\office\\mso97.dll"}),
%%    erl_com:gen_typelib({a, "c:\\program files\\microsoft office\\office\\excel8.olb"}, dispatch),
%%    erl_com:gen_interface({a, "c:\\program files\\microsoft office\\office\\excel8.olb"},
%%                          "_Application", dispatch, [{also_prefix_these, ["application"]}, {prefix, "x"}]),

-module(xc_gen).
-author('jakob@erix.ericsson.se').

-include("erl_com.hrl").
-include("xlChartType.hrl").
-include("xlChartLocation.hrl").
-include("xlRowCol.hrl").

-compile(export_all).

to_cell_col(C) when C > 26 ->
    [C / 26 + 64, C rem 26 + 64];
to_cell_col(C) ->
    [C+64].

populate_area(E, _, _, []) ->
    ok;
populate_area(E, Row, Col, [Data | Resten]) ->
    Cell= to_cell_col(Col)++integer_to_list(Row),
    Range= xapplication:range(E, Cell),
    range:value(Range, Data),
    erl_com:release(Range),
    populate_area(E, Row+1, Col, Resten).

make_graph(E, Row1, Col1, Row2, Col2, Title) ->
    Charts= xapplication:charts(E),
    NewChart= charts:add(Charts),
    erl_com:release(Charts),
    0= chart:chartType(NewChart, ?xlPieExploded),
    Chart= chart:location(NewChart, ?xlLocationAsObject, "Sheet1"),
    erl_com:release(NewChart),
    R= to_cell_col(Col1)++integer_to_list(Row1)++":"
        ++to_cell_col(Col2)++integer_to_list(Row2),
    Range= xapplication:range(E, R),
    []= chart:setSourceData(Chart, Range, ?xlColumns),
    0= chart:hasTitle(Chart, true),
    ChartTitle= chart:chartTitle(Chart),
    0= chartTitle:caption(ChartTitle, Title),
    erl_com:release(Range),
    erl_com:release(Chart),
    erl_com:release(ChartTitle),
    ok.

sample1() ->
    {ok, _Pid}= erl_com:get_program(xc_gen),
    E= erl_com:create_dispatch(xc_gen, "Excel.Application", ?CLSCTX_LOCAL_SERVER),
    0= xapplication:visible(E, true),
    Wb= xapplication:workbooks(E),
    W= workbooks:add(Wb),
    erl_com:release(W),
    erl_com:release(Wb),
    populate_area(E, 1, 1, ["Erlang", "Java", "C++"]),
    populate_area(E, 1, 2, ["25", "100", "250"]),
    ok= make_graph(E, 1, 1, 3, 2, "Bugs in source code, by language"),
    E.

The internet explorer application has a dispatch interface, that implements the IWebBrowser interface. There are a lot of methods. We use the Navigate method to open a specific URL, and the Visible property to show the browser. (By default, the browser is created invisible, like other Microsoft programs used from COM.)

3.4 Example Two, Making a Graph in Excel

In this example, we also start an instance of the Excel application. We use the program name "Excel.Application", which can be used instead of a class ID. This selects the Excel that is installed; Excel from Office 97 or Office 2000.

The easiest way to do anything with Excel is to first record a VBA macro. The resulting VBA macro is shown in figure 1. This macro is manually rewritten a bit to make it simpler. We try it out, and the result is shown in figure 2.

Now, to perform this into Erlang, we have two choices: either we can call the VB code as a subroutine using COM from Erlang, or we can reimplement the VB macro in Erlang. Since this is a user's guide, we of course choose the latter.

To get to the interfaces, we use OLE/COM Object Viewer, and get the IDL for Excel. There is an Excel type library available. We do not want all of it because it is huge. We just pick the needed interfaces, which are _Application, _Graph and _Range. We also extract some enums, which are constants used for parameters in the COM calls.

There are some tricky issues when calling COM from Erlang

First, VB handles releasing of COM interfaces implicitly. Erlang and COM does not do this, so we have to make calls to erl_com:release/1 for every interface we get. For instance, every _Range we get from the property _Application.Range, has to be released. We do this in the helper function data_to_column/3.

Secondly, when an interface is returned, it is returned as an integer. This integer is actually an index into an interface array contained in the erl_com_drv port program. When calling functions in erl_com, we have to provide both the pid and the thread number, so there is a helper function erl_com::package_interface/2, that repackages the interface integer with given thread or other interface. When giving the interface as a parameter to a COM function (through erl_com:call or erl_com:invoke), however, the interface should be converted to a pointer, which is done with the tuple notation for COM types: {vt_unknown, Interface}.

When Excel is started, we execute a series of Excel commands to enter data and to draw a graph. The commands are translated from a VBA macro that we got using Excel's standard macro recorder.

We use some constants that are needed for the Excel commands. These are taken from Visual Basic's code generation from the Excel interfaces. Although these can be fetched from Excel using COM, erl_com does not yet support this. (Future releases will include code-generation that will greatly simplify using big COM-interfaces.

-module(xc).
-author('jakob@erix.ericsson.se').

-include("erl_com.hrl").

%% enum XlChartFormat
-define(XlPieExploded, 69).
-define(XlPie, 5).

%% enum XlChartLocation
-define(xlLocationAsNewSheet, 1).
-define(xlLocationAsObject, 2).
-define(xlLocationAutomatic, 3).


%% enum XlRowCol
-define(xlColumns, 2).
-define(xlRows, 1).


-export([populate_area/4, f/3, make_graph/6, sample1/0]).

to_cell_col(C) when C > 26 ->
    [C / 26 + 64, C rem 26 + 64];
to_cell_col(C) ->
    [C+64].

populate_area(E, _, _, []) ->
    ok;
populate_area(E, Row, Col, [Data | Resten]) ->
    Cell= to_cell_col(Col)++integer_to_list(Row),
    io:format(" ~s ~n ", [Cell]),
    N= erl_com:property_get(E, "range", [Cell]),
    Range= erl_com:package_interface(E, N),
    erl_com:property_put(Range, "Value", Data),
    erl_com:release(Range),
    populate_area(E, Row+1, Col, Resten).

f(E, _, []) ->
    ok;
f(E, Startcell, [Data | Resten]) ->
    {R, C}= Startcell,
    Cell= "R"++integer_to_list(R)++"C"++integer_to_list(C),
    io:format(" ~p ~n ", [Cell]),
    f(E, {R+1, C}, Resten).

make_graph(E, Row1, Col1, Row2, Col2, Title) ->
    Charts = erl_com:package_interface(E, erl_com:property_get(E, "Charts")),
    erl_com:invoke(Charts, "Add"),
    ActiveChart= erl_com:package_interface(E, erl_com:property_get(E, "ActiveChart")),
    erl_com:property_put(ActiveChart, "ChartType", {vt_i4, ?XlPieExploded}),
    erl_com:invoke(ActiveChart, "Location", [{vt_i4, ?xlLocationAsObject}, "Sheet1"]),
    Chart= erl_com:package_interface(E, erl_com:property_get(E, "ActiveChart")),
    R= to_cell_col(Col1)++integer_to_list(Row1)++":"
        ++to_cell_col(Col2)++integer_to_list(Row2),
    io:format(" ~s ~n ", [R]),
    Range= erl_com:property_get(E, "Range", [R]),
    erl_com:invoke(Chart, "SetSourceData", [{vt_unknown, Range}, {vt_i4, ?xlColumns}]),
    erl_com:property_put(Chart, "HasTitle", true),
    ChartTitle= erl_com:package_interface(E, erl_com:property_get(Chart, "ChartTitle")),
    erl_com:property_put(ChartTitle, "Caption", Title).
                                                %erl_com:release(erl_com:package_interface(E, Range)),
                                                %erl_com:release(ActiveChart),
                                                %erl_com:release(Charts).

sample1() ->
    {ok, Pid}= erl_com:start_process(),
    T= erl_com:new_thread(Pid),
    E= erl_com:create_dispatch(T, "Excel.Application", ?CLSCTX_LOCAL_SERVER),
    erl_com:property_put(E, "Visible", true),
    Wb= erl_com:package_interface(T, erl_com:property_get(E, "Workbooks")),
    erl_com:invoke(Wb, "Add"),
    populate_area(E, 1, 1, ["Erlang", "Java", "C++"]),
    populate_area(E, 1, 2, ["25", "100", "250"]),
    make_graph(E, 1, 1, 3, 2, "Programfel i Ericssonprojekt, språkuppdelning"),
    {T, E, Wb}.

Now, from version 1.1 of comet, there is a possibility to generate code stubs that wrapps the erl_com calls in shorter and clearer names. If we use erl_com:gen_typelib(X, dispatch) to generate files, where X is an interface for an Excel object, we have a more readable form for the above:

%% an example of using COM with generated code
%%
%% the code was generated with these commands:
%%    erl_com:get_program(a),
%%    erl_com:gen_typelib({a, "c:\\program files\\microsoft office\\office\\mso97.dll"}),
%%    erl_com:gen_typelib({a, "c:\\program files\\microsoft office\\office\\excel8.olb"}, dispatch),
%%    erl_com:gen_interface({a, "c:\\program files\\microsoft office\\office\\excel8.olb"},
%%                          "_Application", dispatch, [{also_prefix_these, ["application"]}, {prefix, "x"}]),

-module(xc_gen).
-author('jakob@erix.ericsson.se').

-include("erl_com.hrl").
-include("xlChartType.hrl").
-include("xlChartLocation.hrl").
-include("xlRowCol.hrl").

-compile(export_all).

to_cell_col(C) when C > 26 ->
    [C / 26 + 64, C rem 26 + 64];
to_cell_col(C) ->
    [C+64].

populate_area(E, _, _, []) ->
    ok;
populate_area(E, Row, Col, [Data | Resten]) ->
    Cell= to_cell_col(Col)++integer_to_list(Row),
    Range= xapplication:range(E, Cell),
    range:value(Range, Data),
    erl_com:release(Range),
    populate_area(E, Row+1, Col, Resten).

make_graph(E, Row1, Col1, Row2, Col2, Title) ->
    Charts= xapplication:charts(E),
    NewChart= charts:add(Charts),
    erl_com:release(Charts),
    0= chart:chartType(NewChart, ?xlPieExploded),
    Chart= chart:location(NewChart, ?xlLocationAsObject, "Sheet1"),
    erl_com:release(NewChart),
    R= to_cell_col(Col1)++integer_to_list(Row1)++":"
        ++to_cell_col(Col2)++integer_to_list(Row2),
    Range= xapplication:range(E, R),
    []= chart:setSourceData(Chart, Range, ?xlColumns),
    0= chart:hasTitle(Chart, true),
    ChartTitle= chart:chartTitle(Chart),
    0= chartTitle:caption(ChartTitle, Title),
    erl_com:release(Range),
    erl_com:release(Chart),
    erl_com:release(ChartTitle),
    ok.

sample1() ->
    {ok, _Pid}= erl_com:get_program(xc_gen),
    E= erl_com:create_dispatch(xc_gen, "Excel.Application", ?CLSCTX_LOCAL_SERVER),
    0= xapplication:visible(E, true),
    Wb= xapplication:workbooks(E),
    W= workbooks:add(Wb),
    erl_com:release(W),
    erl_com:release(Wb),
    populate_area(E, 1, 1, ["Erlang", "Java", "C++"]),
    populate_area(E, 1, 2, ["25", "100", "250"]),
    ok= make_graph(E, 1, 1, 3, 2, "Bugs in source code, by language"),
    E.

To use the code above, we have to generate Erlang stub modules for a lot of interfaces. Checking with the OLE/COM viewer, we see that Excel uses both it's own type library, and a common library for office programs. We generate these. We use an object application a lot, however that name is used by another module in OTP. So we generate specifically the "_Application" interface, with a prefix "x", to easily use it. Now the interface for "_Application" is in the module xapplication.

When we use the functions, we take care to match every call. This is to catch errors early, remember erl_com returns errors in a {com_error, ...} tuple. Successful invoke and property_put returns [] and 0, respectively.

3.5 Example three, calling a COM object in C++

To be done.

3.6 Example four, using ActiveX Data Objects from Erlang

ActiveX Data Objects, or ADO for short, is Microsoft's new components for data access, using either Ole-DB or ODBC. They provide a nice COM wrapper for accessing SQL databases such as SQL Server, Oracle and Sybase.

The following code snippets uses generated code to access data on SQL server, in the example database "PUBS". For information on ADO, refer to Microsoft documentation.

-module(ado).
-author('jakob@erix.ericsson.se').

-compile(export_all).
%%-export([Function/Arity, ...]).

%% these are generated from ADO:
%% erl_com:create_object(Ado, "ADODB.Connection"), com_gen:gen_typelib(Ado).

-include("cursorlocationenum.hrl").
-include("cursortypeenum.hrl").
-include("locktypeenum.hrl").
-include("commandtypeenum.hrl").

select_sample() ->
    Sql= "select * from titles order by title",
    select_sample(Sql).

select_sample(Sql) ->
    {ok, Pid1} = erl_com:get_program(a),
    C= connection_class:create_object(a),
    %% Load the Driver and connect to the database.
    Strconn= "Provider=SQLOLEDB;Initial Catalog=pubs;"
        "Data Source=eomer;User Id=sa;Password=;",             
    connection:open(C, Strconn),
    %% do the select
    Rs= connection:execute(C, Sql),
    %% get Fields
    Fields= recordset:fields(Rs),
    N= fields:count(Fields),
    %% get names
    Fl= lists:map(fun(J) -> fields:item(Fields, J) end, lists:seq(0, N-1)),
    %% get each field
    Nl= lists:map(fun(F) -> field:name(F) end, Fl),
    %% read values
    Vl= read_all(Rs, Fl, recordset:eOF(Rs), [Nl]),
    erl_com:release(Fields),
    erl_com:release(Rs),
    erl_com:release(C),
    Vl.


read_row(Fl) ->
    lists:map(fun(F) -> field:value(F) end, Fl).

%% read all values
read_all(Rs, Fl, true, Acc) ->
    lists:reverse(Acc);
read_all(Rs, Fl, false, Acc0) ->
    Acc= [read_row(Fl) | Acc0],
    recordset:moveNext(Rs),
    %% limit to 100 records
    read_all(Rs, Fl, (length(Acc) > 100) or recordset:eOF(Rs), Acc).
    

map2_(F, [], _, Acc) ->
    Acc;
map2_(F, _, [], Acc) ->
    Acc;
map2_(F, [A0 | A], [B0 | B], Acc0) ->
    Acc= [F(A0, B0) | Acc0],
    map2_(F, A, B, Acc).

map2(F, A, B) ->
    lists:reverse(map2_(F, A, B, [])).

map3_(F, [], _, _, Acc) ->
    Acc;
map3_(F, _, [], _, Acc) ->
    Acc;
map3_(F, _, _, [], Acc) ->
    Acc;
map3_(F, [A0 | A], [B0 | B], [C0 | C], Acc0) ->
    Acc= [F(A0, B0, C0) | Acc0],
    map3_(F, A, B, C, Acc).

map3(F, A, B, C) ->
    lists:reverse(map3_(F, A, B, C, [])).


insert_sample() ->
    %% Start a new COM server. The application must already be started.
    {ok, Pid1} = erl_com:get_program(a),
    %% Load the Driver and connect to the database, make recordset directly
    Strconn= "Provider=SQLOLEDB;Initial Catalog=pubs;Data Source=eomer;User Id=sa;Password=;",
    Strsql= "select * from titles",
    Rs= recordset_class:create_object(a),
    recordset:open(Rs, Strsql, Strconn, ?adOpenForwardOnly, ?adLockOptimistic),
    %% Add a new row
    recordset:addNew(Rs),
    Fields= recordset:fields(Rs),
    N= fields:count(Fields),
    %% Get each field
    Fl= lists:map(fun(J) -> fields:item(Fields, J) end, lists:seq(0, N-1)),
    Nl= lists:map(fun(F) -> field:name(F) end, Fl),
    %% Fields: title_id, title, type, pub_id, price, advance, royalty, ytd_sales, notes, pubdate
    %% Have some nice values
    FVals = ["TC8789", "Kul med prolog?", "UNDECIDED   ", "1389", 8.99,
             8000, 10, 2000, "Det är inte SÅ kul med Prolog.", 0],
    %% Set values of new row
    map3(fun(F, V, Na) ->
                 io:format("name ~s value ~p ~n", [Na, V]),
                 []= field:value(F, V) end, Fl, FVals, Nl),
    %% "Commit" to the DB
    recordset:update(Rs).

delete_sample(Title_id) ->
    {ok, Pid1} = erl_com:get_program(a),
    %% Load the Driver and connect to the database, create recordset directly
    Strconn= "Provider=SQLOLEDB;Initial Catalog=pubs;Data Source=eomer;User Id=sa;Password=;",
    Strsql= "select * from titles", 
    Filter= "title_id='" ++ Title_id ++ "'",
    Rs= recordset_class:create_object(a),
    []= recordset:open(Rs, Strsql, Strconn, ?adOpenForwardOnly, ?adLockOptimistic),
    %% Set the filter, required for delete (I think?)
    []= recordset:filter(Rs, Filter),
    %% Delete
    []= recordset:delete(Rs),
    %% And "commit"
    []= recordset:update(Rs).

ADO is a nice way of accessing databases from a windows platform. The example shows both reading and changing data on the database. The code is more or less taken from Microsoft's documentation on ADO.


Copyright © 1991-2001 Ericsson Utvecklings AB