Detailed examples on how to use Comet
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.
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.
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.)
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.
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.