Saturday, May 23, 2009

We have huge amounts of memory and concurrent programming is an exception

It's quite amazing to hear Anders advocating bloat in a world where most of the world still has bad internet connections and hate large downloads, mobile phones prefer 1MB applications over 10MB apps, where RAM can be a bottleneck, where battery life is always too short and where many large organizations struggle with bad performance.

I recently had the chance to see the standard configuration for virtual servers in a large organization. In order to improve network traffic, the standard network adapter is limited to 10Mbit/sec.

Add-in Express 2009 for Office and VCL

Visual designers and components of Add-in Express in combination with a perfect Delphi compiler provide you with the best platform for Office development. Add-in Express makes it equally easy to build toolbars, menus and sub-menus for Microsoft Office 2000 – 2003, customize the Office 2007 Ribbon using the Office 2007 Ribbon designer as well as to create custom static and contextual ribbon tabs, task panes, navigation pane and reading pane, Office Menu and Quick Access Toolbar.

Add-in Express for Delphi implements all interfaces and techniques required by supported technologies, you write functional code only. Another key benefit of Add-in Express is version neutrality – you write the plug-in code once and get a secure Microsoft Office extension that works for all applications and versions.

Plug-in types: COM add-ins, smart tags, real-time data servers
Office versions: Office 2000, 2002, 2003, 2007
Applications: Outlook, Word, Excel, PowerPoint, Access, Publisher, Project, MapPoint, InfoPath, Visio
IDEs: Delphi 5, 6, 7, 2006, 2007, 2009
Windows Vista ready

Wednesday, May 20, 2009

DataSnap the fastest multi-tier development

The new approach was introduced with RAD Studio 2009 (including Delphi & C++Builder) and was based on a NON-COM/DCOM approach. The new approach used JSON (JavaScript Object Notation) http://www.json.org/ housed on a TCP/IP message layer wrapped in the DataSnap technology.

What makes this approach great is that it is simple to implement:


  1. Create a DataSnap Server

    1. Create a VCL Form application

    2. Add the 3 DataSnap Server components

    3. Connect the Server components

    4. Add a ServerModule (this is where the developer exposes business logic and data from the backend storage)

    5. Connect the ServerModule to the Server components for the wire protocol and transfer of information

  2. Create a client (generic client)

    1. Create a VCL form application, Web application, ASP.NET (Prism) application

    2. Drop a TSQLConnection component

    3. Set the connection properties (3 in all)

    4. Right-mouse click on the TSQLConnection and generate Client Proxy

    5. Use Proxy to connect to the remote server and call logic at will

    6. Expose information on client

  3. Done

This is a nine minute demo with explanations, 5 without… at best and IMO, it is the fastest multi-tier approach out there compared to JEE, CORBA, RMI, COM/DCOM, or plain old RPC.


This approach isolates the database layer to the middle tier; there is no reason for the client to know that you are even connecting to a backend database. The exposure of business logic is as simple as writing public functions and the building of the actual server is a form and three (3) components. There are no 7 helper files like with Java’s EJB, or 3 helper files with CORBA, or IDL/RIDL with COM/DCOM, and you surely don’t have to define each side of the communication like with RPC.


So there must be a downside to the technology… there always is… isn’t there? As far as I can see today, there are no advert downsides. There is no royalty to use it, it is simple to understand, it helps to reduce database connections, it very fast, it scales well, and it uses open protocols. What is not to like?


Now could there be more features added or could it even become simpler to use in future releases of the feature/functionality? Sure, but then again, if you are a diehard Delphi/C++Builder developer you already know that we don’t rest on the past implementation we are always trying to push the technology to the next level.


This leads to the next item; I’m going to be publishing the roadmap for RAD Studio very soon and also an audio recording of the presentation. There will be more details about where the technology of DataSnap is going in the future and should give every Delphi/C++Builder/Prism developer some great piece of mind to see the investment being put into our beloved product with Embarcadero!

HelpNDoc let's you generate help files in various formats from a single source

  • CHM: Compiled Microsoft Windows HTML Help;

  • HTML: Complete web help optimized for easy online browsing and search engines;

  • DOC: Complete help documentation as a Microsoft Word document or RTF file;

  • PDF: Standard Adobe portable document format documentation for easy sharing and printing;

  • HelpNDoc includes a complete WYSIWYG editor with advanced formatting and layout capabilities.



    • Advanced paragraph formatting: alignment, indentation, line spacing, tabs and text flow;

    • Advanced font customization: character spacing, offset, scaling, superscripts and subscripts;

    • Extensive image format support: JPG, PNG, GIF, BMP, ICO, EMF, WMF;

    • Advanced table support: cell padding and spacing, alignment, customizable borders and fills;

    • Complete live spell checker with custom dictionaries and automatic corrections;

  • Table of content editor with instant reorganization and customization;

  • Extremely fast topic creation, deletion and customization;

  • Advanced keywords editor: easily attach keywords with topics, categorize and manage them;

  • Automatic Topic ID and Context number generation;

  • Writing and integrating help files has never been easier thanks to HelpNDoc's advanced functionalities.



    • Live external document import: the document will be imported when the help file is generated;

    • Project or user defined variables: the variable will be replaced when the help file is generated;

    • Project-wide efficient find and replace tool: find a sentence and replace it by a variable;

    • Multiple language code generation: C/C++ headers, Delphi units, Visual Basic and Fortran modules to ease help file interaction;

    • Extensive command line support to pilot HelpNDoc from an external application, an automated processing or a batch script;

    Delphi Prism Web Services and SOAP Security

    In this article, I'll demonstrate how to use SOAP Headers as security technique for ASP.NET Web Service projects using Delphi Prism (extending the example from last month by adding a security layer to it).


    SOAP Headers

    In order to work with SOAP Headers in an ASP.NET Web Service, we have to add System.Web.Services.Protocols to the uses clause, and define a new class derived from the SoapHeader class found in the aforementioned namespace.
    The new class can be called anything, so I've decided to call it TCredentials (I know that in the .NET Framework you're not supposed to prefix everything with a "T", but since I'm a proud Delphi developer, I always use a T prefix for my types).


    It must be a public class, since we need to export it so any client importing our ASP.NET web service can also know about it.
    Adding a Username and Password field to the TCredentials class, this could lead to the following definition.



    type
    TCredentials = public class(System.Web.Services.Protocols.SoapHeader)
    public
    Username: String;
    Password: String;
    end;


    With this class definition in mind, we can add a public field (for example called token) to the public class Service, as well as a private method (for example called CheckCredentials) to check if the credentials have been received and contain the correct username/password information.
    This leads to the following modification of the Service class:



    Service = public class(System.Web.Services.WebService)
    public
    var

    token: TCredentials;

    private
    method
    CheckCredentials: Boolean;


    In order to specify that the token has to be used for certain web method (from the original set), we have to add the attribute SoapHeader to the web method, specifying the token as well as the direction in which the token is assigned (in our case only on input, so only the web service client will be writing information in the token, and the Service web service "engine" can check the token for valid credentials by looking at its value).


    Using the example from last month, we can modify some of the web methods as follows:



    public
    [WebMethod(EnableSession := true)]
    [SoapHeader('token', Direction := SoapHeaderDirection.&In)]
    method Remember(const Name,Value: String);

    [WebMethod(EnableSession := true)]
    [SoapHeader('token', Direction := SoapHeaderDirection.&In)]
    method Recall(const Name: String): String;

    [WebMethod(EnableSession := true)]
    method Forget(const Name: String);

    [WebMethod(EnableSession := true)]
    method Amnesia; // forget all
    end;


    Note that I only added the SoapHeader attribute to the Remember and Recall web methods, and not to the Forget or Amnesia methods.
    In other words: you are free to forget, but in order to remember or recall something, you need security clearance.


    Inside that Remember and Recall methods, we will have to call the CheckCredentials method before doing their actual work, in order to verify the value of the token.
    Which leads us to the implementation of CheckCredentials, which is shown below (using a hard-coded check for the values of the Username and Password properties of the token - you should obviously implement a stronger check here!).


    Note that apart from returning False is the Username and Password are incorrect, we can also raise an exception, for example if the token is not found in the SOAP header (which might happen if the web service is consumed and used by web service client applications who do not add the token to the SOAP header in the first place).



    method Service.CheckCredentials: Boolean;
    begin
    Result := False;
    if not Assigned(token) then
    raise
    ApplicationException.Create('No token in SOAP Header!');

    if (token.Username.ToLower = 'bob') and
    (token.Password.ToLower = 'swart') then Result := True
    end;


    The implementation of Remember and Recall can simple be extended by calling the CheckCredentials method before doing anything:



    method Service.Remember(const Name,Value: String);
    begin
    if
    CheckCredentials then
    Session[Name] := Value
    end;

    method Service.Recall(const Name: String): String;
    begin
    if
    CheckCredentials then
    if
    Assigned(Session[Name]) then
    Result := Session[Name].ToString
    else Result := ''
    end;


    Note that Forget and Amnesia remain unchanged, since you are always allowed to forget:



    method Service.Forget(const Name: String);
    begin
    Session.Remove(Name) // always allowed
    end;

    method Service.Amnesia;
    begin
    Session.Abandon // always allowed
    end;


    Before deploying and consuming this new secure web service, there's one thing to keep in mind about using SOAP Headers: the contents are sent in plain text over the network.
    So passing a username and password inside a SOAP Header is adding authorization capabilities, but still lacks a little security.
    For that reason, I always recommend to use SOAP Headers in combination with HTTPS and SSL.
    That way, the traffic between the web service and client is encrypted, and nobody else should be able to decypher the information sent by the client to the server (since it's encrypted with the public key from the server, and can only be decrypted by the private key of the server, so anyone sniffing the network only gets an encrypted package).


    Implementing HTTPS and SSL requires two things: first, you should purchase a certificate and install it on your web server (or ask your provider if a certificate is already available for use by your web service), and second, all URLs that are used to communicate with the web service should use https:// and not http://.


    For this example, I give you the option of exploring the difference, so I've deployed the secure web service on two different web servers: one with HTTPS and one without.
    On one server, you can access the web service via http://www.bobswart.net/MyWebService/Service.asmx and on the other (secure) web server you have to use HTTPS and the URL https://www.bobswart.nl/MyWebService/Service.asmx instead.


    Both will work the same (the MyWebService.dll code behind assembly is exactly the same on both machines), but one connection is more secure than the other, which is important when using SOAP Headers.

    Wednesday, May 13, 2009

    Move TPageControl's Tabs Using Drag and Drop in Delphi Applications

    TPageControl is a set of pages used to make a multiple page dialog box. Why not enable your users to rearrange tabs in a TPageControl component at run-time using drag and drop...
    Drop a TPageControl on a form, add several (tab) pages and handle the MouseDown, DragDrop and DragOver events of the TPageControl (PageControl1) component


    Why not enable your users to rearrange tabs in a TPageControl component at run time using drag and drop...
    Drop a TPageControl on a form, add several (tab) pages and handle the MouseDown, DragDrop and DragOver events of the TPageControl (PageControl1) component.


    procedure TMainForm.PageControl1MouseDown(Sender: TObject;
    Button: TMouseButton; Shift: TShiftState; X, Y: Integer) ;
    begin
    PageControl1.BeginDrag(False) ;
    end;

    procedure TMainForm.PageControl1DragDrop(Sender, Source: TObject; X,
    Y: Integer) ;
    const
    TCM_GETITEMRECT = $130A;
    var
    TabRect: TRect;
    j: Integer;
    begin
    if (Sender is TPageControl) then
    for j := 0 to PageControl1.PageCount - 1 do
    begin
    PageControl1.Perform(TCM_GETITEMRECT, j, LParam(@TabRect)) ;
    if PtInRect(TabRect, Point(X, Y)) then
    begin
    if PageControl1.ActivePage.PageIndex <> j then
    PageControl1.ActivePage.PageIndex := j;
    Exit;
    end;
    end;
    end;

    procedure TMainForm.PageControl1DragOver(Sender, Source: TObject; X,
    Y: Integer; State: TDragState; var Accept: Boolean) ;
    begin
    if (Sender is TPageControl) then Accept := True;
    end;

    Tuesday, May 12, 2009

    What is an Access Violation

    Every computer program uses memory for running (*). Memory is consumed by every variable in your program. It can be form, component, object, array, record, string or simple Integer. Memory can be allocated automatically for certain types of variables (such as Integer or static arrays), the other types require manual control of memory (for example, dynamic arrays). Essentially, from the point of operating system, each variable is characterized by its address (i.e. - location) and size.

    Roughly speaking, program uses 3 “types” of memory: area for global variables, the stack and the heap.
    Memory for global variables is allocated by OS loader when executable module is loading and it is freed when module is unloading. Global variables are those, which declared outside of class or any routine. The stack is used for allocating memory for local variables (which are declared in some function or procedure) and auxiliary data (such as return addresses or exception handlers). The heap is used for storing dynamic data.
    Note, that for variables of dynamic types (such as dynamic arrays, strings, objects or components) - though the variable itself is stored in global area or stack, but its data is always allocated on the heap and it (often) require manual control.

    Regardless of who allocates memory for the variable (you, manually or the compiler, automatically), memory for each variable must be allocated before its using, and later (when the variable is no longer needed) it should be freed.
    Sometimes there can be a situation, where your application trying to get access to certain memory location, which wasn’t allocated or was already released - due to bugs in your code. When such things happens - the CPU raises an exception of class EAccessViolation. The usual text for this error is as follows: “Access violation at address XXX in module ‘YYY’. Write/read of address ZZZ”. Though there is the one simple reason for this kind of error, the real situations for it can be very different.

    Sunday, May 10, 2009

    The server records for small business

    Usually the boss needs accurate reports on all divisions «here and now» – in such moments, there is neither time, nor means to acquire and implement big cooperative systems. But, there is an easy solution to such tasks.

    Maybe, you had to undertake the installation of automatic systems at film level. Any such system must accumulate data, process it and give the result. Usually, there is already a working program support, which does all this, but when the data is accumulated, sometimes it happens that, the process is not enough and there isn’t the needed report. It is okay, if it’s possible to turn to the developer and ask to do everything, but this is not possible, if the program was installed a long time ago or was bought in a box .In this case, it is important to get a limited fast result which can satisfy the boss and will not be very expensive. The tools for IT development-infrastructure are now, as a rule, limited. But because of the automation, there is need to optimize the business process now, controlling it and as a result, future economy.

    Often there is need to do a difficult job, collect data from several bases, connect them together,analyse it and give the result in a group, accessible for analysis and controlling. The data can be different – text file, data base in the formats DBF-file, electronic file table. It happens that, the similar task cannot be solved or needs a lot of money to develop a specific program. Here, one might refuse automation, or to creating a specific solution – using early operating time and collecting constructors from different program «blocks», collect everything from the module available at the market, do a minimum binding and build upon a certain task.

    The company Fast Reports (fast-report.com) has got several program products on the market, which can substantially lessen the solution to the above given tasks. This is the very missing «block», which can handle the creation of reports and the given data in the needed form. The base to all these solutions, is a powerful report generator FastReport, which has shown itself to be dependable, fast and an easy solution for data processing. 11 years of successful work at the market has allowed the company to create an easy to use program product for information systems developers and users to create needed reports

    One of FastReports flag products – Report server. It is a stand-alone HTTP-server, which can be installed on any computer on a local network under the MS Windows 2000 and above operating system. Apart from the function saving static files, it can process query concerning the creation of reports, including the prior entrance of data through web forms. The user receives the result in form of HTML-page, by using an ordinary web browser or the format needed by him, for example, PDF, Open Office, Rich Text и and many others. The printing function is accessible on any printer on the network, registered on the computer, where FastReport Server, works. Server settings are very easy. Generally, it is ready to be used immediately after installation. If on the computer, there is already a different web server installed, then, only changing the number of the working port and restarting FastReportServer, is needed. It is possible to work together with the already installed web servers like IIS and Apache with the help of the CGI- application or in form of ISAP-module which comes with the FastReport Server packet. Doing that makes the running of the individual servers unnecessary – all the functions of the report server will concentrate on ISAPI DLL. All the server settings will be saved in xml-file, supplying comments and can be changed with the help of the program-configuration. All reports, being done on the FastReport Server, are compatible with the FastReport 4 format and can be transferred from early developed programs, in which FastReport 4 was. The user can, develop his own reports with the help of the FastReport Studio, report designer. Trial version comes together with the server packet, and the license of FastReport Studio Single the buyer of server, receives as a present. It is important to note the possibility of FastReport to create several connections to the data base in one report and collect information from different data bases. Generally, any data can be used, accessible through ODBC-driver

    Not long ago, Fast Reports released an update of its server solution. FastReport Server 2.1 has become stable and faster, improved the interaction with famous web servers like Microsoft IIS and Apache. The most important innovation became the possibility to use in the MS Windows authentication , which opens the possibility for a tight integration in the Active Directory domain and make simpler the running of records account and users group – each users group can form a report which it needs – managing – someone, accountant – another. And every user can be absolutely calm because of the accuracy of the information being received – the report will be formed on its query exactly at that moment, when it is needed!

    Saturday, May 9, 2009

    Execute a Delphi Method (Procedure/Function) by Name

    When you need to convert an integer to a string, you would use the IntToStr Delphi function. When you need to display a form modally you would use the ShowModal method of a form object.

    In short, functions, procedures and methods are called by their name. Ok, I know you knew that.

    What if you need to call a function by its name as a string variable value? What if you need to use a code like:

    ExecMethod(Form1, 'ShowModal');



    type
    TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject) ;
    procedure CallMeByName(Sender: TObject) ;
    private
    procedure ExecMethod(OnObject: TObject; MethodName: string) ;
    end;

    var
    Form1: TForm1;

    type
    TExec = procedure of object;

    procedure TForm1.ExecMethod(OnObject: TObject; MethodName: string) ;
    var
    Routine: TMethod;
    Exec: TExec;
    begin
    Routine.Data := Pointer(OnObject) ;
    Routine.Code := OnObject.MethodAddress(MethodName) ;
    if NOT Assigned(Routine.Code) then Exit;
    Exec := TExec(Routine) ;
    Exec;
    end;

    procedure TForm1.CallMeByName(Sender: TObject) ;
    begin
    ShowMessage('Hello Delphi!') ;
    end;

    procedure TForm1.Button1Click(Sender: TObject) ;
    begin
    ExecMethod(Form1, 'CallMeByName') ;
    end;

    Friday, May 8, 2009

    How to Remove the "Today" Mark from the TDateTimePicker

    The TDateTimePicker component displays a list box for entering dates or times. By design it displays the label "today" at the bottom of the control allowing a user to quickly select the today's date.

    Here's how to remove the "today" marker from the TDateTimePicker. Handle the OnDropDown event as:

    uses CommCtrl, ...

    procedure TForm1.DateTimePicker1DropDown(Sender: TObject) ;
    var
    wnd: HWND;
    style: Integer;
    begin
    wnd := DateTime_GetMonthCal(DateTimePicker1.Handle) ;
    if wnd <> 0 then
    begin
    style := GetWindowLong(wnd, GWL_STYLE) ;
    SetWindowLong(wnd, GWL_STYLE, style or MCS_NOTODAY or MCS_NOTODAYCIRCLE) ;
    end;
    end;

    Finalizing DelphiLive Material

    Well today I am putting the finishing touches on my DelphiLive presentations.

    I will be presenting:
    • Power of the RTTI In Delphi
    • Moving from BDE to DBX

    I have this love hate relationship with PowerPoint, sometimes I find its the best way to communication information. However as soon as things get technical I really dislike seeing a bunch of code on slides.

    In My Power of the RTTI in Delphi Session I currently I have grand total of 3 Slides for this presentation the rest is code.

    I will be covering:

    • Interacting with Published Information
    • Dynamic Method Invocation
    • Dynamic Class Creation
    • Ideas on how you might use it your applications.

    In My BDE to DBX Session, I have several slides, but don't worry I have plenty of code and examples to show.

    One of the things I like the best about this presentation won't come till near the end, but at the State of Utah we wrote a simple DFM parser and combined it with Martin Waldenburg Delphi Parser (TmwPasLex) and wrote a simple but effective conversion utility. It saved us tons of time when converting a BDE Application to DBX. The application we converted was not a simple demo program, It has 650+ DFMs. And several more units 500+ with no associated dfm. That does not count any third party code, or our own internally developed components.

    I have updated the conversion utility to be slightly more generic as the original was specific to our needs. I will share that code with the attendees.

    I hope to see you there!

    Thursday, May 7, 2009

    UK firm splashes out to buy Borland and Compuware

    One of the grandees of the IT industry for the past 30 years, Borland Software, has been acquired by British software company Micro Focus International, in a cash deal worth £50 million ($75 million).

    At the same time, Micro Focus said it is also acquiring the application testing and automated software quality business of Compuware Corporation for £53 million ($80 million).

    Micro Focus said the two deals would give it a leading market position in the testing and automation market.

    Borland was founded way back in 1981 by three Danish citizens, but soon moved to the United States where it was headed up by the flamboyant Philippe Kahn, who led the company for most of the 1980s and 1990s.

    Borland developed a series of well-regarded software development tools during its existence, and is probably most associated with Turbo Pascal, Quattro Pro (spreadsheet), and Paradox (database). The company was also known for acquiring Ashton-Tate back in September 1991, a deal that netted it the dBase and InterBase databases, and for a time Borland was counted as a serious rival to both Microsoft and Oracle.

    But it soon found itself squeezed by its rivals and in 1998, Borland changed its name to Inprise Corporation in an effort to appeal to the enterprise applications development market. The idea was to integrate Borland's tools, Delphi, C++ Builder, and JBuilder with enterprise environment software. But the move backfired, and in January 2001, the Inprise name was abandoned and the company reverted to the "Borland" name once more.

    In February 2006 Borland announced the divestiture of its IDE division, including Delphi, JBuilder, and InterBase. At the same time it announced the planned acquisition of Segue Software, a maker of software test and quality tools. The spinoff was called CodeGear.

    From 2007, Borland concentrated on Application Lifecycle Management (ALM) and in May 2008 it sold its CodeGear division to Embarcadero Technologies.

    The deal to acquire Borland means that Micro Focus will expand its presence in the application testing market, where it already operates with its Data Express product.

    "Micro Focus's proposed acquisition of Borland represents the next logical stage in Micro Focus's growth journey," said Stephen Kelly, CEO of Micro Focus, in a statement. "Our organic performance remains strong as reflected in our trading statement also released today, and this transaction will add new scale and breadth to further develop our customer proposition, in an attractive adjacent market to our existing business."

    Newbury, Berkshire-based Micro Focus was founded back in the 1970s and is best known as a software provider that helps companies move older legacy software to more modern business systems.

    The UK company is also acquiring the application testing and automated software quality business of Compuware for $80 million, again also in cash. Compuware made its name in the testing, development and management software for programs running on mainframe computer and client-server systems.

    Micro Focus feels its products are "highly complementary to Micro Focus' core solutions as they address a logically adjacent portion of the software development and deployment value chain."

    "Acquiring the Compuware Testing and ASQ business is a logical extension to our existing application management proposition, and we see strong growth potential in this market," said Kelly.

    Both deals have been approved by the relevant boards, but still need shareholder and regulator approval. Micro Focus also said its group revenues for 2009 were expected to grow year on year by approximately 20 percent.

    Silverpoint MultiInstaller

    Silverpoint MultiInstaller is a multi component package installer for CodeGear Delphi and C++Builder, it was created to ease the components installation on the IDE.

    Silverpoint MultiInstaller can help you install multiple component packs in a few clicks.
    Just download the zips and select the destination folder, all the components will be uninstalled from the IDE if they were previously installed, unziped, patched, compiled and installed on the Delphi IDE.

    Getting Started
    To install a component pack with MultiInstaller you have to follow these steps:
    1) Read the licenses of the component packs you want to install.
    2) Get the zip files of component packs.
    3) Get the Silverpoint MultiInstaller.
    4) Get the Setup.ini file for that component pack installation or create one.

    Wednesday, May 6, 2009

    class helper to get SourceFileName from an EAssertionFailed

    In one of our applications, we have a default Config.xml file that is in our source tree, but it can be overridden by a Config.xml file relative to the application’s .EXE file.

    So we have a two step process looking for the Config.xml, and we’d like to have the full path to the source file where the sources were build.

    It seems the only way to get that, is by looking inside the Message of an EAssertionFailed exception.


    So I wrote yet another class helper to get this code to work:




    function GetConfigPathName(const Application: TApplication): string; overload;
    var
    TriedFileNames: IStringListWrapper;
    SourceFileName: string;
    begin
    TriedFileNames := TStringListWrapper.Create();
    Result := FindConfigPathName(Application, TriedFileNames);
    try
    AssertConfigFileNameExists(Result, Application.ExeName, TriedFileNames);
    except
    on E: EAssertionFailed do
    begin
    SourceFileName := E.GetSourceFileName();
    Result := FindConfigPathName(SourceFileName, TriedFileNames);
    AssertConfigFileNameExists(Result, Application.ExeName, SourceFileName, TriedFileNames);
    end;
    end;
    end;

    Note the IStringListWrapper; I’ll get back to that in a later blog-post.


    The full code of the class helper is right below.

    Just a few remarks:



    1. EAssertionFailed is in the SysUtils unit, and builds its Message by formatting a string using the SAssertError const in the SysConst unit as a mask.

    2. We use that mask to “parse” the Message from right to left. Parsing from left to right is virtually impossible, as the first string is the assertion message itself, and we don’t know how that looks like.

    3. If you are not using Delphi 2009, then replace the CharInSet call with a regular set check.




    unit AssertionFailedHelperUnit;

    interface

    uses
    SysUtils;

    type
    TAssertionFailedHelper = class helper for EAssertionFailed
    public
    function GetSourceFileName: string;
    end;

    implementation

    uses
    SysConst;

    function TAssertionFailedHelper.GetSourceFileName: string;
    var
    Mask: string;
    ExceptionMessage: string;
    OpeningParenthesesPosition: Integer;
    begin
    //no valid configuration file found relative to C:\Program Files\CodeGear\RAD Studio\6.0\bin\bds.exe (C:\develop\ActiveMQDemo\common\src\ConfigHelperUnit.pas, line 84)
    // SAssertError = '%s (%s, line %d)';
    // now create this string " (, line 0)" and use it to parse from right to left
    Mask := Format(SAssertError, ['', '', 0]);
    ExceptionMessage := Self.Message;
    // remove the closing ")"
    if (ExceptionMessage <> '') then
    begin
    Delete(ExceptionMessage, Length(ExceptionMessage), 1);
    Delete(Mask, Length(Mask), 1);
    // remove the "0" from the Mask
    Delete(Mask, Length(Mask), 1);
    end;
    while (ExceptionMessage <> '') and (CharInSet(ExceptionMessage[Length(ExceptionMessage)], ['0'..'9'])) do
    Delete(ExceptionMessage, Length(ExceptionMessage), 1);
    // from the right side, remove the ", line " portion
    while (ExceptionMessage <> '') and (ExceptionMessage[Length(ExceptionMessage)] = Mask[Length(Mask)]) do
    begin
    Delete(ExceptionMessage, Length(ExceptionMessage), 1);
    Delete(Mask, Length(Mask), 1);
    end;
    // now find the opening "("
    OpeningParenthesesPosition := Length(ExceptionMessage);
    while (ExceptionMessage <> '') and (ExceptionMessage[OpeningParenthesesPosition] <> '(') do
    Dec(OpeningParenthesesPosition);
    Delete(ExceptionMessage, 1, OpeningParenthesesPosition);
    Result := ExceptionMessage;
    end;

    end.

    Tuesday, May 5, 2009

    Understanding Delphi Project Files

    Since it is quite common for Delphi applications to share code or previously customized forms, Delphi organizes applications into what is called projects.

    A project is made up of the visual interface along with the code that activates the interface. Each project can have multiple forms, allowing us to build applications that have multiple windows. The code that is needed for a form in our project is stored in a separate Unit file that Delphi automatically associates to the form. General code that we want to be shared by all the forms in our application is placed in unit files as well. Simply put, a Delphi project is a collection of files that make up an application.

    What this means is that each project is made of one or more Form files (files with the .dfm extension) and one or more Unit files (.pas extension). We can also add resource files, and they are compiled into .RES files and linked when we compile the project.

    Project File

    Each project is made up of a single project file (.dpr). Project files contain directions for building an application. This is normally a set of simple routines which open the main form and any other forms that are set to be opened automatically and then starts the program by calling the Initialize, CreateForm and Run methods of the global Application object (which is actually a form of zero width and height, so it never actually appears on the screen).

    Note: The global variable Application, of type TApplication, is in every Delphi Windows application. Application encapsulates your application as well as provides many functions that occur in the background of the program. For instance, Application would handle how you would call a help file from the menu of your program.

    Project Unit

    Use Project - View Source to display the project file for the current project.

    Althogh you can look and edit the Project File, in most cases, you'll let Delphi maintain the DPR file. The main reason to view the project file is so we can see the units and forms that make up the project, and which form is specified as the application's main form.

    Another reason to work with the project file is when we are creating a DLL rather than a stand-alone application or need some start-up code, such as a splash screen before the main form is created by Delphi.

    Here is the default project file for a new application (containing one form: "Form1"):

    program Project1;
    uses
    Forms,
    Unit1 in 'Unit1.pas' {Form1};
    {$R *.RES}
    begin
    Application.Initialize;
    Application.CreateForm(TForm1, Form1) ;
    Application.Run;
    end.
    The program keyword identifies this unit as a program's main source unit. You can see that the unit name, Project1, follows the program keyword (Delphi gives the project a default name until you save the project with a more meaningful name). When we run a project file from the IDE, Delphi uses the name of the Project file for the name of the EXE file that it creates.

    Delphi reads the uses clause of the project file to determine which units are part of a project.

    The .dpr file is linked with the .pas file with the compile directive {$R *.RES} (in this case ‘*’ represents the root of the .pas filename rather than "any file"). This compiler directive tells Delphi to include this project's resource file. The project's resource file contains such items as the project's icon image.

    The begin..end block is the main source-code block for the project.

    Although Initialize is the first method called in the main project source code, it is not the first code that is executed in an application. The application first executes the "initialization" section of all the units used by the application.

    The Application.CreateForm statement loads the form specified in its argument. Delphi adds an Application.CreateForm statement to the project file for each form you add to the project. This code's job is to first allocate memory for the form. The statements are listed in the order the forms are added to the project. This is the order that the forms will be created in memory at runtime. If you want to change this order, do not edit the project source code. Use the ProjectOptions menu command.

    The Application.Run statement starts your application. This instruction tells the predeclared object called Application to begin processing the events that occur during the run of a program.

    An Example: Hide Main Form / Hide Taskbar Button

    The Application object's ShowMainForm property determines whether or not a form will show at startup. The only condition of setting this property is that it has to be called before the Application.Run line.
    //Presume: Form1 is the MAIN FORM
    Application.CreateForm(TForm1, Form1) ;
    Application.ShowMainForm := False;
    Application.Run;

    Design Your Delphi Program in a Way That it Keeps its Memory Usage in Check

    When writing long running applications - the kind of programs that will spend most of the day minimized to the task bar or system tray, it can become important not to let the program 'run away' with memory usage.

    Learn how to clean up the memory used by your Delphi program using the SetProcessWorkingSetSize Windows API function.

    Memory Usage of a Program / Application / Process

    Take a look at the screen shot of the Windows Task Manager...

    The two rightmost columns indicate CPU (time) usage and memory usage. If a process impacts on either of these severely, your system will slow down.

    The kind of thing that frequently impacts on CPU usage is a program that is looping (ask any programmer that has forgotten to put a "read next" statement in a file processing loop). Those sorts of problems are usually quite easily corrected.

    Memory usage on the other hand is not always apparent, and needs to be managed more than corrected. Assume for example that a capture type program is running.

    This program is used right throughout the day, possibly for telephonic capture at a help desk, or for some other reason. It just doesn’t make sense to shut it down every twenty minutes and then start it up again. It’ll be used throughout the day, although at infrequent intervals.

    If that program relies on some heavy internal processing, or has lots of art work on its forms, sooner or later its memory usage is going to grow, leaving less memory for other more frequent processes, pushing up the paging activity, and ultimately slowing down the computer.

    Monday, May 4, 2009

    How can I implement threads in my programs without using the VCL TThread object?

    I've done extensive work in multi-threaded applications. And in my experience, there
    have been times when a particular program I'm writing should be written as a
    multi-threaded application, but using the TThread object just seems like overkill.
    For instance, I write a lot of single function programs; that is, the entire functionality
    (beside the user interface portion) of the program is contained in one single execution
    procedure or function. Usually, this procedure contains a looping mechanism (e.g. FOR,
    WHILE, REPEAT) that operates on a table or an incredibly large text file (for me, that's
    on the order of 500MB-plus!). Since it's just a single procedure, using a TThread
    is just too much work for my preferences.


    For those experienced Delphi programmers, you know what happens to the user interface
    when you run a procedure with a loop in it: The application stops receiving messages.
    The most simple way of dealing with this situation is to make a call to Application.ProcessMessages
    within the body of the loop so that the application can still receive messages from
    external sources. And in many, if not most, cases, this is a perfectly valid thing to do.
    However, if some or perhaps even one of the steps within the loop take more than a couple
    of seconds to complete processing — as in the case of a query — Application.ProcessMessages
    is practically useless because the application will only receive messages at the time the
    call is made. So what you ultimately achieve is intermittent response at best. Using a
    thread, on the other hand, frees up the interface because the process is running
    completely separate from the main thread of the program where the interface resides. So
    regardless of what you execute within a loop that is running in a separate thread, your
    interface will never get locked up.


    Don't confuse the discussion above with multi-threaded user interfaces. What I'm
    talking about is executing long background threads that won't lock up your user interface
    while they run. This is an important distinction to make because it's not really
    recommended to write multi-user interfaces, because each thread that is created in the
    system has its own message queue. Thus, a message loop must be created to fetch messages
    out of the queue so they can be dispatched appropriately. The TApplication object that
    controls the UI would be the natural place to set up message loops for background threads,
    but it's not set up to detect when other threads are executed. The gist of all this is
    that the sole reason you create threads is to distribute processing of independent
    tasks. Since the UI and controls are fairly integrated, threads just don't make sense here
    because in order to make the separate threads work together, you have to synchronize them
    to work in tandem, which practically defeats threading altogether!


    I mentioned above that the TThread object is overkill for really simple threaded
    stuff. This is strictly an opinion, but experience has made me lean that way. In any case,
    what is the alternative to TThread in Delphi?


    The solution isn't so much an alternative as it is going a bit more low-level into the
    Windows API. I've said this several times before: The VCL is essentially one giant wrapper
    around the Windows API and all its complexities. But fortunately for us, Delphi provides a
    very easy way to access lower-level functionality beyond the wrapper interface with
    which it comes. And even more fortunate for us, we can create threads using a simple
    Windows API function called CreateThread to bypass the TThread object
    altogether. As you'll see below, creating threads in this fashion is incredibly easy to
    do.


    Setting Yourself Up


    There are two distinct steps for creating a thread: 1)Create the thread itself, then 2)
    Provide a function that will act as the thread entry point. The thread function or thread
    entry point
    is the function (actually the address of the function) that tells your
    thread where to start.


    Unlike a regular function, there are some specific requirements regarding the thread
    function that you have to obey:


    1. You can give the function any name you want, but it must be a function name (ie.
      function MyThreadFunc)

    2. The function must have a single formal parameter of type Pointer (I'll discuss
      this below)

    3. The function return type is always LongInt

    4. Its declaration must always be preceded by the stdcall directive. This tells the
      compiler that the function will be passing parameters in the standard Windows convention.


    Whew! That seems like a lot but it's really not as complicated as it might seem from
    the description above. Here's an example declaration:


    function MyThreadFunc(Ptr : Pointer) : LongInt; stdcall;

    That's it! Hope I didn't get you worried. The CreateThread call is a bit more
    involved, but it too is not very complicated once you understand how to call it. Here's
    its declaration, straight out of the help file:


    function CreateThread
    (lpThreadAttributes: Pointer; //Address of thread security attributes
    dwStackSize: DWORD; //Thread stack size
    lpStartAddress: TFNThreadStartRoutine;//Address of the thread function
    lpParameter: Pointer; //Input parameter for the thread
    dwCreationFlags: DWORD; //Creation flags
    var lpThreadId: DWORD): //ThreadID reference
    THandle; stdcall; //Function returns a handle to the thread

    This is not as complicated as it seems. First of all, you rarely have to set security
    attributes, so that can be set to nil. Secondly, in most cases, your stack size can
    be 0 (actually, I've never found an instance where I have to set this to a value higher
    than zero). You can optionally pass a parameter through the lpParameter argument as
    a pointer to a structure or address of a variable, but I've usually opted to use global
    variables instead (I know, this breaking a cardinal rule of structured programming, but it
    sure eases things). Lastly, I've rarely had to set creation flags unless I want my thread
    to start in a suspended state so I can do some preprocessing. For the most part, I set
    this value as zero.


    Now that I've thoroughly confused you, let's look at an example function that creates a
    thread:



    procedure TForm1.Button1Click(Sender: TObject);
    var
    thr : THandle;
    thrID : DWORD;
    begin
    FldName := ListBox1.Items[ListBox1.ItemIndex];
    thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
    if (thr = 0) then
    ShowMessage('Thread not created');
    end;

    Embarrassingly simple, right? It is. To make the thread in the function above, I
    declared two variables, thr and thrID, which stand for the handle of the
    thread and its identifier, respectively. I set a global variable that the thread function
    will access immediately before the call to CreateThread, then make the declaration,
    assigning the return value of the function to thr and inputting the address of my
    thread function, and the thread ID variable. The rest of the parameters I set to nil or 0.
    Not much to it.


    Notice that the procedure that actually makes the call is an OnClick handler for a
    button on a form. You can pretty much create a thread anywhere in your code as long as you
    set up properly. Here's the entire unit code for my program; you can use it for a
    template. This program is actually fairly simple. It adds an incremental numeric key value
    to a table called RecID, based on the record number (which makes things really easy).
    Browse the code; we'll discuss it below:



    unit main;

    interface

    uses
    Windows, Messages, SysUtils, Classes,
    Graphics, Controls, Forms, Dialogs, DB, DBTables, StdCtrls, ComCtrls,
    Buttons;

    type
    TForm1 = class(TForm)
    Edit1: TEdit;
    Label1: TLabel;
    OpenDialog1: TOpenDialog;
    SpeedButton1: TSpeedButton;
    Label2: TLabel;
    StatusBar1: TStatusBar;
    Button1: TButton;
    ListBox1: TListBox;
    procedure SpeedButton1Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    end;

    var
    Form1: TForm1;
    TblName : String;
    FldName : String;

    implementation

    {$R *.DFM}

    function CreateRecID(P : Pointer) : LongInt; stdcall;
    var
    tbl : TTable;
    I : Integer;
    ses : TSession;
    msg : String;
    begin
    Randomize; //Initialize random number generator
    I := 0;
    {Disable the Execute button so another thread can't be executed
    while this one is running}
    EnableWindow(Form1.Button1.Handle, False);

    {If you're going to access any data in a thread, you have to create a
    separate }
    ses := TSession.Create(Application);
    ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));

    tbl := TTable.Create(Application);
    with tbl do begin
    Active := False;
    SessionName := ses.SessionName;
    DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
    TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
    Open;
    First;
    try
    {Start looping structure}
    while NOT EOF do begin
    if (State <> dsEdit) then
    Edit;
    msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
    {Display message in status bar}
    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
    FieldByName(FldName).AsInteger := RecNo;
    Next;
    end;
    finally
    Free;
    ses.Free;
    EnableWindow(Form1.Button1.Handle, True);
    end;
    end;
    msg := 'Operation Complete!';
    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
    end;

    procedure TForm1.SpeedButton1Click(Sender: TObject);
    var
    tbl : TTable;
    I : Integer;
    begin
    with OpenDialog1 do
    if Execute then begin
    Edit1.Text := FileName;
    TblName := FileName;
    tbl := TTable.Create(Application);
    with tbl do begin
    Active := False;
    DatabaseName := ExtractFilePath(TblName);
    TableName := ExtractFileName(TblName);
    Open;
    LockWindowUpdate(Self.Handle);
    for I := 0 to FieldCount - 1 do begin
    ListBox1.Items.Add(Fields[I].FieldName);
    end;
    LockWindowUpdate(0);
    Free;
    end;
    end;
    end;

    procedure TForm1.Button1Click(Sender: TObject);
    var
    thr : THandle;
    thrID : DWORD;
    begin
    FldName := ListBox1.Items[ListBox1.ItemIndex];
    thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
    if (thr = 0) then
    ShowMessage('Thread not created');
    end;

    end.

    The most important function here, obviously, is the thread function, CreateRecID.
    Let's take a look at it:



    function CreateRecID(P : Pointer) : LongInt; stdcall;
    var
    tbl : TTable;
    I : Integer;
    ses : TSession;
    msg : String;
    begin
    Randomize; //Initialize random number generator
    I := 0;
    {Disable the Execute button so another thread can't be executed
    while this one is running}
    EnableWindow(Form1.Button1.Handle, False);

    {If you're going to access any data in a thread, you have to create a
    separate }
    ses := TSession.Create(Application);
    ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));

    tbl := TTable.Create(Application);
    with tbl do begin
    Active := False;
    SessionName := ses.SessionName;
    DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
    TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
    Open;
    First;
    try
    {Start looping structure}
    while NOT EOF do begin
    if (State <> dsEdit) then
    Edit;
    msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
    {Display message in status bar}
    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
    FieldByName(FldName).AsInteger := RecNo;
    Next;
    end;
    finally
    Free;
    ses.Free;
    EnableWindow(Form1.Button1.Handle, True);
    end;
    end;
    msg := 'Operation Complete!';
    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
    end;

    This is a pretty basic function. I'll leave it up to you to follow the flow of
    execution. However, let's look at some very interesting things that are happening in the
    thread function.


    First of all, notice that I created a TSession object before I created the table I was
    going to access. This is to ensure that the program will behave itself with the BDE. This
    is required any time you access a table or other data source from within the context of a
    thread. I've explained this in more detail in another article called How
    Can I Run Queries in Threads?
    Directly above that, I made a call to the Windows
    API function EnableWindow to disable the button that executes the code. I had to do
    this because since the VCL is not thread-safe, there's no guarantee I'd be able to
    successfully access the button's Enabled property safely. So I had to disable it
    using the Windows API call that performs enabling and disabling of controls.


    Moving on, notice how I update the caption of a status bar that's on the bottom of the
    my form. First, I set the value of a text variable to the message I want displayed:



    msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);

    Then I do a SendMessage, sending the WM_SETTEXT message to the status bar:



    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));

    SendMessage will send a message directly to a control and bypass the window
    procedure of the form that owns it.


    Why did I go to all this trouble? For the very same reason that I used EnableWindow
    for the button that creates the thread. But unfortunately, unlike the single call to EnableWindow,
    there's no other way to set the text of a control other than sending it the WM_SETTEXT
    message.


    The point to all this sneaking behind the VCL is that for the most part, it's not safe
    to access VCL properties or procedures in threads. In fact, the objects that are
    particularly dangerous to access from threads are those descended from TComponent. These
    comprise a large part of the VCL, so in cases where you have to perform some interaction
    with them from a thread, you'll have to use a roundabout method. But as you can see from
    the code above, it's not all that difficult.


    Of the thousands of functions in the Windows API, CreateThread is one of the
    most simple and straightforward. I spent a lot of time explaining things here, but there's
    a lot of ground I didn't cover. Use this example as a template for your thread
    exploration. Once you get the hang of it, you'll use threads in practically everything you
    do.

    Sunday, May 3, 2009

    Get the Url of a Hyperlink when the Mouse moves Over a TWebBrowser Document

    The TWebBrowser Delphi component provides access to the Web browser functionality from your Delphi applications.

    In most situations you use the TWebBrowser to display HTML documents to the user - thus creating your own version of the (Internet Explorer) Web browser. Note that the TWebBrowser can also display Word documents, for example.

    A very nice feature of a Browser is to display link information, for example, in the status bar, when the mouse hovers over a link in a document.

    The TWebBrowser does not expose an event like "OnMouseMove". Even if such an event would exist it would be fired for the TWebBrowser component - NOT for the document being displayed inside the TWebBrowser.

    In order to provide such information (and much more, as you will see in a moment) in your Delphi application using the TWebBrowser component, a technique called "events sinking" must be implemeted.

    WebBrowser Event Sink

    To navigate to a web page using the TWebBrowser component you call the Navigate method. The Document property of the TWebBrowser returns an IHTMLDocument2 value (for web documents). This interface is used to retrieve information about a document, to examine and modify the HTML elements and text within the document, and to process related events.

    To get the "href" attribute (link) of an "a" tag inside a document, while the mouse hovers over a document, you need to react on the "onmousemove" event of the IHTMLDocument2.

    Here are the steps to sink events for the currently loaded document:


    1. Sink the WebBrowser control's events in the DocumentComplete event raised by the TWebBrowser. This event is fired when the document is fully loaded into the Web Browser.
    2. Inside DocumentComplete, retrieve the WebBrowser's document object and sink the HtmlDocumentEvents interface.
    3. Handle the event you are interested in.
    4. Clear the sink in the in BeforeNavigate2 - that is when the new document is loaded in the Web Browser.

    HTML Document OnMouseMove

    Since we are interested in the HREF attribute of an A element - in order to show the URL of a link the mouse is over, we will sink the "onmousemove" event.

    The procedure to get the tag (and its attributes) "below" the mouse can be defined as:

    var
    htmlDoc : IHTMLDocument2;

    ...
    procedure TForm1.Document_OnMouseOver;
    var
    element : IHTMLElement;
    begin
    if htmlDoc = nil then Exit;

    element := htmlDoc.parentWindow.event.srcElement;

    elementInfo.Clear;

    if LowerCase(element.tagName) = 'a' then
    begin
    ShowMessage('Link, HREF : ' + element.getAttribute('href',0)]) ;
    end
    else if LowerCase(element.tagName) = 'img' then
    begin
    ShowMessage('IMAGE, SRC : ' + element.getAttribute('src',0)]) ;
    end
    else
    begin
    elementInfo.Lines.Add(Format('TAG : %s',[element.tagName])) ;
    end;
    end; (*Document_OnMouseOver*)

    As explained above, we attach to the onmousemove event of a document in the OnDocumentComplete event of a TWebBrowser:

    procedure TForm1.WebBrowser1DocumentComplete( ASender: TObject;
    const pDisp: IDispatch;
    var URL: OleVariant) ;
    begin
    if Assigned(WebBrowser1.Document) then
    begin
    htmlDoc := WebBrowser1.Document as IHTMLDocument2;

    htmlDoc.onmouseover := (TEventObject.Create(Document_OnMouseOver) as IDispatch) ;
    end;
    end; (*WebBrowser1DocumentComplete*)

    And this is where the problems arise! As you might guess the "onmousemove" event is *not* a usual event - as are those we are used to work with in Delphi.

    The "onmousemove" expects a pointer to a variable of type VARIANT of type VT_DISPATCH that receives the IDispatch interface of an object with a default method that is invoked when the event occurs.

    In order to attach a Delphi procedure to "onmousemove" you need to create a wrapper that implements IDispatch and raises your event in its Invoke method.

    Here's the TEventObject interface:

    TEventObject = class(TInterfacedObject, IDispatch)
    private
    FOnEvent: TObjectProcedure;
    protected
    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
    public
    constructor Create(const OnEvent: TObjectProcedure) ;
    property OnEvent: TObjectProcedure read FOnEvent write FOnEvent;
    end;